From ea498860ef156724dbae48a55f333ca39a664737 Mon Sep 17 00:00:00 2001 From: nchopra Date: Wed, 10 Sep 2025 02:39:54 +0530 Subject: [PATCH] Squashed commit of the following: commit f441264f6f8e069a272043de5addf3cf14bfca89 Author: haritabh-z01 Date: Mon Sep 1 08:38:36 2025 +0530 fix: super properties handling commit dea49e27c36ba8c4ef6e8edea74fd1cd509acdc2 Author: haritabh-z01 Date: Mon Sep 1 01:37:23 2025 +0530 chore: update release workflow to support telemetry commit 8919d884bb94665f85710c16af8a9238da896582 Author: nchopra Date: Mon Sep 1 01:08:26 2025 +0530 feat: add simple telemetry --- .github/workflows/release.yml | 7 + apps/desktop/package.json | 3 + .../src/main/managers/recording-manager.ts | 12 +- .../src/main/managers/service-manager.ts | 17 ++ .../src/pipeline/core/pipeline-types.ts | 4 + .../desktop/src/services/telemetry-service.ts | 217 ++++++++++++++++++ .../src/services/transcription-service.ts | 80 ++++++- pnpm-lock.yaml | 35 +++ 8 files changed, 371 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/services/telemetry-service.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1517b66..02d7b43 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -105,6 +105,9 @@ jobs: APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} CODESIGNING_IDENTITY: ${{ secrets.CODESIGNING_IDENTITY }} + POSTHOG_HOST: https://app.posthog.com + TELEMETRY_ENABLED: true + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} run: | echo "Building macOS ${{ matrix.arch }} artifacts" pnpm make:${{ matrix.arch }} @@ -112,6 +115,10 @@ jobs: - name: Build artifacts (Windows) if: matrix.os == 'windows' working-directory: apps/desktop + env: + POSTHOG_HOST: https://app.posthog.com + TELEMETRY_ENABLED: true + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} run: | echo "Building Windows x64 artifacts" pnpm make:windows diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 1b24252..7f4ea11 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -148,9 +148,11 @@ "libsql": "^0.5.13", "lucide-react": "^0.510.0", "next-themes": "^0.4.6", + "node-machine-id": "^1.1.12", "onnxruntime-node": "^1.20.1", "openai": "^4.98.0", "react": "^19.1.1", + "posthog-node": "^5.8.1", "react-day-picker": "8.10.1", "react-dom": "^19.1.1", "react-hook-form": "^7.56.3", @@ -159,6 +161,7 @@ "sonner": "^2.0.3", "split2": "^4.2.0", "superjson": "^2.2.2", + "systeminformation": "^5.27.8", "tailwind-merge": "^3.3.0", "tw-animate-css": "^1.2.9", "update-electron-app": "^3.1.1", diff --git a/apps/desktop/src/main/managers/recording-manager.ts b/apps/desktop/src/main/managers/recording-manager.ts index 34b31bf..b97fab3 100644 --- a/apps/desktop/src/main/managers/recording-manager.ts +++ b/apps/desktop/src/main/managers/recording-manager.ts @@ -1,5 +1,4 @@ import { ipcMain, app, dialog } from "electron"; -import type { TranscriptionService } from "../../services/transcription-service"; import { EventEmitter } from "node:events"; import { logger, logPerformance } from "../logger"; import { ServiceManager } from "./service-manager"; @@ -26,6 +25,8 @@ export class RecordingManager extends EventEmitter { audioFilePath: string; wavWriter: StreamingWavWriter; } | null = null; + private recordingStartedAt: number | null = null; + private recordingStoppedAt: number | null = null; constructor(private serviceManager: ServiceManager) { super(); @@ -197,6 +198,9 @@ export class RecordingManager extends EventEmitter { this.setState("starting"); this.setMode(mode); + this.recordingStartedAt = performance.now(); + this.recordingStoppedAt = null; // Reset stopped time + // Create session ID const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); this.currentSessionId = `session-${timestamp}`; @@ -245,6 +249,8 @@ export class RecordingManager extends EventEmitter { return; } + this.recordingStoppedAt = performance.now(); + this.setState("stopping"); // Reset recording mode when stopping @@ -342,13 +348,15 @@ export class RecordingManager extends EventEmitter { } const startTime = Date.now(); - // Process the chunk - pass isFinal flag and audio file path + // Process the chunk - pass isFinal flag, audio file path, and timing const transcriptionResult = await transcriptionService.processStreamingChunk({ sessionId: this.currentSessionId, audioChunk: chunk, isFinal: isFinalChunk, audioFilePath: this.currentAudioRecording.audioFilePath, + recordingStartedAt: this.recordingStartedAt || undefined, + recordingStoppedAt: this.recordingStoppedAt || undefined, }); logger.audio.debug("Processed audio chunk", { diff --git a/apps/desktop/src/main/managers/service-manager.ts b/apps/desktop/src/main/managers/service-manager.ts index 68eba69..70e8d44 100644 --- a/apps/desktop/src/main/managers/service-manager.ts +++ b/apps/desktop/src/main/managers/service-manager.ts @@ -12,11 +12,13 @@ import { createIPCHandler } from "electron-trpc-experimental/main"; import { router } from "../../trpc/router"; import { createContext } from "../../trpc/context"; import { isMacOS, isWindows } from "../../utils/platform"; +import { TelemetryService } from "../../services/telemetry-service"; /** * Service map for type-safe service access */ export interface ServiceMap { + telemetryService: TelemetryService; modelManagerService: ModelManagerService; transcriptionService: TranscriptionService; settingsService: SettingsService; @@ -35,6 +37,7 @@ export class ServiceManager { private static instance: ServiceManager | null = null; private isInitialized = false; + private telemetryService: TelemetryService | null = null; private modelManagerService: ModelManagerService | null = null; private transcriptionService: TranscriptionService | null = null; private settingsService: SettingsService | null = null; @@ -56,6 +59,7 @@ export class ServiceManager { } try { + await this.initializeTelemetryService(); this.initializeSettingsService(); await this.initializeModelServices(); this.initializePlatformServices(); @@ -74,6 +78,12 @@ export class ServiceManager { } } + private async initializeTelemetryService(): Promise { + this.telemetryService = new TelemetryService(); + await this.telemetryService.initialize(); + logger.main.info("Telemetry service initialized"); + } + private initializeSettingsService(): void { this.settingsService = new SettingsService(); logger.main.info("Settings service initialized"); @@ -113,6 +123,7 @@ export class ServiceManager { this.modelManagerService, this.vadService, this.settingsService, + this.telemetryService, ); await this.transcriptionService.initialize(); @@ -213,6 +224,7 @@ export class ServiceManager { } const services: Partial = { + telemetryService: this.telemetryService ?? undefined, modelManagerService: this.modelManagerService ?? undefined, transcriptionService: this.transcriptionService ?? undefined, settingsService: this.settingsService ?? undefined, @@ -250,6 +262,11 @@ export class ServiceManager { logger.main.info("Stopping native helper..."); this.nativeBridge.stopHelper(); } + + if (this.telemetryService) { + logger.main.info("Shutting down telemetry service..."); + await this.telemetryService.shutdown(); + } } static getInstance(): ServiceManager | null { diff --git a/apps/desktop/src/pipeline/core/pipeline-types.ts b/apps/desktop/src/pipeline/core/pipeline-types.ts index 60a0ab8..6191e73 100644 --- a/apps/desktop/src/pipeline/core/pipeline-types.ts +++ b/apps/desktop/src/pipeline/core/pipeline-types.ts @@ -67,6 +67,10 @@ export interface StreamingPipelineContext extends PipelineContext { export interface StreamingSession { context: StreamingPipelineContext; transcriptionResults: string[]; // Accumulate all transcription chunks + firstChunkReceivedAt?: number; // When first audio chunk arrived at transcription service + recordingStartedAt?: number; // When user pressed record button (from RecordingManager) + recordingStoppedAt?: number; // When user released record button (from RecordingManager) + finalChunkReceivedAt?: number; // When final chunk arrived at transcription service } // Simple pipeline configuration diff --git a/apps/desktop/src/services/telemetry-service.ts b/apps/desktop/src/services/telemetry-service.ts new file mode 100644 index 0000000..17b5b79 --- /dev/null +++ b/apps/desktop/src/services/telemetry-service.ts @@ -0,0 +1,217 @@ +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"; + +export interface TranscriptionMetrics { + session_id?: string; + model_id: string; + model_preloaded?: boolean; + 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 = {}; + + constructor() { + // Public constructor for consistency with other services + } + + async initialize(): Promise { + if (this.initialized) { + return; + } + + try { + // Check if telemetry is enabled via environment variable + const apiKey = process.env.POSTHOG_API_KEY; + const telemetryEnabled = process.env.TELEMETRY_ENABLED !== "false"; + + if (!apiKey || !telemetryEnabled) { + logger.main.info("Telemetry disabled or no API key provided"); + this.enabled = false; + return; + } + + // Get unique machine ID + this.machineId = await machineId(); + logger.main.info("Machine ID generated for telemetry"); + + // Collect system information + this.systemInfo = await this.collectSystemInfo(); + logger.main.info("System information collected for telemetry"); + + // Initialize PostHog + const host = process.env.POSTHOG_HOST || "https://app.posthog.com"; + this.posthog = new PostHog(apiKey, { + host, + flushAt: 1, + flushInterval: 10000, + }); + + // ! 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, + }, + }; + + // Identify the machine with system properties + this.posthog.identify({ + distinctId: this.machineId, + properties: { + ...this.persistedProperties, + }, + }); + this.enabled = true; + this.initialized = true; + logger.main.info("Telemetry service initialized successfully"); + } catch (error) { + logger.main.error("Failed to initialize telemetry service:", error); + this.enabled = false; + } + } + + 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.enabled || !this.posthog) return; + + try { + 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, + }); + } catch (error) { + logger.main.error("Failed to track transcription completed:", error); + } + } + + async shutdown(): Promise { + if (this.posthog) { + try { + await this.posthog.shutdown(); + logger.main.info("Telemetry service shut down"); + } catch (error) { + logger.main.error("Error shutting down telemetry:", error); + } + } + } + + isEnabled(): boolean { + return this.enabled; + } + + getMachineId(): string { + return this.machineId; + } +} diff --git a/apps/desktop/src/services/transcription-service.ts b/apps/desktop/src/services/transcription-service.ts index 0899e8a..284ba5e 100644 --- a/apps/desktop/src/services/transcription-service.ts +++ b/apps/desktop/src/services/transcription-service.ts @@ -9,6 +9,7 @@ import { OpenRouterProvider } from "../pipeline/providers/formatting/openrouter- import { ModelManagerService } from "../services/model-manager"; import { SettingsService } from "../services/settings-service"; import { appContextStore } from "../stores/app-context"; +import { TelemetryService } from "../services/telemetry-service"; import { createTranscription } from "../db/transcriptions"; import { logger } from "../main/logger"; import { v4 as uuid } from "uuid"; @@ -28,17 +29,23 @@ export class TranscriptionService { private settingsService: SettingsService; private vadMutex: Mutex; private transcriptionMutex: Mutex; + private telemetryService: TelemetryService; + private modelManagerService: ModelManagerService; + private modelWasPreloaded: boolean = false; constructor( modelManagerService: ModelManagerService, - vadService: VADService | null, + vadService: VADService, settingsService: SettingsService, + telemetryService: TelemetryService, ) { this.whisperProvider = new WhisperProvider(modelManagerService); this.vadService = vadService; this.settingsService = settingsService; this.vadMutex = new Mutex(); this.transcriptionMutex = new Mutex(); + this.telemetryService = telemetryService; + this.modelManagerService = modelManagerService; } async initialize(): Promise { @@ -59,6 +66,7 @@ export class TranscriptionService { if (hasModels) { logger.transcription.info("Preloading Whisper model..."); await this.preloadWhisperModel(); + this.modelWasPreloaded = true; logger.transcription.info("Whisper model preloaded successfully"); } else { logger.transcription.info( @@ -119,6 +127,7 @@ export class TranscriptionService { try { // Dispose current model await this.whisperProvider.dispose(); + this.modelWasPreloaded = false; // Reset preload flag on model change // Check if preloading is enabled and models are available if (this.settingsService) { @@ -134,6 +143,7 @@ export class TranscriptionService { "Reloading Whisper model after model change...", ); await this.whisperProvider.preloadModel(); + this.modelWasPreloaded = true; logger.transcription.info("Whisper model reloaded successfully"); } else { logger.transcription.info("No models available to preload"); @@ -181,8 +191,17 @@ export class TranscriptionService { audioChunk: Float32Array; isFinal?: boolean; audioFilePath?: string; + recordingStartedAt?: number; + recordingStoppedAt?: number; }): Promise { - const { sessionId, audioChunk, isFinal = false, audioFilePath } = options; + const { + sessionId, + audioChunk, + isFinal = false, + audioFilePath, + recordingStartedAt, + recordingStoppedAt, + } = options; // Run VAD on the audio chunk let speechProbability = 0; @@ -229,6 +248,8 @@ export class TranscriptionService { session = { context: streamingContext, transcriptionResults: [], + firstChunkReceivedAt: performance.now(), + recordingStartedAt: recordingStartedAt, // From RecordingManager (when user pressed record) }; this.streamingSessions.set(sessionId, session); @@ -288,7 +309,12 @@ export class TranscriptionService { return completeTranscriptionTillNow; } + session.finalChunkReceivedAt = performance.now(); + session.recordingStoppedAt = recordingStoppedAt; + let completeTranscription = completeTranscriptionTillNow; + let formattingStartTime: number | undefined; + let formattingDuration: number | undefined; logger.transcription.info("Finalizing streaming session", { sessionId, @@ -302,6 +328,7 @@ export class TranscriptionService { completeTranscription.trim().length ) { try { + formattingStartTime = performance.now(); const style = session.context.sharedData.userPreferences?.formattingStyle; const formattedText = await this.openRouterProvider.format({ @@ -321,12 +348,15 @@ export class TranscriptionService { }, }); + formattingDuration = performance.now() - formattingStartTime; + logger.transcription.info("Text formatted successfully", { sessionId, originalTranscription: completeTranscription, formattedTranscription: formattedText, originalLength: completeTranscription.length, formattedLength: formattedText.length, + formattingDuration, }); completeTranscription = formattedText; @@ -365,6 +395,52 @@ export class TranscriptionService { }, }); + // Track transcription completion + const completionTime = performance.now(); + + // Calculate durations: + // - Recording duration: from when recording started to when it ended + // - Processing duration: from when recording ended to completion + // - Total duration: from recording start to completion + const recordingDuration = + session.recordingStartedAt && session.recordingStoppedAt + ? session.recordingStoppedAt - session.recordingStartedAt + : undefined; + const processingDuration = session.recordingStoppedAt + ? completionTime - session.recordingStoppedAt + : undefined; + const totalDuration = session.recordingStartedAt + ? completionTime - session.recordingStartedAt + : undefined; + + const selectedModel = + this.modelManagerService.getSelectedModel() || "unknown"; + const audioDurationSeconds = + session.context.sharedData.audioMetadata?.duration; + + this.telemetryService.trackTranscriptionCompleted({ + session_id: sessionId, + model_id: selectedModel, + model_preloaded: this.modelWasPreloaded, + total_duration_ms: totalDuration || 0, + recording_duration_ms: recordingDuration, + processing_duration_ms: processingDuration, + audio_duration_seconds: audioDurationSeconds, + realtime_factor: + audioDurationSeconds && totalDuration + ? audioDurationSeconds / (totalDuration / 1000) + : undefined, + text_length: completeTranscription.length, + word_count: completeTranscription.trim().split(/\s+/).length, + formatting_enabled: this.formatterEnabled, + formatting_model: this.formatterEnabled ? "openrouter" : undefined, + formatting_duration_ms: formattingDuration, + vad_enabled: !!this.vadService, + session_type: "streaming", + language: session.context.sharedData.userPreferences?.language || "en", + vocabulary_size: session.context.sharedData.vocabulary?.size || 0, + }); + this.streamingSessions.delete(sessionId); logger.transcription.info("Streaming session completed", { sessionId }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f8bffa..570fa79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -233,12 +233,18 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + node-machine-id: + specifier: ^1.1.12 + version: 1.1.12 onnxruntime-node: specifier: ^1.20.1 version: 1.22.0 openai: specifier: ^4.98.0 version: 4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76) + posthog-node: + specifier: ^5.8.1 + version: 5.8.1 react: specifier: ^19.1.1 version: 19.1.1 @@ -266,6 +272,9 @@ importers: superjson: specifier: ^2.2.2 version: 2.2.2 + systeminformation: + specifier: ^5.27.8 + version: 5.27.8 tailwind-merge: specifier: ^3.3.0 version: 3.3.1 @@ -2593,6 +2602,9 @@ packages: '@poppinss/exception@1.2.2': resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==} + '@posthog/core@1.0.2': + resolution: {integrity: sha512-hWk3rUtJl2crQK0WNmwg13n82hnTwB99BT99/XI5gZSvIlYZ1TPmMZE8H2dhJJ98J/rm9vYJ/UXNzw3RV5HTpQ==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -7880,6 +7892,9 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + node-machine-id@1.1.12: + resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} + node-plop@0.26.3: resolution: {integrity: sha512-Cov028YhBZ5aB7MdMWJEmwyBig43aGL5WT4vdoB28Oitau1zZAcHUn8Sgfk9HM33TqhtLJ9PlM/O0Mv+QpV/4Q==} engines: {node: '>=8.9.4'} @@ -8325,6 +8340,10 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + posthog-node@5.8.1: + resolution: {integrity: sha512-YJYlYnlpItVjHqM9IhvZx8TzK8gnx2nU+0uhiog4RN47NnV0Z0K1AdC4ul+O8VuvS/jHqKCQvL8iAONRA37+0A==} + engines: {node: '>=20'} + postject@1.0.0-alpha.6: resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} engines: {node: '>=14.0.0'} @@ -9376,6 +9395,12 @@ packages: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} + systeminformation@5.27.8: + resolution: {integrity: sha512-d3Z0gaQO1MlUxzDUKsmXz5y4TOBCMZ8IyijzaYOykV3AcNOTQ7mT+tpndUOXYNSxzLK3la8G32xiUFvZ0/s6PA==} + engines: {node: '>=8.0.0'} + os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] + hasBin: true + table-layout@4.1.1: resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} engines: {node: '>=12.17'} @@ -12511,6 +12536,8 @@ snapshots: '@poppinss/exception@1.2.2': {} + '@posthog/core@1.0.2': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -19020,6 +19047,8 @@ snapshots: node-int64@0.4.0: {} + node-machine-id@1.1.12: {} + node-plop@0.26.3: dependencies: '@babel/runtime-corejs3': 7.28.3 @@ -19488,6 +19517,10 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + posthog-node@5.8.1: + dependencies: + '@posthog/core': 1.0.2 + postject@1.0.0-alpha.6: dependencies: commander: 9.5.0 @@ -20881,6 +20914,8 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 + systeminformation@5.27.8: {} + table-layout@4.1.1: dependencies: array-back: 6.2.2