chore: add resume capability to onboarding flow

This commit is contained in:
haritabh-z01 2025-11-24 20:39:26 +05:30
parent 84235860de
commit 0e72fb2eb6
8 changed files with 337 additions and 117 deletions

View file

@ -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<string, any>): void {
this.onboardingService.trackEvent(eventName, properties);
}
}

View file

@ -46,12 +46,24 @@ export function App() {
const [discoveryDetails, setDiscoveryDetails] = useState<string>("");
// 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,
});

View file

@ -16,7 +16,6 @@ interface UseOnboardingStateReturn {
error: Error | null;
savePreferences: (preferences: OnboardingPreferences) => Promise<void>;
completeOnboarding: (finalState: OnboardingState) => Promise<void>;
trackEvent: (eventName: string, properties?: Record<string, any>) => void;
resetOnboarding: () => Promise<void>;
}
@ -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<string, any>) => {
// 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,
};
}

View file

@ -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<string, any>): 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(),
});
}
/**

View file

@ -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<string, any>): 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
*/

View file

@ -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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
const serviceManager = ServiceManager.getInstance();
const telemetryService = serviceManager?.getService("telemetryService");
telemetryService?.trackOnboardingCompleted(input);
}),
/**
* Complete onboarding and save final state

View file

@ -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
// ============================================================================

View file

@ -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;
}