Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/cli/src/commands/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import {
} from "../ui/format.js";
import { warnIfWebmAlphaDropped } from "../utils/webmAlphaCheck.js";
import { renderFailureDiagnostic } from "../ui/renderDiagnostic.js";
import { missingPathKind } from "../telemetry/missingPathKind.js";
import { renderProgress, renderMachineProgress } from "../ui/progress.js";
import {
trackRenderComplete,
Expand Down Expand Up @@ -1815,6 +1816,7 @@ function handleRenderError(
// the render_complete success path, since nothing failed to name.
errorName: error instanceof Error ? error.name : "unknown",
failedStageCode: normalizeStageCode(failedStage || "pipeline"),
missingPathKind: missingPathKind(error),
...renderJobObservabilityTelemetryPayload(job),
...getMemorySnapshot(),
...cpu.stop(),
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/telemetry/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,7 @@ export function trackRenderError(
errorName?: string;
/** failedStage normalized to a stable snake_case code. */
failedStageCode?: string;
missingPathKind?: string;
errorMessage?: string;
elapsedMs?: number;
peakMemoryMb?: number;
Expand Down Expand Up @@ -702,6 +703,7 @@ export function trackRenderError(
failed_stage: props.failedStage,
error_name: props.errorName,
failed_stage_code: props.failedStageCode,
missing_path_kind: props.missingPathKind,
error_message: props.errorMessage ? redactTelemetryMessage(props.errorMessage) : undefined,
elapsed_ms: props.elapsedMs,
...renderOutputShapeEventProperties(props),
Expand Down
51 changes: 51 additions & 0 deletions packages/cli/src/telemetry/missingPathKind.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, expect, it } from "vitest";
import { missingPathKind } from "./missingPathKind.js";

const enoent = (path: string) =>
Object.assign(new Error(`ENOENT: no such file or directory, open '${path}'`), {
code: "ENOENT",
path,
});

describe("missingPathKind", () => {
it.each([
[
"C:\\Users\\a\\AppData\\Local\\Temp\\hf-cache\\hfcache-v8-0a1b2c\\frame_000041.jpg",
"extract_cache",
],
[
"/tmp/hf-render-Ab12Cd/compiled/__hyperframes_video_frames/v-s0/frame_000041.jpg",
"video_frames",
],
[
"/proj/work-0a1b2c3d-1111-2222-3333-444455556666-Xy12Ab/captured-frames/frame_005280.jpg",
"captured_frames",
],
["C:\\Temp\\hf-render-Ab12Cd\\capture-attempt-1\\worker-3\\frame_000007.jpg", "worker_frames"],
["/tmp/hf-render-Ab12Cd/capture-calibration/frame_000001.jpg", "calibration"],
["/tmp/hf-render-Ab12Cd/compiled/index.html", "compiled"],
["/tmp/hf-render-Ab12Cd/audio-work/mix.wav", "work_dir"],
["/Users/a/Projects/Launch/assets/intro.mp4", "other"],
])("names %s as %s", (path, kind) => {
expect(missingPathKind(enoent(path))).toBe(kind);
});

it("finds the missing file in an error's causes and in a message that only names it", () => {
const wrapped = new Error("[Parallel] Capture failed", {
cause: enoent("/tmp/hf-render-x/captured-frames/frame_1.jpg"),
});
expect(missingPathKind(wrapped)).toBe("captured_frames");
expect(
missingPathKind(
new Error("ENOENT: no such file or directory, open 'C:\\T\\hfcache-v8-ab\\frame_2.jpg'"),
),
).toBe("extract_cache");
});

it("names nothing when no file was missing", () => {
expect(missingPathKind(new Error("Composition has zero duration"))).toBeUndefined();
expect(
missingPathKind(Object.assign(new Error("EPERM: rename"), { code: "EPERM" })),
).toBeUndefined();
});
});
37 changes: 37 additions & 0 deletions packages/cli/src/telemetry/missingPathKind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const KINDS: ReadonlyArray<[RegExp, string]> = [
[/(^|[\\/])hfcache-/, "extract_cache"],
[/(^|[\\/])__hyperframes_video_frames([\\/]|$)/, "video_frames"],
[/(^|[\\/])captured-frames([\\/]|$)/, "captured_frames"],
[
/(^|[\\/])(capture-attempt-\d+|retry-\d+-batch-\d+-worker-\d+|worker-\d+)([\\/]|$)/,
"worker_frames",
],
[/(^|[\\/])capture-calibration/, "calibration"],
[/(^|[\\/])compiled([\\/]|$)/, "compiled"],
[/(^|[\\/])(hf-render-|work-[0-9a-f]{8}-)/, "work_dir"],
];

const quotedPath = (text: string) => /'([^']+)'/.exec(text)?.[1] ?? "";

function errorChain(error: unknown): Error[] {
const chain: Error[] = [];
for (let seen = error; seen instanceof Error && chain.length < 5; seen = seen.cause)
chain.push(seen);
return chain;
}

function missingPathOf(error: unknown): string | undefined {
const enoent = errorChain(error).find(
(seen) => (seen as NodeJS.ErrnoException).code === "ENOENT",
);
if (enoent) return (enoent as NodeJS.ErrnoException).path ?? quotedPath(enoent.message);
const message = error instanceof Error ? error.message : String(error);
return /\bENOENT\b/.test(message) ? quotedPath(message) : undefined;
}

/** Which kind of file a render found missing (a fixed name, never the path), or undefined when nothing was missing. */
export function missingPathKind(error: unknown): string | undefined {
const path = missingPathOf(error);
if (path === undefined) return undefined;
return KINDS.find(([pattern]) => pattern.test(path))?.[1] ?? "other";
}
Loading