feat(engine,producer,cli): capture failing dB/frame index on drawElement verify fallback

de_fallback_reason only told you the fallback happened (blank/psnr/oom/
capture_error), not the failing PSNR or frame index — that data existed as
text inside the thrown error's message and was discarded on the way to
telemetry. DrawElementVerificationError now carries structured
frameIndex/failedDb/verifyThresholdDb; the orchestrator reads them via the
new getDrawElementVerificationDetails helper instead of regexing message
text, and both telemetry surfaces (the render_complete perfSummary path and
the crash-survival RenderCaptureObservability mirror) emit
de_fallback_failed_db / de_fallback_frame_index.

Needed to distinguish "32dB vs the 32dB threshold, tune it" from "12dB real
corruption, investigate" during the parallel-router soak — currently that
distinction is invisible.
This commit is contained in:
Vance Ingalls
2026-07-14 23:29:49 -07:00
parent f3800f3579
commit a5cbb78ff9
12 changed files with 202 additions and 1 deletions
@@ -1,11 +1,14 @@
import { describe, it, expect } from "vitest";
import {
DrawElementVerificationError,
formatHttpErrorDiagnostic,
formatConsoleDiagnostic,
formatNavigationFailureDiagnostic,
formatNavigationStartDiagnostic,
formatRequestFailureDiagnostic,
getDrawElementVerificationDetails,
isFontResourceError,
isDrawElementVerificationError,
sanitizeDiagnosticUrl,
} from "./frameCapture.js";
@@ -222,3 +225,38 @@ describe("navigation diagnostics", () => {
).toBe("[Browser:HTTP403] GET https://cdn.example.com/frame.png resource=image Forbidden");
});
});
describe("DrawElementVerificationError details", () => {
it("carries frameIndex/failedDb/verifyThresholdDb when provided", () => {
const err = new DrawElementVerificationError("drawElement self-verify failed at frame 649", {
frameIndex: 649,
failedDb: 28.4,
verifyThresholdDb: 32,
});
expect(isDrawElementVerificationError(err)).toBe(true);
expect(getDrawElementVerificationDetails(err)).toEqual({
frameIndex: 649,
failedDb: 28.4,
verifyThresholdDb: 32,
});
});
it("omits fields that weren't supplied (blank-frame throws have no dB)", () => {
const err = new DrawElementVerificationError("blank drawElement frame 12", { frameIndex: 12 });
expect(getDrawElementVerificationDetails(err)).toEqual({ frameIndex: 12 });
});
it("returns undefined for a non-verification error", () => {
expect(getDrawElementVerificationDetails(new Error("boring"))).toBeUndefined();
});
it("finds details through a wrapping cause chain (producer's CaptureStageError)", () => {
const inner = new DrawElementVerificationError("psnr breach", {
frameIndex: 5,
failedDb: 12.1,
});
const wrapper = new Error("capture stage failed", { cause: inner });
expect(isDrawElementVerificationError(wrapper)).toBe(true);
expect(getDrawElementVerificationDetails(wrapper)).toEqual({ frameIndex: 5, failedDb: 12.1 });
});
});