import { PostHog } from "posthog-node"; import { machineId } from "node-machine-id"; 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; model_id: string; model_preloaded?: boolean; whisper_native_binding?: string; total_duration_ms?: number; recording_duration_ms?: number; processing_duration_ms?: number; audio_duration_seconds?: number; realtime_factor?: number; text_length?: number; word_count?: number; formatting_enabled?: boolean; formatting_model?: string; formatting_duration_ms?: number; vad_enabled?: boolean; session_type?: "streaming" | "batch"; language?: string; vocabulary_size?: number; } export interface SystemInfo { // Hardware cpu_model: string; cpu_cores: number; cpu_threads: number; cpu_speed_ghz: number; memory_total_gb: number; // OS os_platform: string; os_distro: string; os_release: string; os_arch: string; // Graphics gpu_model: string; gpu_vendor: string; // System manufacturer: string; model: string; } export class TelemetryService { private posthog: PostHog | null = null; private machineId: string = ""; private systemInfo: SystemInfo | null = null; private enabled: boolean = false; private initialized: boolean = false; private persistedProperties: Record = {}; private settingsService: SettingsService; constructor(settingsService: SettingsService) { this.settingsService = settingsService; // Initialize PostHog const host = process.env.POSTHOG_HOST || __BUNDLED_POSTHOG_HOST; // Check runtime env first, then fall back to bundled values const apiKey = process.env.POSTHOG_API_KEY || __BUNDLED_POSTHOG_API_KEY; const telemetryEnabled = process.env.TELEMETRY_ENABLED ? process.env.TELEMETRY_ENABLED !== "false" : __BUNDLED_TELEMETRY_ENABLED; if (!host || !apiKey || !telemetryEnabled) { logger.main.info( "Telemetry disabled since either api key or host has not been provided", ); return; } this.posthog = new PostHog(apiKey, { host, flushAt: 1, flushInterval: 10000, }); } async initialize(): Promise { if (this.initialized || !this.posthog) { return; } // Sync opt-out state with database settings const telemetrySettings = await this.settingsService.getTelemetrySettings(); if (telemetrySettings?.enabled === false) { await this.posthog.optOut(); logger.main.debug("Opted out of telemetry"); } else { await this.posthog.optIn(); logger.main.debug("Opted into telemetry"); } // Get unique machine ID this.machineId = await machineId(); logger.main.info("Machine ID generated for telemetry", { machineId: this.machineId, }); // Collect system information this.systemInfo = await this.collectSystemInfo(); logger.main.info("System information collected for telemetry", { systemInfo: this.systemInfo, }); // ! posthog-node code flow doesn't use register to set super properties // ! Track them manually this.persistedProperties = { app_version: app.getVersion(), machine_id: this.machineId, app_is_packaged: app.isPackaged, system_info: { ...this.systemInfo, }, }; this.enabled = true; this.initialized = true; logger.main.info("Telemetry service initialized successfully"); } private async collectSystemInfo(): Promise { try { const [cpu, mem, osInfo, graphics, system] = await Promise.all([ si.cpu(), si.mem(), si.osInfo(), si.graphics(), si.system(), ]); return { // Hardware cpu_model: `${cpu.manufacturer} ${cpu.brand}`.trim(), cpu_cores: cpu.physicalCores, cpu_threads: cpu.cores, cpu_speed_ghz: cpu.speed, memory_total_gb: Math.round(mem.total / 1073741824), // OS os_platform: osInfo.platform, os_distro: osInfo.distro, os_release: osInfo.release, os_arch: osInfo.arch, // Graphics gpu_model: graphics.controllers[0]?.model || "Unknown", gpu_vendor: graphics.controllers[0]?.vendor || "Unknown", // System manufacturer: system.manufacturer || "Unknown", model: system.model || "Unknown", }; } catch (error) { logger.main.error("Failed to collect system info:", error); // Return minimal info on error return { cpu_model: "Unknown", cpu_cores: 0, cpu_threads: 0, cpu_speed_ghz: 0, memory_total_gb: 0, os_platform: process.platform, os_distro: "Unknown", os_release: "Unknown", os_arch: process.arch, gpu_model: "Unknown", gpu_vendor: "Unknown", manufacturer: "Unknown", model: "Unknown", }; } } trackTranscriptionCompleted(metrics: TranscriptionMetrics): void { if (!this.posthog) { return; } this.posthog.capture({ distinctId: this.machineId, event: "transcription_completed", properties: { ...metrics, ...this.persistedProperties, }, }); logger.main.debug("Tracked transcription completion", { session_id: metrics.session_id, model: metrics.model_id, duration: metrics.total_duration_ms, recording_duration: metrics.recording_duration_ms, processing_duration: metrics.processing_duration_ms, }); } async shutdown(): Promise { if (!this.posthog) { return; } await this.posthog.shutdown(); logger.main.info("Telemetry service shut down"); } isEnabled(): boolean { return this.enabled; } getMachineId(): string { return this.machineId; } async optIn(): Promise { await this.settingsService.setTelemetrySettings({ enabled: true }); if (!this.posthog) { return; } await this.posthog.optIn(); logger.main.info("Telemetry opt-in successful"); } async optOut(): Promise { await this.settingsService.setTelemetrySettings({ enabled: false }); if (!this.posthog) { return; } await this.posthog.optOut(); logger.main.info("Telemetry opt-out successful"); } async setEnabled(enabled: boolean): Promise { if (enabled) { await this.optIn(); } else { await this.optOut(); } } // ============================================================================ // User Identification // ============================================================================ /** * Identify user in telemetry after login. * Also creates an alias to link machine ID with user ID. */ identifyUser(userId: string, email?: string, name?: string): void { if (!this.posthog || !this.enabled) return; // Identify with user ID this.posthog.identify({ distinctId: userId, properties: { ...this.persistedProperties, email, name, }, }); // Alias machine ID to user ID so previous anonymous events are linked this.posthog.alias({ distinctId: userId, alias: this.machineId, }); } trackAppLaunch(): void { if (!this.posthog || !this.enabled) return; this.posthog.capture({ distinctId: this.machineId, event: "app_launch", properties: { ...this.persistedProperties }, }); logger.main.debug("Tracked app launch"); } // ============================================================================ // Onboarding Events // ============================================================================ trackOnboardingStarted(props: OnboardingStartedEvent): void { if (!this.posthog || !this.enabled) return; this.posthog.capture({ distinctId: this.machineId, event: "onboarding_started", properties: { ...props, ...this.persistedProperties }, }); 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 */ getSystemInfo(): SystemInfo | null { return this.systemInfo; } }