amical/apps/desktop/src/services/telemetry-service.ts
2025-12-13 01:55:34 +05:30

403 lines
11 KiB
TypeScript

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<string, unknown> = {};
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<void> {
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<SystemInfo> {
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<void> {
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<void> {
await this.settingsService.setTelemetrySettings({ enabled: true });
if (!this.posthog) {
return;
}
await this.posthog.optIn();
logger.main.info("Telemetry opt-in successful");
}
async optOut(): Promise<void> {
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<void> {
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;
}
}