mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 10:14:30 +00:00
Observability showed `browser` (~75% fail, ~1.3k users/day) and `info` (~60% fail) failing at high rates with no captured reason — only `cli_command_result success=false`. citty's `runMain` catches a command's thrown error and `process.exit(1)`s without re-throwing, so a thrown failure never reached the existing `cli_error` telemetry (which only fired from the uncaughtException / unhandledRejection handlers). Wrap every command's `run()` at the dispatch boundary (cli.ts) so a thrown failure reports its reason via `cli_error` (kind=command_error) before being re-thrown unchanged — citty's print + exit-1 behavior is preserved. This de-blinds every throw-style command at once: `browser ensure` (Chrome download), `tts`, `inspect`, `render`, etc. Paths that bypass the wrapper are handled inline: - `browser` self-exits (`path` download failure, unknown subcommand) — report inline; the ARM64 `ensure` branch previously swallowed a failed install and returned success, now reports and exits 1. - `resolveProject()` self-exits on InvalidProjectError (the dominant `info` failure — run outside a project) — report inline before exit. Hardening: - PII: `trackCliError` now redacts error_message + stack_trace via redactTelemetryString (matching render_* events) — CLI errors and stacks carry absolute install paths / cache dirs / user args. - Race: the wrapper awaits an on-demand telemetry import before re-throwing, so a command that fails before the lazy telemetry import settles still reports (a telemetry failure is swallowed and never masks the real error). Pure helpers in utils/command-failure-tracking.ts with unit tests for the throw / success / no-run / onFailure-rejection cases, the reporter wiring, and trackCliError redaction. CommandDef<any> mirrors citty's SubCommandsDef. Known scope: commands that print + `process.exit(1)` on their own validation paths (tts/validate/lint argument errors) remain wrapper-blind — follow-up.
60 lines
2.2 KiB
TypeScript
60 lines
2.2 KiB
TypeScript
import type { CommandDef } from "citty";
|
|
|
|
// citty types subcommands as `CommandDef<any>` (SubCommandsDef); mirror that so
|
|
// each command's specific args type is accepted without per-command generics.
|
|
type AnyCommandDef = CommandDef<any>;
|
|
|
|
/**
|
|
* Wrap a lazy command loader so a thrown failure is reported via `onFailure`
|
|
* before it propagates. citty's `runMain` catches command errors and exits 1
|
|
* without re-throwing, so this is the only place to capture the reason. The
|
|
* error is re-thrown unchanged, preserving citty's print + exit-1 behavior.
|
|
*
|
|
* `onFailure` is awaited so it can resolve the (lazily-loaded) telemetry module
|
|
* before the error propagates — otherwise a command that throws before the
|
|
* telemetry import settles would lose its event. A throw from `onFailure` is
|
|
* swallowed so telemetry can never mask the real command failure.
|
|
*
|
|
* Commands that call `process.exit()` themselves bypass this (the process is
|
|
* already gone) and must report their failure inline.
|
|
*/
|
|
export function trackCommandFailures(
|
|
load: () => Promise<AnyCommandDef>,
|
|
onFailure: (err: unknown) => void | Promise<void>,
|
|
): () => Promise<AnyCommandDef> {
|
|
return () =>
|
|
load().then((cmd) => {
|
|
const run = cmd.run;
|
|
if (typeof run !== "function") return cmd;
|
|
return {
|
|
...cmd,
|
|
run: async (ctx: Parameters<typeof run>[0]) => {
|
|
try {
|
|
return await run(ctx);
|
|
} catch (err) {
|
|
try {
|
|
await onFailure(err);
|
|
} catch {
|
|
// Telemetry must never mask the real command failure.
|
|
}
|
|
throw err;
|
|
}
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Report a command failure to telemetry, loading the telemetry module on demand
|
|
* (keeps it off the CLI cold-start path) and awaiting it so the event is
|
|
* enqueued before the caller re-throws / exits. Best-effort — never throws.
|
|
*/
|
|
export async function reportCommandFailure(command: string, err: unknown): Promise<void> {
|
|
try {
|
|
const { trackCommandFailure } = await import("../telemetry/events.js");
|
|
trackCommandFailure(command, err);
|
|
} catch {
|
|
// ignore: a telemetry failure must not affect the command's exit path
|
|
}
|
|
}
|