diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8f09d52d..ff8f361f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -177,6 +177,8 @@ jobs: TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig APPLE_SIGNING_IDENTITY: ${{ steps.apple-cert.outputs.identity }} + VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }} + VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }} with: projectPath: apps/desktop # Upload to the existing draft release diff --git a/apps/desktop/.env.example b/apps/desktop/.env.example deleted file mode 100644 index 4b24a0cd..00000000 --- a/apps/desktop/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -# Logging level (debug, info, warn, error) -RUST_LOG="debug" - -# Registry API URL -# For production: https://api.mcpmux.com -# For local dev: http://localhost:8787 -MCPMUX_REGISTRY_URL="https://api.mcpmux.com" diff --git a/apps/desktop/package.json b/apps/desktop/package.json index de84e66f..73a6809a 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -24,6 +24,7 @@ "@tauri-apps/plugin-updater": "^2", "immer": "^11.0.1", "lucide-react": "^0.561.0", + "posthog-js": "^1.351.4", "react": "^19.1.0", "react-dom": "^19.1.0", "zustand": "^5.0.9" diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 0cef2153..d98841ff 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -33,7 +33,9 @@ import { ServerInstallModal } from '@/components/ServerInstallModal'; import { SpaceSwitcher } from '@/components/SpaceSwitcher'; import { ConnectIDEs } from '@/components/ConnectIDEs'; import { useDataSync } from '@/hooks/useDataSync'; -import { useAppStore, useActiveSpace, useViewSpace, useTheme } from '@/stores'; +import { useAnalytics } from '@/hooks/useAnalytics'; +import { initAnalytics, capture, optIn, optOut } from '@/lib/analytics'; +import { useAppStore, useActiveSpace, useViewSpace, useTheme, useAnalyticsEnabled } from '@/stores'; import { RegistryPage } from '@/features/registry'; import { FeatureSetsPage } from '@/features/featuresets'; import { ClientsPage } from '@/features/clients'; @@ -111,6 +113,7 @@ function AppContent() { const setTheme = useAppStore((state) => state.setTheme); const activeSpace = useActiveSpace(); const viewSpace = useViewSpace(); + const analyticsEnabled = useAnalyticsEnabled(); // App version from Rust backend const [appVersion, setAppVersion] = useState(''); @@ -120,6 +123,36 @@ function AppContent() { .catch((err) => console.error('Failed to get version:', err)); }, []); + // Initialize analytics once we have the app version + useEffect(() => { + if (!appVersion) return; + initAnalytics(appVersion); + if (analyticsEnabled) { + optIn(); + capture('app_opened'); + } else { + optOut(); + } + }, [appVersion]); // eslint-disable-line react-hooks/exhaustive-deps + + // Sync opt-in/out when user toggles analytics + useEffect(() => { + if (!appVersion) return; + if (analyticsEnabled) { + optIn(); + } else { + optOut(); + } + }, [analyticsEnabled, appVersion]); + + // Track domain events (server install/uninstall) + useAnalytics(); + + // Track page navigation + useEffect(() => { + capture('page_viewed', { page: activeNav }); + }, [activeNav]); + // Gateway status for sidebar footer const [gatewayUrl, setGatewayUrl] = useState(null); const loadGatewayUrl = useCallback(async () => { diff --git a/apps/desktop/src/features/registry/RegistryPage.tsx b/apps/desktop/src/features/registry/RegistryPage.tsx index f7ad77c2..053ecc89 100644 --- a/apps/desktop/src/features/registry/RegistryPage.tsx +++ b/apps/desktop/src/features/registry/RegistryPage.tsx @@ -11,6 +11,7 @@ import { useRegistryStore } from '../../stores/registryStore'; import { ServerCard } from './ServerCard'; import { ServerDetailModal } from './ServerDetailModal'; import { useViewSpace } from '@/stores'; +import { capture } from '@/lib/analytics'; export function RegistryPage() { const { @@ -85,6 +86,9 @@ export function RegistryPage() { const timer = setTimeout(() => { if (localSearch !== searchQuery) { search(localSearch); + if (localSearch.trim()) { + capture('registry_search', { query: localSearch.trim() }); + } } }, 300); return () => clearTimeout(timer); diff --git a/apps/desktop/src/features/settings/SettingsPage.tsx b/apps/desktop/src/features/settings/SettingsPage.tsx index d8a1df43..1fda7757 100644 --- a/apps/desktop/src/features/settings/SettingsPage.tsx +++ b/apps/desktop/src/features/settings/SettingsPage.tsx @@ -22,8 +22,9 @@ import { Minimize2, XCircle, Trash2, + BarChart3, } from 'lucide-react'; -import { useAppStore, useTheme } from '@/stores'; +import { useAppStore, useTheme, useAnalyticsEnabled } from '@/stores'; import { UpdateChecker } from './UpdateChecker'; interface StartupSettings { @@ -35,6 +36,8 @@ interface StartupSettings { export function SettingsPage() { const theme = useTheme(); const setTheme = useAppStore((state) => state.setTheme); + const analyticsEnabled = useAnalyticsEnabled(); + const setAnalyticsEnabled = useAppStore((state) => state.setAnalyticsEnabled); const [logsPath, setLogsPath] = useState(''); const [openingLogs, setOpeningLogs] = useState(false); const { toasts, success, error } = useToast(); @@ -302,6 +305,38 @@ export function SettingsPage() { + {/* Analytics Section */} + + + + + Analytics + + + Help improve McpMux by sharing anonymous usage data. No personal information is collected. + + + +
+
+ +
+ +

+ Sends anonymous data like app version, OS, and feature usage to help us prioritize improvements. + Location is approximated from IP by PostHog. No credentials or server configurations are shared. +

+
+
+ +
+
+
+ {/* Logs Section */} diff --git a/apps/desktop/src/hooks/useAnalytics.ts b/apps/desktop/src/hooks/useAnalytics.ts new file mode 100644 index 00000000..14332dc3 --- /dev/null +++ b/apps/desktop/src/hooks/useAnalytics.ts @@ -0,0 +1,30 @@ +/** + * Hook that subscribes to domain events and forwards core metrics to PostHog. + * + * Tracked: server_installed, server_uninstalled + * (app_opened, registry_search, version, OS, location are handled elsewhere) + */ + +import { useEffect } from 'react'; +import { capture } from '@/lib/analytics'; +import { useDomainEvents } from './useDomainEvents'; +import type { ServerChangedPayload } from './useDomainEvents'; + +export function useAnalytics() { + const { subscribe } = useDomainEvents(); + + useEffect(() => { + return subscribe('server-changed', (payload: ServerChangedPayload) => { + if (payload.action === 'installed') { + capture('server_installed', { + server_id: payload.server_id, + server_name: payload.server_name, + }); + } else if (payload.action === 'uninstalled') { + capture('server_uninstalled', { + server_id: payload.server_id, + }); + } + }); + }, [subscribe]); +} diff --git a/apps/desktop/src/lib/analytics.ts b/apps/desktop/src/lib/analytics.ts new file mode 100644 index 00000000..a85e37e9 --- /dev/null +++ b/apps/desktop/src/lib/analytics.ts @@ -0,0 +1,70 @@ +/** + * PostHog analytics for McpMux desktop app. + * + * - Anonymous by default (no PII collected) + * - Opt-out toggle available in Settings + * - PostHog auto-captures geolocation from IP + */ + +import posthog from 'posthog-js'; + +const POSTHOG_KEY = import.meta.env.VITE_POSTHOG_KEY ?? ''; +const POSTHOG_HOST = import.meta.env.VITE_POSTHOG_HOST ?? 'https://us.i.posthog.com'; + +let initialized = false; + +/** Initialize PostHog with app-level super properties. */ +export function initAnalytics(appVersion: string) { + if (initialized || typeof window === 'undefined') return; + if (!POSTHOG_KEY) return; + + posthog.init(POSTHOG_KEY, { + api_host: POSTHOG_HOST, + person_profiles: 'identified_only', + capture_pageview: false, + capture_pageleave: false, + autocapture: false, + persistence: 'localStorage', + }); + + // Super properties sent with every event + posthog.register({ + app_version: appVersion, + os: getOS(), + platform: 'desktop', + }); + + initialized = true; +} + +/** Capture an analytics event (no-op if not initialized or opted out). */ +export function capture(event: string, properties?: Record) { + if (!initialized) return; + posthog.capture(event, properties); +} + +/** Opt out of analytics. */ +export function optOut() { + if (!initialized) return; + posthog.opt_out_capturing(); +} + +/** Opt back in to analytics. */ +export function optIn() { + if (!initialized) return; + posthog.opt_in_capturing(); +} + +/** Check if user has opted out. */ +export function hasOptedOut(): boolean { + if (!initialized) return false; + return posthog.has_opted_out_capturing(); +} + +function getOS(): string { + const ua = navigator.userAgent.toLowerCase(); + if (ua.includes('win')) return 'windows'; + if (ua.includes('mac')) return 'macos'; + if (ua.includes('linux')) return 'linux'; + return 'unknown'; +} diff --git a/apps/desktop/src/stores/appStore.ts b/apps/desktop/src/stores/appStore.ts index f3030da2..7f5b1618 100644 --- a/apps/desktop/src/stores/appStore.ts +++ b/apps/desktop/src/stores/appStore.ts @@ -9,6 +9,7 @@ const initialState: AppState = { viewSpaceId: null, sidebarCollapsed: false, theme: 'system', + analyticsEnabled: true, loading: { spaces: false, servers: false, @@ -95,6 +96,11 @@ export const useAppStore = create()( state.theme = theme; }), + setAnalyticsEnabled: (enabled) => + set((state) => { + state.analyticsEnabled = enabled; + }), + // Loading setLoading: (key, value) => set((state) => { @@ -110,6 +116,7 @@ export const useAppStore = create()( activeSpaceId: state.activeSpaceId, sidebarCollapsed: state.sidebarCollapsed, theme: state.theme, + analyticsEnabled: state.analyticsEnabled, }), } ) diff --git a/apps/desktop/src/stores/selectors.ts b/apps/desktop/src/stores/selectors.ts index 77cefe3c..756b3219 100644 --- a/apps/desktop/src/stores/selectors.ts +++ b/apps/desktop/src/stores/selectors.ts @@ -7,6 +7,7 @@ export const useActiveSpaceId = () => useAppStore((state) => state.activeSpaceId export const useViewSpaceId = () => useAppStore((state) => state.viewSpaceId); export const useTheme = () => useAppStore((state) => state.theme); export const useSidebarCollapsed = () => useAppStore((state) => state.sidebarCollapsed); +export const useAnalyticsEnabled = () => useAppStore((state) => state.analyticsEnabled); // Computed selectors export const useActiveSpace = (): Space | null => { diff --git a/apps/desktop/src/stores/types.ts b/apps/desktop/src/stores/types.ts index 99d4feab..48686aae 100644 --- a/apps/desktop/src/stores/types.ts +++ b/apps/desktop/src/stores/types.ts @@ -9,6 +9,7 @@ export interface AppState { // UI state sidebarCollapsed: boolean; theme: 'light' | 'dark' | 'system'; + analyticsEnabled: boolean; // Loading states loading: { @@ -29,6 +30,7 @@ export interface AppActions { // UI toggleSidebar: () => void; setTheme: (theme: 'light' | 'dark' | 'system') => void; + setAnalyticsEnabled: (enabled: boolean) => void; // Loading setLoading: (key: keyof AppState['loading'], value: boolean) => void; diff --git a/apps/desktop/src/vite-env.d.ts b/apps/desktop/src/vite-env.d.ts index 11f02fe2..14f0edb7 100644 --- a/apps/desktop/src/vite-env.d.ts +++ b/apps/desktop/src/vite-env.d.ts @@ -1 +1,10 @@ /// + +interface ImportMetaEnv { + readonly VITE_POSTHOG_KEY: string; + readonly VITE_POSTHOG_HOST: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76f79091..59e9be94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,6 +122,9 @@ importers: lucide-react: specifier: ^0.561.0 version: 0.561.0(react@19.2.3) + posthog-js: + specifier: ^1.351.4 + version: 1.351.4 react: specifier: ^19.1.0 version: 19.2.3 @@ -1194,6 +1197,78 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@opentelemetry/api-logs@0.208.0': + resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.2.0': + resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.5.1': + resolution: {integrity: sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-http@0.208.0': + resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.208.0': + resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.208.0': + resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.2.0': + resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/resources@2.5.1': + resolution: {integrity: sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.208.0': + resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.2.0': + resolution: {integrity: sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.2.0': + resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.39.0': + resolution: {integrity: sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==} + engines: {node: '>=14'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1203,9 +1278,45 @@ packages: engines: {node: '>=18'} hasBin: true + '@posthog/core@1.23.1': + resolution: {integrity: sha512-GViD5mOv/mcbZcyzz3z9CS0R79JzxVaqEz4sP5Dsea178M/j3ZWe6gaHDZB9yuyGfcmIMQ/8K14yv+7QrK4sQQ==} + + '@posthog/types@1.351.4': + resolution: {integrity: sha512-PMrNdOGVIiUczixF+AkodxIigAr3OM2LMfs565uTPZaIUip7S+njoaZRoIT66VJJviWtbLtzJHR49YJtIRIiWQ==} + '@promptbook/utils@0.69.5': resolution: {integrity: sha512-xm5Ti/Hp3o4xHrsK9Yy3MS6KbDxYbq485hDsFvxqaNA7equHLPdo8H8faTitTeb14QCDfLW4iwCxdVYu5sn6YQ==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@puppeteer/browsers@2.11.2': resolution: {integrity: sha512-GBY0+2lI9fDrjgb5dFL9+enKXqyOPok9PXg/69NVkjW3bikbK9RQrNrI3qccQXmDNN7ln4j/yL89Qgvj/tfqrw==} engines: {node: '>=18'} @@ -2145,6 +2256,9 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + core-js@3.48.0: + resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -2311,6 +2425,9 @@ packages: dompurify@3.2.7: resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -2592,6 +2709,9 @@ packages: picomatch: optional: true + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -3213,6 +3333,9 @@ packages: resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} engines: {node: '>= 0.6.0'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} @@ -3599,6 +3722,12 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + posthog-js@1.351.4: + resolution: {integrity: sha512-vLSYHMN31FGs1cTJ/SCW0aCLiV6K6ZW63Q1K3B16iWRBVTwz/kg6sJZi6Z/oFeKOuy9I5K/LbVLDBCaTTlcESA==} + + preact@10.28.4: + resolution: {integrity: sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3692,6 +3821,10 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -4344,6 +4477,9 @@ packages: wdio-video-reporter@6.2.0: resolution: {integrity: sha512-b/LHQFluxIcP7foWW0gJ/5W6x1NsTVzJ2yiZZ+W4ETtYBUFU0Xfd2fYVC3q3Uxb3He6QkuNT+h0xCefJSZRyVg==} + web-vitals@5.1.0: + resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==} + webdriver@9.23.2: resolution: {integrity: sha512-HZy3eydZbmex0pbyLwHaDsAyZ+S+V4XQTdGK/nAOi4uPa74U6yT9vXqtb+3B+5/LDM7L8kTD6Z3b1y4gB4pmTw==} engines: {node: '>=18.20.0'} @@ -5278,6 +5414,82 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@opentelemetry/api-logs@0.208.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + + '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/resources@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/semantic-conventions@1.39.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -5285,10 +5497,39 @@ snapshots: dependencies: playwright: 1.58.1 + '@posthog/core@1.23.1': + dependencies: + cross-spawn: 7.0.6 + + '@posthog/types@1.351.4': {} + '@promptbook/utils@0.69.5': dependencies: spacetrim: 0.11.59 + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@puppeteer/browsers@2.11.2': dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -6371,6 +6612,8 @@ snapshots: cookie@0.7.2: {} + core-js@3.48.0: {} + core-util-is@1.0.3: {} cors@2.8.6: @@ -6519,6 +6762,10 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -6912,6 +7159,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.4.8: {} + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -7519,6 +7768,8 @@ snapshots: loglevel@1.9.2: {} + long@5.3.2: {} + loupe@3.2.1: {} lru-cache@10.4.3: {} @@ -7871,6 +8122,24 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + posthog-js@1.351.4: + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@posthog/core': 1.23.1 + '@posthog/types': 1.351.4 + core-js: 3.48.0 + dompurify: 3.3.1 + fflate: 0.4.8 + preact: 10.28.4 + query-selector-shadow-dom: 1.0.1 + web-vitals: 5.1.0 + + preact@10.28.4: {} + prelude-ls@1.2.1: {} prettier-plugin-tailwindcss@0.6.14(prettier@3.7.4): @@ -7901,6 +8170,21 @@ snapshots: progress@2.0.3: {} + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 25.0.1 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -8665,6 +8949,8 @@ snapshots: - expect-webdriverio - webdriverio + web-vitals@5.1.0: {} + webdriver@9.23.2: dependencies: '@types/node': 20.19.30 diff --git a/tests/ts/components/App.test.tsx b/tests/ts/components/App.test.tsx index 2c9fb8ab..e32a2e04 100644 --- a/tests/ts/components/App.test.tsx +++ b/tests/ts/components/App.test.tsx @@ -68,6 +68,15 @@ vi.mock('@/hooks/useDomainEvents', () => ({ gatewayEventCallbacks.push(cb); }), useServerStatusEvents: vi.fn(), + useDomainEvents: vi.fn(() => ({ subscribe: vi.fn(() => vi.fn()) })), +})); + +vi.mock('@/lib/analytics', () => ({ + initAnalytics: vi.fn(), + capture: vi.fn(), + optOut: vi.fn(), + optIn: vi.fn(), + hasOptedOut: vi.fn(() => false), })); function fireGatewayEvent(payload: GatewayPayload) {