403 lines
11 KiB
TypeScript
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;
|
|
}
|
|
}
|