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
+46 -1
View File
@@ -201,14 +201,34 @@ export interface CaptureSession {
* forceScreenshot. Discriminant-based guard (not instanceof) so it survives
* duplicated module instances across package boundaries.
*/
/**
* Structured detail carried alongside the human-readable message — lets
* telemetry report the actual failing dB / frame index instead of the
* orchestrator having to regex them back out of formatted text. All optional:
* a blank-frame trip has no PSNR score, so `failedDb`/`verifyThresholdDb`
* are omitted for that throw site.
*/
export interface DrawElementVerificationDetails {
frameIndex?: number;
failedDb?: number;
verifyThresholdDb?: number;
}
export class DrawElementVerificationError extends Error {
constructor(message: string) {
readonly frameIndex?: number;
readonly failedDb?: number;
readonly verifyThresholdDb?: number;
constructor(message: string, details?: DrawElementVerificationDetails) {
super(message);
this.name = "DrawElementVerificationError";
// Discriminant property, assigned dynamically: isDrawElementVerificationError
// reads it structurally so detection survives duplicated module instances
// across package boundaries (where instanceof fails).
(this as unknown as { deVerificationFailure: boolean }).deVerificationFailure = true;
this.frameIndex = details?.frameIndex;
this.failedDb = details?.failedDb;
this.verifyThresholdDb = details?.verifyThresholdDb;
}
}
@@ -222,6 +242,31 @@ export function isDrawElementVerificationError(err: unknown): boolean {
return false;
}
/**
* Extracts the structured details off a (possibly cause-wrapped) verification
* error — same chain-walk as isDrawElementVerificationError, structural
* (not instanceof) for the same duplicated-module-instance reason. Returns
* undefined when the error isn't a verification failure at all.
*/
export function getDrawElementVerificationDetails(
err: unknown,
): DrawElementVerificationDetails | undefined {
let e: unknown = err;
for (let depth = 0; depth < 5 && typeof e === "object" && e !== null; depth++) {
const rec = e as { deVerificationFailure?: boolean } & Partial<DrawElementVerificationDetails>;
if (rec.deVerificationFailure === true) {
const details: DrawElementVerificationDetails = {};
if (typeof rec.frameIndex === "number") details.frameIndex = rec.frameIndex;
if (typeof rec.failedDb === "number") details.failedDb = rec.failedDb;
if (typeof rec.verifyThresholdDb === "number")
details.verifyThresholdDb = rec.verifyThresholdDb;
return details;
}
e = (e as { cause?: unknown }).cause;
}
return undefined;
}
/** Wait for inline CSS background images introduced by the latest seek. */
export async function decodeDynamicCssBackgroundImages(page: Page): Promise<void> {
await page.evaluate(async () => {