diff --git a/packages/cli/src/commands/transcribe.ts b/packages/cli/src/commands/transcribe.ts index 4605ffdd39..7a283b736e 100644 --- a/packages/cli/src/commands/transcribe.ts +++ b/packages/cli/src/commands/transcribe.ts @@ -14,6 +14,7 @@ import { resolve, join, extname, dirname } from "node:path"; import * as clack from "@clack/prompts"; import { c } from "../ui/colors.js"; import { DEFAULT_MODEL } from "../whisper/manager.js"; +import { trackCommandFailure } from "../telemetry/events.js"; export default defineCommand({ meta: { @@ -52,7 +53,9 @@ export default defineCommand({ async run({ args }) { const inputPath = resolve(args.input); if (!existsSync(inputPath)) { - console.error(c.error(`File not found: ${args.input}`)); + const message = `File not found: ${args.input}`; + trackCommandFailure("transcribe", message); + console.error(c.error(message)); process.exit(1); } @@ -87,7 +90,9 @@ async function importTranscript(inputPath: string, dir: string, json: boolean): const { words, format } = loadTranscript(inputPath); if (words.length === 0) { - console.error(c.error("No words found in transcript.")); + const message = "No words found in transcript."; + trackCommandFailure("transcribe", message); + console.error(c.error(message)); process.exit(1); } @@ -170,6 +175,7 @@ async function transcribeAudio( } } catch (err) { const message = err instanceof Error ? err.message : String(err); + trackCommandFailure("transcribe", err); if (opts.json) { console.log(JSON.stringify({ ok: false, error: message })); } else { diff --git a/packages/cli/src/telemetry/events.test.ts b/packages/cli/src/telemetry/events.test.ts index 761b4ef674..924c2ebe06 100644 --- a/packages/cli/src/telemetry/events.test.ts +++ b/packages/cli/src/telemetry/events.test.ts @@ -5,7 +5,8 @@ vi.mock("./client.js", () => ({ trackEvent: (...args: unknown[]) => trackEvent(...args), })); -const { trackRenderError, trackRenderObservation } = await import("./events.js"); +const { trackRenderError, trackRenderObservation, trackCommandFailure } = + await import("./events.js"); describe("render telemetry events", () => { beforeEach(() => { @@ -50,3 +51,38 @@ describe("render telemetry events", () => { ); }); }); + +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: err.stack, + }), + ); + }); + + 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.", + }), + ); + }); +}); diff --git a/packages/cli/src/telemetry/events.ts b/packages/cli/src/telemetry/events.ts index 508b72f4f8..767f376374 100644 --- a/packages/cli/src/telemetry/events.ts +++ b/packages/cli/src/telemetry/events.ts @@ -284,6 +284,21 @@ export function trackCliError(props: { }); } +// Report why a command failed before it exits non-zero. cli_command_result +// records the failure but not the reason; this fills that gap via cli_error so +// command failures are diagnosable. Enqueues synchronously — the process `exit` +// handler flushes it. Drop this into any command's failure path. +export function trackCommandFailure(command: string, err: unknown): void { + const error = err instanceof Error ? err : new Error(String(err)); + trackCliError({ + error_name: error.name, + error_message: error.message, + stack_trace: error.stack, + command, + kind: "command_error", + }); +} + export function trackRenderFeedback(props: { rating: number; renderDurationMs: number;