import { describe, expect, it, vi, beforeEach } from "vitest"; const trackEvent = vi.fn(); const flush = vi.fn(() => Promise.resolve()); const shouldTrack = vi.fn(() => true); vi.mock("./client.js", () => ({ trackEvent: (...args: unknown[]) => trackEvent(...args), flush: () => flush(), shouldTrack: () => shouldTrack(), })); // Power state shells out to `pmset`; spy so tests can assert it is NOT // sampled for opted-out installs (the fields are built at the call site, // before trackEvent's own shouldTrack guard). const getPowerState = vi.fn(() => ({ on_battery: true, low_power_mode: false })); vi.mock("./system.js", async () => ({ ...(await vi.importActual("./system.js")), getPowerState: () => getPowerState(), })); // identifyUser reads the install anonymousId; pin it so the $identify alias is // deterministic and the test never touches disk. vi.mock("./config.js", () => ({ readConfig: () => ({ anonymousId: "anon-test-123", telemetryEnabled: true }), })); const { trackCommand, trackCommandResult, trackCheckReport, trackRenderComplete, trackRenderError, trackRenderObservation, trackCommandFailure, trackCliError, trackFigmaImport, trackRenderFeedback, trackRenderPreflightRejected, trackAuthLoginStarted, trackAuthLoginCompleted, trackAuthLoginFailed, identifyUser, } = await import("./events.js"); describe("command telemetry events", () => { beforeEach(() => { trackEvent.mockClear(); }); it("includes run_id in cli_command when a run ID is provided", () => { trackCommand("check", "run-123"); expect(trackEvent).toHaveBeenCalledWith("cli_command", { command: "check", run_id: "run-123", }); }); it("omits run_id from cli_command when no run ID is provided", () => { trackCommand("check"); const properties = trackEvent.mock.lastCall?.[1]; expect(properties).not.toHaveProperty("run_id"); }); it("includes run_id in cli_command_result when a run ID is provided", () => { trackCommandResult({ command: "check", success: true, exitCode: 0, durationMs: 42, runId: "run-123", }); expect(trackEvent).toHaveBeenCalledWith("cli_command_result", { command: "check", success: true, exit_code: 0, duration_ms: 42, run_id: "run-123", }); }); it("omits run_id from cli_command_result when no run ID is provided", () => { trackCommandResult({ command: "check", success: false, exitCode: 1, durationMs: 42, }); const properties = trackEvent.mock.lastCall?.[1]; expect(properties).not.toHaveProperty("run_id"); }); }); describe("trackCheckReport", () => { beforeEach(() => { trackEvent.mockClear(); }); it("emits the check breakdown with snake_case properties and a run ID", () => { trackCheckReport({ contrastGate: true, motionGate: false, captionZoneGate: true, frameCheckGate: false, snapshotsGate: true, lintErrors: 1, lintWarnings: 2, runtimeErrors: 3, runtimeWarnings: 4, layoutErrors: 5, layoutWarnings: 6, motionErrors: 7, motionWarnings: 8, contrastErrors: 9, contrastWarnings: 10, launchSettleMs: 11, seekLoopMs: 12, contrastMs: 13, gridPoints: 14, contrastPoints: 15, ok: false, exitCode: 1, runId: "run-123", }); expect(trackEvent).toHaveBeenCalledWith("check_report", { gate_contrast: true, gate_motion: false, gate_caption_zone: true, gate_frame_check: false, gate_snapshots: true, lint_errors: 1, lint_warnings: 2, runtime_errors: 3, runtime_warnings: 4, layout_errors: 5, layout_warnings: 6, motion_errors: 7, motion_warnings: 8, contrast_errors: 9, contrast_warnings: 10, launch_settle_ms: 11, seek_loop_ms: 12, contrast_ms: 13, grid_points: 14, contrast_points: 15, ok: false, exit_code: 1, run_id: "run-123", }); }); it("omits run_id when no run ID is provided", () => { trackCheckReport({ contrastGate: false, motionGate: false, captionZoneGate: false, frameCheckGate: false, snapshotsGate: false, lintErrors: 0, lintWarnings: 0, runtimeErrors: 0, runtimeWarnings: 0, layoutErrors: 0, layoutWarnings: 0, motionErrors: 0, motionWarnings: 0, contrastErrors: 0, contrastWarnings: 0, launchSettleMs: 0, seekLoopMs: 0, contrastMs: 0, gridPoints: 0, contrastPoints: 0, ok: true, exitCode: 0, }); const properties = trackEvent.mock.lastCall?.[1]; expect(properties).not.toHaveProperty("run_id"); }); }); describe("render telemetry events", () => { beforeEach(() => { trackEvent.mockClear(); flush.mockClear(); }); it("flushes immediately after render_complete and render_error (exit races the lazy flush)", () => { trackRenderComplete({ durationMs: 1000, fps: 30, quality: "draft", docker: false, gpu: false }); expect(flush).toHaveBeenCalledTimes(1); trackRenderError({ fps: 30, quality: "draft", docker: false }); expect(flush).toHaveBeenCalledTimes(2); }); // The enforcement decision for the advisory heap budget reads these fleet // props (see computeWorkerSizing) — a silent drop in the summary→event hop // would invalidate that decision without anyone noticing. it("carries every worker-sizing provenance prop on render_complete", () => { trackRenderComplete({ durationMs: 1000, fps: 30, quality: "high", docker: false, gpu: false, workers: 6, workersBoundBy: "max_workers", workersCpuBased: 16, workersMemoryBased: 8, workersHeapBased: 4, workersFrameBased: 24, workersHeapLimitMb: 4096, workersExceedHeapAdvisory: true, }); expect(trackEvent).toHaveBeenCalledWith( "render_complete", expect.objectContaining({ workers: 6, workers_bound_by: "max_workers", workers_cpu_based: 16, workers_memory_based: 8, workers_heap_based: 4, workers_frame_based: 24, workers_heap_limit_mb: 4096, workers_exceed_heap_advisory: true, }), undefined, ); }); it("ties feedback to its report and recent renders via feedback_id + recent_render_ids", () => { trackRenderFeedback({ rating: 3, comment: "hook scene blank", feedbackId: "feedback-uuid", recentRenderIds: ["render-a", "render-b"], }); expect(trackEvent).toHaveBeenCalledWith( "cli_render_feedback", expect.objectContaining({ feedback_id: "feedback-uuid", recent_render_ids: "render-a,render-b", }), ); }); it("redacts paths and URL query strings from render error messages", () => { trackRenderError({ fps: 30, quality: "standard", docker: false, errorMessage: "ENOENT: open '/home/ubuntu/project/media/video.mp4' https://example.com/video.mp4?token=secret", observabilityCompositionHash: "abc123", }); expect(trackEvent).toHaveBeenCalledWith( "render_error", expect.objectContaining({ error_message: "ENOENT: open '[path]' https://example.com/video.mp4?…", observability_composition_hash: "abc123", }), undefined, ); }); it("carries the DE parallel-router/inversion cohort on render_error (hard failure, not just self-verify revert)", () => { trackRenderError({ fps: 30, quality: "standard", docker: false, errorMessage: "worker crashed", captureDeParallelRouter: "routed", captureDePreRouterWorkers: 2, captureWorkerCount: 3, captureMemoryExhaustionDetected: true, }); expect(trackEvent).toHaveBeenCalledWith( "render_error", expect.objectContaining({ de_parallel_router: "routed", de_pre_router_workers: 2, capture_worker_count: 3, capture_memory_exhaustion_detected: true, }), undefined, ); }); it("carries de_fallback_reason on render_error so a render that fails AFTER an OOM-triggered fallback attempt is distinguishable from one that never attempted a fallback", () => { trackRenderError({ fps: 30, quality: "standard", docker: false, errorMessage: "worker crashed again after fallback", captureDeParallelRouter: "reverted", captureDeSelfVerifyFallback: false, captureDeFallbackReason: "oom", }); expect(trackEvent).toHaveBeenCalledWith( "render_error", expect.objectContaining({ de_parallel_router: "reverted", de_self_verify_fallback: false, de_fallback_reason: "oom", }), undefined, ); }); it("carries the failing dB, frame index, and threshold on render_error for a psnr fallback that failed hard afterward", () => { trackRenderError({ fps: 30, quality: "standard", docker: false, errorMessage: "worker crashed after a psnr fallback", captureDeParallelRouter: "reverted", captureDeSelfVerifyFallback: true, captureDeFallbackReason: "psnr", captureDeFallbackFailedDb: 28.4, captureDeFallbackFrameIndex: 649, captureDeFallbackThresholdDb: 32, }); expect(trackEvent).toHaveBeenCalledWith( "render_error", expect.objectContaining({ de_fallback_reason: "psnr", de_fallback_failed_db: 28.4, de_fallback_frame_index: 649, de_fallback_threshold_db: 32, }), undefined, ); }); it("prefers the explicit perfSummary-sourced de_worker_inversion over the capture-observability fallback on render_complete", () => { trackRenderComplete({ durationMs: 1000, fps: 30, quality: "standard", docker: false, gpu: false, deWorkerInversion: "inverted", // Simulates a stale/divergent capture-observability value — the explicit // perfSummary field above must win, not this one. captureDeWorkerInversion: "reverted", }); expect(trackEvent).toHaveBeenCalledWith( "render_complete", expect.objectContaining({ de_worker_inversion: "inverted" }), undefined, ); }); it("carries the perfSummary-sourced failing dB, frame index, and threshold on render_complete", () => { trackRenderComplete({ durationMs: 1000, fps: 30, quality: "standard", docker: false, gpu: false, deParallelRouter: "reverted", deFallbackReason: "psnr", deFallbackFailedDb: 28.4, deFallbackFrameIndex: 649, deFallbackThresholdDb: 32, }); expect(trackEvent).toHaveBeenCalledWith( "render_complete", expect.objectContaining({ de_fallback_reason: "psnr", de_fallback_failed_db: 28.4, de_fallback_frame_index: 649, de_fallback_threshold_db: 32, }), undefined, ); }); it("emits render_preflight_rejected with the low-cardinality issue kind", () => { trackRenderPreflightRejected({ kind: "aspect-mismatch" }); expect(trackEvent).toHaveBeenCalledWith("render_preflight_rejected", { kind: "aspect-mismatch", }); }); it("forwards distinctId to trackEvent so studio renders attribute to the browser user", () => { trackRenderError({ fps: 30, quality: "standard", docker: false, source: "studio", distinctId: "browser-user-123", }); expect(trackEvent).toHaveBeenCalledWith( "render_error", expect.objectContaining({ source: "studio" }), "browser-user-123", ); }); it("sends split capture-stage timing fields on render_complete", () => { trackRenderComplete({ durationMs: 6000, fps: 30, quality: "standard", docker: false, gpu: false, stageCaptureMs: 5100, stageCaptureSetupMs: 1860, stageCaptureFrameMs: 3240, captureAvgMs: 27, }); expect(trackEvent).toHaveBeenCalledWith( "render_complete", expect.objectContaining({ stage_capture_ms: 5100, stage_capture_setup_ms: 1860, stage_capture_frame_ms: 3240, capture_avg_ms: 27, }), undefined, ); }); it("sends beginframe no-damage reuse counters on render_complete", () => { trackRenderComplete({ durationMs: 6000, fps: 30, quality: "standard", docker: false, gpu: false, beginFrameNoDamageFrames: 720, beginFrameHasDamageFrames: 480, }); expect(trackEvent).toHaveBeenCalledWith( "render_complete", expect.objectContaining({ begin_frame_no_damage_frames: 720, begin_frame_has_damage_frames: 480, }), undefined, ); }); it("redacts render_observation messages and includes renderJobId for correlation", () => { trackRenderObservation({ renderJobId: "render-123", phase: "capture_hdr_layered", status: "error", compositionHash: "abc123", captureMode: "screenshot", captureOperation: "captureScreenshot", framesCompleted: 12, totalFrames: 900, heartbeatIndex: 1, stageElapsedMs: 30_000, message: "Navigation failed for C:\\Users\\Alice\\project\\video.mov?not-a-query", }); expect(trackEvent).toHaveBeenCalledWith( "render_observation", expect.objectContaining({ render_job_id: "render-123", composition_hash: "abc123", capture_mode: "screenshot", capture_operation: "captureScreenshot", frames_completed: 12, total_frames: 900, heartbeat_index: 1, stage_elapsed_ms: 30_000, message: "Navigation failed for [path]", }), ); }); it("carries capture_parallel_stream on render_error via the shared payload", () => { trackRenderError({ fps: 30, quality: "standard", docker: false, errorMessage: "worker crashed", captureParallelStream: "beginframe", }); expect(trackEvent).toHaveBeenCalledWith( "render_error", expect.objectContaining({ capture_parallel_stream: "beginframe" }), undefined, ); }); }); describe("trackRenderFeedback", () => { beforeEach(() => { trackEvent.mockClear(); }); it("omits render_duration_ms when no duration is known (standalone feedback)", () => { trackRenderFeedback({ rating: 4, comment: "great" }); const [, props] = trackEvent.mock.calls[0] as [string, Record]; expect(props).not.toHaveProperty("render_duration_ms"); expect(props.rating).toBe(4); expect(props.rating_scale).toBe(10); }); it("includes render_duration_ms when a real duration is supplied", () => { trackRenderFeedback({ rating: 5, renderDurationMs: 6000 }); expect(trackEvent).toHaveBeenCalledWith( "cli_render_feedback", expect.objectContaining({ render_duration_ms: 6000 }), ); }); }); describe("trackCliError", () => { beforeEach(() => { trackEvent.mockClear(); }); it("redacts install paths from error_message and stack_trace", () => { trackCliError({ error_name: "Error", error_message: "ENOENT: open '/Users/alice/project/index.html'", stack_trace: "Error: boom\n at /Users/alice/.cache/hyperframes/chrome/headless", command: "info", kind: "command_error", }); const [, props] = trackEvent.mock.calls[0] as [string, Record]; expect(props.error_message).not.toContain("/Users/alice"); expect(props.error_message).toContain("[path]"); expect(props.stack_trace).not.toContain("/Users/alice"); }); it("forwards the figma endpoint label when supplied", () => { trackCliError({ error_name: "RATE_LIMITED", error_message: "figma rate limit hit (429)", command: "figma asset", kind: "command_error", endpoint: "images", }); expect(trackEvent).toHaveBeenCalledWith( "cli_error", expect.objectContaining({ endpoint: "images" }), ); }); }); describe("trackCommandFailure", () => { beforeEach(() => { trackEvent.mockClear(); }); it("reports an Error as a command_error with name/message/stack", () => { const err = new Error("ffmpeg is required to extract audio"); trackCommandFailure("transcribe", err); expect(trackEvent).toHaveBeenCalledWith( "cli_error", expect.objectContaining({ kind: "command_error", command: "transcribe", error_name: "Error", error_message: "ffmpeg is required to extract audio", // stack_trace is asserted (redacted) in the trackCliError suite; the // raw err.stack no longer matches once paths are stripped. }), ); }); it("coerces a non-Error reason (e.g. a string) into the message", () => { trackCommandFailure("transcribe", "No words found in transcript."); expect(trackEvent).toHaveBeenCalledWith( "cli_error", expect.objectContaining({ kind: "command_error", command: "transcribe", error_message: "No words found in transcript.", }), ); }); }); describe("trackFigmaImport", () => { beforeEach(() => { trackEvent.mockClear(); }); it("emits figma_import with phase + quality counters, no identifiers", () => { trackFigmaImport({ phase: "component", durationMs: 1234, unresolvedBindings: 2, rasterizedNodes: 3, }); expect(trackEvent).toHaveBeenCalledWith("figma_import", { phase: "component", duration_ms: 1234, unresolved_bindings: 2, rasterized_nodes: 3, }); }); it("carries reused for the asset phase and omits absent props entirely", () => { trackFigmaImport({ phase: "asset", durationMs: 42, reused: true }); expect(trackEvent).toHaveBeenCalledWith("figma_import", { phase: "asset", duration_ms: 42, reused: true, }); }); it("carries tokens mode + entry count for the tokens phase", () => { trackFigmaImport({ phase: "tokens", durationMs: 10, tokensMode: "styles", entryCount: 0 }); expect(trackEvent).toHaveBeenCalledWith( "figma_import", expect.objectContaining({ phase: "tokens", tokens_mode: "styles", entry_count: 0 }), ); }); }); describe("auth login telemetry events", () => { beforeEach(() => { trackEvent.mockClear(); }); it("emits auth_login_started tagged with the method", () => { trackAuthLoginStarted("oauth"); expect(trackEvent).toHaveBeenCalledWith("auth_login_started", { method: "oauth" }, undefined); }); it("emits auth_login_completed tagged with the method", () => { trackAuthLoginCompleted("api_key"); expect(trackEvent).toHaveBeenCalledWith( "auth_login_completed", { method: "api_key" }, undefined, ); }); it("emits auth_login_failed with the method and a low-cardinality reason", () => { trackAuthLoginFailed("oauth", "flow_error"); expect(trackEvent).toHaveBeenCalledWith( "auth_login_failed", { method: "oauth", reason: "flow_error" }, undefined, ); }); it("distinguishes a timed-out browser flow from a real error", () => { trackAuthLoginFailed("oauth", "flow_timeout"); expect(trackEvent).toHaveBeenCalledWith( "auth_login_failed", { method: "oauth", reason: "flow_timeout" }, undefined, ); }); it("records an aborted prompt / stdin timeout as its own reason", () => { trackAuthLoginFailed("api_key", "aborted"); expect(trackEvent).toHaveBeenCalledWith( "auth_login_failed", { method: "api_key", reason: "aborted" }, undefined, ); }); it("carries only method + reason — never a key, token, or free text", () => { trackAuthLoginFailed("api_key", "rejected"); expect(trackEvent).toHaveBeenCalledWith( "auth_login_failed", { method: "api_key", reason: "rejected" }, undefined, ); }); it("forwards an explicit distinctId to trackEvent for user-level attribution", () => { trackAuthLoginCompleted("oauth", "alice@example.com"); expect(trackEvent).toHaveBeenCalledWith( "auth_login_completed", { method: "oauth" }, "alice@example.com", ); }); it("identifyUser emits a $identify alias linking the anon install to the identity", () => { identifyUser("alice@example.com"); expect(trackEvent).toHaveBeenCalledWith( "$identify", { $anon_distinct_id: "anon-test-123" }, "alice@example.com", ); }); it("identifyUser is a no-op when there is no identity to attach", () => { identifyUser(""); expect(trackEvent).not.toHaveBeenCalled(); }); }); describe("power-state sampling respects the telemetry opt-out", () => { beforeEach(() => { getPowerState.mockClear(); shouldTrack.mockReturnValue(true); }); it("samples power state for a tracked render", () => { trackRenderComplete({ durationMs: 1, fps: 30, quality: "high", docker: false, gpu: false }); expect(getPowerState).toHaveBeenCalled(); const props = trackEvent.mock.calls.at(-1)?.[1] as Record; expect(props.on_battery).toBe(true); expect(props.low_power_mode).toBe(false); }); it("does NOT spawn pmset when telemetry is disabled", () => { // Regression: powerStateFields() is spread into the properties object at // the call site, so it runs BEFORE trackEvent's `if (!shouldTrack())` // guard — an opted-out install would otherwise pay two blocking // subprocess spawns per render for an event that is then discarded. shouldTrack.mockReturnValue(false); trackRenderComplete({ durationMs: 1, fps: 30, quality: "high", docker: false, gpu: false }); expect(getPowerState).not.toHaveBeenCalled(); }); });