diff --git a/apps/desktop/src/main/managers/onboarding-manager.ts b/apps/desktop/src/main/managers/onboarding-manager.ts index d0e9e89..f0e8cc8 100644 --- a/apps/desktop/src/main/managers/onboarding-manager.ts +++ b/apps/desktop/src/main/managers/onboarding-manager.ts @@ -41,10 +41,7 @@ export class OnboardingManager { await this.windowManager.createOrShowOnboardingWindow(); // Track onboarding started event - this.onboardingService.trackEvent("onboarding_started", { - version: 1, - timestamp: new Date().toISOString(), - }); + this.onboardingService.trackOnboardingStarted(process.platform); } /** @@ -93,11 +90,11 @@ export class OnboardingManager { // Track abandonment event const currentState = await this.onboardingService.getOnboardingState(); - this.onboardingService.trackEvent("onboarding_abandoned", { - last_screen: - currentState?.skippedScreens?.[currentState.skippedScreens.length - 1], - timestamp: new Date().toISOString(), - }); + const lastScreen = + currentState?.lastVisitedScreen || + currentState?.skippedScreens?.[currentState.skippedScreens.length - 1] || + "unknown"; + this.onboardingService.trackOnboardingAbandoned(lastScreen); // Close the onboarding window const onboardingWindow = this.windowManager.getOnboardingWindow(); @@ -169,11 +166,4 @@ export class OnboardingManager { getFeatureFlags(): any { return this.onboardingService.getFeatureFlags(); } - - /** - * Track an onboarding event - */ - trackEvent(eventName: string, properties?: Record): void { - this.onboardingService.trackEvent(eventName, properties); - } } diff --git a/apps/desktop/src/renderer/onboarding/App.tsx b/apps/desktop/src/renderer/onboarding/App.tsx index 298951f..5f4713e 100644 --- a/apps/desktop/src/renderer/onboarding/App.tsx +++ b/apps/desktop/src/renderer/onboarding/App.tsx @@ -46,12 +46,24 @@ export function App() { const [discoveryDetails, setDiscoveryDetails] = useState(""); // Hooks - const { state, isLoading, savePreferences, completeOnboarding, trackEvent } = + const { state, isLoading, savePreferences, completeOnboarding } = useOnboardingState(); // tRPC queries const featureFlagsQuery = api.onboarding.getFeatureFlags.useQuery(); const skippedScreensQuery = api.onboarding.getSkippedScreens.useQuery(); + + // Telemetry mutations + const trackOnboardingStarted = + api.onboarding.trackOnboardingStarted.useMutation(); + const trackOnboardingScreenViewed = + api.onboarding.trackOnboardingScreenViewed.useMutation(); + const trackOnboardingFeaturesSelected = + api.onboarding.trackOnboardingFeaturesSelected.useMutation(); + const trackOnboardingDiscoverySelected = + api.onboarding.trackOnboardingDiscoverySelected.useMutation(); + const trackOnboardingModelSelected = + api.onboarding.trackOnboardingModelSelected.useMutation(); const utils = api.useUtils(); // Screen order - can be modified based on feature flags @@ -155,10 +167,9 @@ export function App() { } // Track onboarding started event (T034) - trackEvent("onboarding_started", { + trackOnboardingStarted.mutate({ platform: platformResult, resumed: !!state?.lastVisitedScreen, - // Enum values are strings at runtime, safe for telemetry resumedFrom: state?.lastVisitedScreen, }); }; @@ -166,7 +177,7 @@ export function App() { initialize(); }, [ checkPermissionsWithResult, - trackEvent, + trackOnboardingStarted, utils, state?.lastVisitedScreen, getActiveScreens, @@ -185,12 +196,17 @@ export function App() { // Track screen views (T035) useEffect(() => { - trackEvent("onboarding_screen_viewed", { - screen: currentScreen, // OnboardingScreen enum, string value at runtime + trackOnboardingScreenViewed.mutate({ + screen: currentScreen, index: getCurrentScreenIndex(), total: getTotalScreens(), }); - }, [currentScreen, trackEvent, getCurrentScreenIndex, getTotalScreens]); + }, [ + currentScreen, + trackOnboardingScreenViewed, + getCurrentScreenIndex, + getTotalScreens, + ]); // Navigation functions (T028 - Back navigation) const navigateBack = useCallback(() => { @@ -234,7 +250,7 @@ export function App() { // Handle feature interests selection (T036) const handleFeatureInterests = async (interests: FeatureInterest[]) => { - trackEvent("onboarding_features_selected", { + trackOnboardingFeaturesSelected.mutate({ features: interests, count: interests.length, }); @@ -247,7 +263,7 @@ export function App() { source: DiscoverySource, details?: string, ) => { - trackEvent("onboarding_discovery_selected", { + trackOnboardingDiscoverySelected.mutate({ source, details, }); @@ -261,7 +277,7 @@ export function App() { modelType: ModelType, recommendationFollowed: boolean, ) => { - trackEvent("onboarding_model_selected", { + trackOnboardingModelSelected.mutate({ model_type: modelType, recommendation_followed: recommendationFollowed, }); diff --git a/apps/desktop/src/renderer/onboarding/hooks/useOnboardingState.ts b/apps/desktop/src/renderer/onboarding/hooks/useOnboardingState.ts index a0686c8..3c230c2 100644 --- a/apps/desktop/src/renderer/onboarding/hooks/useOnboardingState.ts +++ b/apps/desktop/src/renderer/onboarding/hooks/useOnboardingState.ts @@ -16,7 +16,6 @@ interface UseOnboardingStateReturn { error: Error | null; savePreferences: (preferences: OnboardingPreferences) => Promise; completeOnboarding: (finalState: OnboardingState) => Promise; - trackEvent: (eventName: string, properties?: Record) => void; resetOnboarding: () => Promise; } @@ -32,7 +31,8 @@ export function useOnboardingState(): UseOnboardingStateReturn { const getStateQuery = api.onboarding.getState.useQuery(); const savePreferencesMutation = api.onboarding.savePreferences.useMutation(); const completeMutation = api.onboarding.complete.useMutation(); - const trackEventMutation = api.onboarding.trackEvent.useMutation(); + const trackOnboardingCompleted = + api.onboarding.trackOnboardingCompleted.useMutation(); const resetMutation = api.onboarding.reset.useMutation(); // Load initial state @@ -90,18 +90,6 @@ export function useOnboardingState(): UseOnboardingStateReturn { [savePreferencesMutation], ); - // Track analytics event - const trackEvent = useCallback( - (eventName: string, properties?: Record) => { - // Fire and forget - we don't wait for the result - trackEventMutation.mutate({ - eventName, - properties: properties || {}, - }); - }, - [trackEventMutation], - ); - // Complete onboarding const completeOnboarding = useCallback( async (finalState: OnboardingState) => { @@ -113,11 +101,14 @@ export function useOnboardingState(): UseOnboardingStateReturn { } // Track completion event - trackEvent("onboarding_completed", { - features_selected: finalState.featureInterests, + trackOnboardingCompleted.mutate({ + version: finalState.completedVersion, + features_selected: finalState.featureInterests || [], discovery_source: finalState.discoverySource, model_type: finalState.selectedModelType, - recommendation_followed: finalState.modelRecommendation?.followed, + recommendation_followed: + finalState.modelRecommendation?.followed || false, + skipped_screens: finalState.skippedScreens, }); // Handle relaunch if needed @@ -135,7 +126,7 @@ export function useOnboardingState(): UseOnboardingStateReturn { throw err; } }, - [completeMutation, trackEvent], + [completeMutation, trackOnboardingCompleted], ); // Reset onboarding (for testing) @@ -158,7 +149,6 @@ export function useOnboardingState(): UseOnboardingStateReturn { error, savePreferences, completeOnboarding, - trackEvent, resetOnboarding, }; } diff --git a/apps/desktop/src/services/onboarding-service.ts b/apps/desktop/src/services/onboarding-service.ts index 5450d5f..a5abc45 100644 --- a/apps/desktop/src/services/onboarding-service.ts +++ b/apps/desktop/src/services/onboarding-service.ts @@ -268,16 +268,16 @@ export class OnboardingService { await this.saveOnboardingState(completeState); - // Track completion event if telemetry is enabled - if (this.telemetryService.isEnabled()) { - this.telemetryService.track("onboarding_completed", { - version: completeState.completedVersion, - features: completeState.featureInterests, - model: completeState.selectedModelType, - followed_recommendation: completeState.modelRecommendation?.followed, - skipped_screens: completeState.skippedScreens, - }); - } + // Track completion event + this.telemetryService.trackOnboardingCompleted({ + version: completeState.completedVersion, + features_selected: completeState.featureInterests || [], + discovery_source: completeState.discoverySource, + model_type: completeState.selectedModelType, + recommendation_followed: + completeState.modelRecommendation?.followed || false, + skipped_screens: completeState.skippedScreens, + }); logger.main.info("Onboarding completed successfully"); } catch (error) { @@ -508,15 +508,24 @@ export class OnboardingService { } /** - * Track onboarding event + * Track onboarding started event */ - trackEvent(eventName: string, properties?: Record): void { - if (this.telemetryService.isEnabled()) { - this.telemetryService.track(eventName, { - ...properties, - onboarding_session: this.currentState, - }); - } + trackOnboardingStarted(platform: string): void { + this.telemetryService.trackOnboardingStarted({ + platform, + resumed: !!this.currentState?.lastVisitedScreen, + resumedFrom: this.currentState?.lastVisitedScreen, + }); + } + + /** + * Track onboarding abandoned event + */ + trackOnboardingAbandoned(lastScreen: string): void { + this.telemetryService.trackOnboardingAbandoned({ + last_screen: lastScreen, + timestamp: new Date().toISOString(), + }); } /** diff --git a/apps/desktop/src/services/telemetry-service.ts b/apps/desktop/src/services/telemetry-service.ts index 23c8878..e549905 100644 --- a/apps/desktop/src/services/telemetry-service.ts +++ b/apps/desktop/src/services/telemetry-service.ts @@ -4,6 +4,15 @@ import * as si from "systeminformation"; import { app } from "electron"; import { logger } from "../main/logger"; import type { SettingsService } from "./settings-service"; +import type { + OnboardingStartedEvent, + OnboardingScreenViewedEvent, + OnboardingFeaturesSelectedEvent, + OnboardingDiscoverySelectedEvent, + OnboardingModelSelectedEvent, + OnboardingCompletedEvent, + OnboardingAbandonedEvent, +} from "../types/telemetry-events"; export interface TranscriptionMetrics { session_id?: string; @@ -252,30 +261,102 @@ export class TelemetryService { } } - /** - * Generic track method for any analytics event - * PostHog SDK handles queueing, retries, and batching automatically - */ - track(eventName: string, properties?: Record): void { - if (!this.posthog || !this.enabled) { - return; - } + // ============================================================================ + // Onboarding Events + // ============================================================================ + + trackOnboardingStarted(props: OnboardingStartedEvent): void { + if (!this.posthog || !this.enabled) return; this.posthog.capture({ distinctId: this.machineId, - event: eventName, - properties: { - ...properties, - ...this.persistedProperties, - }, + event: "onboarding_started", + properties: { ...props, ...this.persistedProperties }, }); - logger.main.debug("Tracked event:", { - event: eventName, - properties: properties, - }); + logger.main.debug("Tracked onboarding started", props); } + trackOnboardingScreenViewed(props: OnboardingScreenViewedEvent): void { + if (!this.posthog || !this.enabled) return; + + this.posthog.capture({ + distinctId: this.machineId, + event: "onboarding_screen_viewed", + properties: { ...props, ...this.persistedProperties }, + }); + + logger.main.debug("Tracked onboarding screen viewed", props); + } + + trackOnboardingFeaturesSelected( + props: OnboardingFeaturesSelectedEvent, + ): void { + if (!this.posthog || !this.enabled) return; + + this.posthog.capture({ + distinctId: this.machineId, + event: "onboarding_features_selected", + properties: { ...props, ...this.persistedProperties }, + }); + + logger.main.debug("Tracked onboarding features selected", props); + } + + trackOnboardingDiscoverySelected( + props: OnboardingDiscoverySelectedEvent, + ): void { + if (!this.posthog || !this.enabled) return; + + this.posthog.capture({ + distinctId: this.machineId, + event: "onboarding_discovery_selected", + properties: { ...props, ...this.persistedProperties }, + }); + + logger.main.debug("Tracked onboarding discovery selected", props); + } + + trackOnboardingModelSelected(props: OnboardingModelSelectedEvent): void { + if (!this.posthog || !this.enabled) return; + + this.posthog.capture({ + distinctId: this.machineId, + event: "onboarding_model_selected", + properties: { ...props, ...this.persistedProperties }, + }); + + logger.main.debug("Tracked onboarding model selected", props); + } + + trackOnboardingCompleted(props: OnboardingCompletedEvent): void { + if (!this.posthog || !this.enabled) return; + + this.posthog.capture({ + distinctId: this.machineId, + event: "onboarding_completed", + properties: { ...props, ...this.persistedProperties }, + }); + + logger.main.debug("Tracked onboarding completed", props); + } + + trackOnboardingAbandoned(props: OnboardingAbandonedEvent): void { + if (!this.posthog || !this.enabled) return; + + this.posthog.capture({ + distinctId: this.machineId, + event: "onboarding_abandoned", + properties: { ...props, ...this.persistedProperties }, + }); + + logger.main.debug("Tracked onboarding abandoned", props); + } + + // ============================================================================ + // Transcription Events + // ============================================================================ + /** * Get system information for model recommendations */ diff --git a/apps/desktop/src/trpc/routers/onboarding.ts b/apps/desktop/src/trpc/routers/onboarding.ts index 1290d08..7461fed 100644 --- a/apps/desktop/src/trpc/routers/onboarding.ts +++ b/apps/desktop/src/trpc/routers/onboarding.ts @@ -5,7 +5,6 @@ import { ServiceManager } from "../../main/managers/service-manager"; import { OnboardingPreferencesSchema, OnboardingStateSchema, - AnalyticsEventSchema, ModelTypeSchema, FeatureInterestSchema, DiscoverySourceSchema, @@ -200,42 +199,106 @@ export const onboardingRouter = createRouter({ ), /** - * Track analytics event + * Track onboarding started event */ - trackEvent: procedure - .input(AnalyticsEventSchema) - .mutation( - async ({ input }): Promise<{ tracked: boolean; reason?: string }> => { - try { - const serviceManager = ServiceManager.getInstance(); - if (!serviceManager) { - return { tracked: false, reason: "ServiceManager not available" }; - } - const onboardingService = serviceManager.getOnboardingService(); - const settingsService = serviceManager.getSettingsService(); + trackOnboardingStarted: procedure + .input( + z.object({ + platform: z.string(), + resumed: z.boolean(), + resumedFrom: z.string().optional(), + }), + ) + .mutation(async ({ input }): Promise => { + const serviceManager = ServiceManager.getInstance(); + const telemetryService = serviceManager?.getService("telemetryService"); + telemetryService?.trackOnboardingStarted(input); + }), - if (!onboardingService || !settingsService) { - return { tracked: false, reason: "Services not available" }; - } + /** + * Track onboarding screen viewed event + */ + trackOnboardingScreenViewed: procedure + .input( + z.object({ + screen: z.string(), + index: z.number(), + total: z.number(), + }), + ) + .mutation(async ({ input }): Promise => { + const serviceManager = ServiceManager.getInstance(); + const telemetryService = serviceManager?.getService("telemetryService"); + telemetryService?.trackOnboardingScreenViewed(input); + }), - // Check if telemetry is enabled - const telemetrySettings = - await settingsService.getTelemetrySettings(); - if (telemetrySettings?.enabled === false) { - return { tracked: false, reason: "telemetry_disabled" }; - } + /** + * Track onboarding features selected event + */ + trackOnboardingFeaturesSelected: procedure + .input( + z.object({ + features: z.array(z.string()), + count: z.number(), + }), + ) + .mutation(async ({ input }): Promise => { + const serviceManager = ServiceManager.getInstance(); + const telemetryService = serviceManager?.getService("telemetryService"); + telemetryService?.trackOnboardingFeaturesSelected(input); + }), - // Track the event - onboardingService.trackEvent(input.eventName, input.properties); - logger.main.debug("Tracked onboarding event:", input); + /** + * Track onboarding discovery selected event + */ + trackOnboardingDiscoverySelected: procedure + .input( + z.object({ + source: z.string(), + details: z.string().optional(), + }), + ) + .mutation(async ({ input }): Promise => { + const serviceManager = ServiceManager.getInstance(); + const telemetryService = serviceManager?.getService("telemetryService"); + telemetryService?.trackOnboardingDiscoverySelected(input); + }), - return { tracked: true }; - } catch (error) { - logger.main.error("Failed to track onboarding event:", error); - return { tracked: false, reason: "error" }; - } - }, - ), + /** + * Track onboarding model selected event + */ + trackOnboardingModelSelected: procedure + .input( + z.object({ + model_type: z.string(), + recommendation_followed: z.boolean(), + }), + ) + .mutation(async ({ input }): Promise => { + const serviceManager = ServiceManager.getInstance(); + const telemetryService = serviceManager?.getService("telemetryService"); + telemetryService?.trackOnboardingModelSelected(input); + }), + + /** + * Track onboarding completed event + */ + trackOnboardingCompleted: procedure + .input( + z.object({ + version: z.number(), + features_selected: z.array(z.string()), + discovery_source: z.string().optional(), + model_type: z.string(), + recommendation_followed: z.boolean(), + skipped_screens: z.array(z.string()).optional(), + }), + ) + .mutation(async ({ input }): Promise => { + const serviceManager = ServiceManager.getInstance(); + const telemetryService = serviceManager?.getService("telemetryService"); + telemetryService?.trackOnboardingCompleted(input); + }), /** * Complete onboarding and save final state diff --git a/apps/desktop/src/types/onboarding.ts b/apps/desktop/src/types/onboarding.ts index a3dd6a5..8e99379 100644 --- a/apps/desktop/src/types/onboarding.ts +++ b/apps/desktop/src/types/onboarding.ts @@ -158,11 +158,6 @@ export const OnboardingPreferencesSchema = z.object({ followedRecommendation: z.boolean().optional(), }); -export const AnalyticsEventSchema = z.object({ - eventName: z.string(), - properties: z.record(z.any()), -}); - // ============================================================================ // Type Guards // ============================================================================ diff --git a/apps/desktop/src/types/telemetry-events.ts b/apps/desktop/src/types/telemetry-events.ts new file mode 100644 index 0000000..5c5e2f2 --- /dev/null +++ b/apps/desktop/src/types/telemetry-events.ts @@ -0,0 +1,76 @@ +/** + * Telemetry Event Type Definitions + * + * Each event tracked in the application should have a corresponding interface here. + * These interfaces ensure type safety when calling telemetry methods. + * + * Naming conventions: + * - Event names: snake_case with domain prefix (e.g., onboarding_started) + * - Properties: snake_case for consistency + */ + +// ============================================================================ +// Onboarding Events +// ============================================================================ + +/** + * Fired when user begins onboarding flow + */ +export interface OnboardingStartedEvent { + platform: string; + resumed: boolean; + resumedFrom?: string; +} + +/** + * Fired when user views an onboarding screen + */ +export interface OnboardingScreenViewedEvent { + screen: string; + index: number; + total: number; +} + +/** + * Fired when user selects feature interests + */ +export interface OnboardingFeaturesSelectedEvent { + features: string[]; + count: number; +} + +/** + * Fired when user selects how they discovered the app + */ +export interface OnboardingDiscoverySelectedEvent { + source: string; + details?: string; +} + +/** + * Fired when user selects their preferred model type + */ +export interface OnboardingModelSelectedEvent { + model_type: string; + recommendation_followed: boolean; +} + +/** + * Fired when user completes the onboarding flow + */ +export interface OnboardingCompletedEvent { + version: number; + features_selected: string[]; + discovery_source?: string; + model_type: string; + recommendation_followed: boolean; + skipped_screens?: string[]; +} + +/** + * Fired when user abandons the onboarding flow + */ +export interface OnboardingAbandonedEvent { + last_screen: string; + timestamp: string; +}