feat(cli): run-ID telemetry correlation and check breakdown event

HYPERFRAMES_RUN_ID (trimmed, 128-char cap) attaches as run_id to the
generic cli_command / cli_command_result events, absent when unset, so
an orchestrator setting it per design element can group a verify loop's
invocations in analytics. check additionally emits one check_report
event per invocation (including lint-short-circuited and failing runs):
gate booleans, per-class error/warning counts, launch/seek/contrast
phase timings, sample counts, ok and exit code. Timings stay internal;
no command output changes.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-10 13:27:52 -04:00
parent 7ab6c2b7a2
commit 3a02942a03
12 changed files with 526 additions and 21 deletions
@@ -44,6 +44,11 @@ afterEach(() => {
});
it("carries raw browser geometry through the page driver and pipeline", async () => {
vi.spyOn(Date, "now")
.mockReturnValueOnce(100)
.mockReturnValueOnce(160)
.mockReturnValueOnce(200)
.mockReturnValueOnce(240);
document.body.innerHTML = `
<div data-composition-id="main" data-duration="10" data-width="640" data-height="360">
<section data-composition-file="scenes/hero.html">
@@ -85,6 +90,7 @@ it("carries raw browser geometry through the page driver and pipeline", async ()
time: 5,
}),
]);
expect(result.timings).toEqual({ launchSettleMs: 60, seekLoopMs: 40, contrastMs: 0 });
expect(mocks.serverClose).toHaveBeenCalledOnce();
});
+3
View File
@@ -93,6 +93,7 @@ export async function runBrowserCheck(
let chromeBrowser: import("puppeteer-core").Browser | undefined;
try {
const launchSettleStart = Date.now();
const session = await openSettledCompositionPage(html, server.url, {
renderReadyTimeoutMs: options.timeout,
renderReadyWarningSuffix: "checking the current page state",
@@ -104,12 +105,14 @@ export async function runBrowserCheck(
await waitForPreferredSeekTarget(page, 500);
const rootAnchor = await resolveRootAnchor(page);
const launchSettleMs = Date.now() - launchSettleStart;
const driver = createPageDriver(page, (time) => {
currentTime = time;
});
const result = await runGrid(driver, options, motion);
return {
...result,
timings: { ...result.timings, launchSettleMs },
runtimeFindings: drafts.map((draft) => runtimeFinding(draft, rootAnchor)),
};
} finally {
+37 -1
View File
@@ -1,5 +1,7 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { join, relative } from "node:path";
import { trackCheckReport } from "../telemetry/events.js";
import { getRunId } from "../telemetry/runId.js";
import type { ProjectDir } from "./project.js";
import { lintProject, shouldBlockRender, type ProjectLintResult } from "./lintProject.js";
import {
@@ -180,6 +182,7 @@ interface GridSamples {
motionFrames: MotionFrame[];
contrastEntries: ContrastAuditEntry[];
screenshots: CheckScreenshot[];
contrastMs: number;
}
interface GeometrySeen {
@@ -349,6 +352,7 @@ async function collectGridSamples(
motionFrames: [],
contrastEntries: [],
screenshots: [],
contrastMs: 0,
};
for (const time of mergeSampleTimes(grid.layoutSamples, motion.times)) {
await driver.seek(time);
@@ -366,7 +370,9 @@ async function collectGridSamples(
);
}
if (contrastSet.has(time)) {
const contrastStart = Date.now();
const capture = await driver.collectContrast(time);
collected.contrastMs += Date.now() - contrastStart;
collected.contrastEntries.push(...capture.entries);
collected.screenshots.push({ time, pngBase64: capture.pngBase64 });
}
@@ -382,7 +388,9 @@ export async function runAuditGrid(
await driver.initialize(options.contrast);
const grid = await buildSampleGrid(driver, options);
const plan = await planMotionSampling(driver, motion, grid.duration);
const seekLoopStart = Date.now();
const collected = await collectGridSamples(driver, options, grid, plan);
const seekLoopMs = Date.now() - seekLoopStart;
let motionIssues = plan.preflightIssues;
if (motion.kind === "valid" && motionIssues.length === 0 && collected.motionFrames.length > 0) {
@@ -408,6 +416,7 @@ export async function runAuditGrid(
contrastChecked: collected.contrastEntries.length,
contrastPassed: contrast.passed,
screenshots: collected.screenshots,
timings: { launchSettleMs: 0, seekLoopMs, contrastMs: collected.contrastMs },
};
}
@@ -555,7 +564,7 @@ function buildReport(
layout.errorCount +
motionSection.errorCount +
contrastSection.errorCount;
return {
const report: CheckReport = {
ok: errorCount === 0 && (!options.strict || warningCount === 0),
strict: options.strict,
lint,
@@ -580,6 +589,32 @@ function buildReport(
times: options.snapshots ? browser.screenshots.map((shot) => shot.time) : [],
},
};
trackCheckReport({
contrastGate: options.contrast,
motionGate: motion.kind !== "none",
captionZoneGate: options.captionZone !== undefined,
frameCheckGate: options.frameCheck !== undefined,
snapshotsGate: options.snapshots,
lintErrors: lint.errorCount,
lintWarnings: lint.warningCount,
runtimeErrors: runtime.errorCount,
runtimeWarnings: runtime.warningCount,
layoutErrors: layout.errorCount,
layoutWarnings: layout.warningCount,
motionErrors: motionSection.errorCount,
motionWarnings: motionSection.warningCount,
contrastErrors: contrastSection.errorCount,
contrastWarnings: contrastSection.warningCount,
launchSettleMs: browser.timings.launchSettleMs,
seekLoopMs: browser.timings.seekLoopMs,
contrastMs: browser.timings.contrastMs,
gridPoints: browser.layoutSamples.length,
contrastPoints: browser.contrastChecked,
ok: report.ok,
exitCode: checkExitCode(report),
runId: getRunId(),
});
return report;
}
function shapeLayoutSection(
@@ -651,6 +686,7 @@ function emptyBrowserResult(): CheckBrowserResult {
contrastChecked: 0,
contrastPassed: 0,
screenshots: [],
timings: { launchSettleMs: 0, seekLoopMs: 0, contrastMs: 0 },
};
}
+7
View File
@@ -133,6 +133,12 @@ export interface CheckScreenshot {
pngBase64: string;
}
export interface CheckTimings {
launchSettleMs: number;
seekLoopMs: number;
contrastMs: number;
}
export interface CheckBrowserResult {
duration: number;
layoutSamples: number[];
@@ -147,6 +153,7 @@ export interface CheckBrowserResult {
contrastChecked: number;
contrastPassed: number;
screenshots: CheckScreenshot[];
timings: CheckTimings;
}
/** The seek-grid audit loop, injected into checkBrowser so it never imports checkPipeline back. */