From 72defef286c9686e9d18a8ce6d9d70ecaac6589c Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 20 Feb 2026 16:27:44 +0800 Subject: [PATCH 1/3] feat: add PostHog analytics to desktop app and wire up discover UI tracking Add anonymous usage analytics to understand active users, OS distribution, app versions, server popularity, and search behavior. Desktop app: - PostHog JS SDK with env-var config (VITE_POSTHOG_KEY/VITE_POSTHOG_HOST) - Core events: app_opened, page_viewed, registry_search, server_installed, server_uninstalled - Super properties: app_version, os, platform - Opt-out toggle in Settings with Zustand persistence - Analytics silently disabled when no PostHog key is set Discover UI: - Wire up trackSearch() on debounced search input - Wire up trackServerView() on server detail pages CI: - Pass VITE_POSTHOG_KEY/HOST secrets to release build Signed-off-by: McpMux Signed-off-by: Mohammod Al Amin Ashik --- .github/workflows/release.yml | 2 + apps/desktop/.env.example | 4 + apps/desktop/package.json | 1 + apps/desktop/src/App.tsx | 35 ++- .../src/features/registry/RegistryPage.tsx | 4 + .../src/features/settings/SettingsPage.tsx | 37 ++- apps/desktop/src/hooks/useAnalytics.ts | 30 ++ apps/desktop/src/lib/analytics.ts | 70 +++++ apps/desktop/src/stores/appStore.ts | 7 + apps/desktop/src/stores/selectors.ts | 1 + apps/desktop/src/stores/types.ts | 2 + apps/desktop/src/vite-env.d.ts | 9 + pnpm-lock.yaml | 286 ++++++++++++++++++ 13 files changed, 486 insertions(+), 2 deletions(-) create mode 100644 apps/desktop/src/hooks/useAnalytics.ts create mode 100644 apps/desktop/src/lib/analytics.ts 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 index 4b24a0cd..8c6effb1 100644 --- a/apps/desktop/.env.example +++ b/apps/desktop/.env.example @@ -5,3 +5,7 @@ RUST_LOG="debug" # For production: https://api.mcpmux.com # For local dev: http://localhost:8787 MCPMUX_REGISTRY_URL="https://api.mcpmux.com" + +# PostHog analytics +VITE_POSTHOG_KEY="" +VITE_POSTHOG_HOST="https://us.i.posthog.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 From 2e52364c9b86f32c308095972f1bd39e68ae0362 Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 20 Feb 2026 16:30:17 +0800 Subject: [PATCH 2/3] chore: remove .env.example PostHog config is handled via CI secrets only. Signed-off-by: Mohammod Al Amin Ashik --- apps/desktop/.env.example | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 apps/desktop/.env.example diff --git a/apps/desktop/.env.example b/apps/desktop/.env.example deleted file mode 100644 index 8c6effb1..00000000 --- a/apps/desktop/.env.example +++ /dev/null @@ -1,11 +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" - -# PostHog analytics -VITE_POSTHOG_KEY="" -VITE_POSTHOG_HOST="https://us.i.posthog.com" From 135c761fbbd19bdec7f415c28b9f61a97e28ee5f Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 20 Feb 2026 16:45:49 +0800 Subject: [PATCH 3/3] fix: add missing mocks for analytics in App tests The App.test.tsx mock for @/hooks/useDomainEvents was missing the useDomainEvents export used by the new useAnalytics hook, causing all 12 App tests to fail in CI. Signed-off-by: Mohammod Al Amin Ashik --- tests/ts/components/App.test.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) 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) {