fix(web-probe): capture web performance diagnostics payloads

This commit is contained in:
Codex
2026-07-02 17:03:45 +00:00
parent 6ebe5e377a
commit 049ebf2539
6 changed files with 577 additions and 0 deletions
@@ -786,6 +786,7 @@ function attachPassiveListeners(targetPage, pageRole = "control", targetPageId =
void installPagePerformanceProbe(targetPage, pageRole, targetPageId)
.catch((error) => appendJsonl(files.errors, eventRecord("performance-probe-install-error", { pageRole, pageId: targetPageId, error: errorSummary(error), valuesRedacted: true })));
targetPage.on("request", (request) => {
const webPerformancePayload = summarizeWebPerformanceRequestPayload(request);
void appendJsonl(files.network, eventRecord("request", {
pageRole,
pageId: targetPageId,
@@ -795,6 +796,7 @@ function attachPassiveListeners(targetPage, pageRole = "control", targetPageId =
url: safeUrl(request.url()),
resourceType: request.resourceType(),
frameUrl: safeFrameUrl(request.frame()),
...(webPerformancePayload ? { webPerformancePayload } : {}),
}));
});
targetPage.on("response", (response) => {
@@ -851,5 +853,158 @@ function attachPassiveListeners(targetPage, pageRole = "control", targetPageId =
void appendJsonl(files.control, eventRecord("continuity-break", { pageRole, pageId: targetPageId, reason: "page-closed" }));
});
}
function summarizeWebPerformanceRequestPayload(request) {
if (!request || request.method() !== "POST" || !isSameOriginWebPerformanceRequestUrl(request.url())) return null;
let raw = null;
try {
raw = request.postData();
} catch (error) {
return {
captureStatus: "post-data-read-failed",
parseStatus: "post-data-read-failed",
byteCount: null,
byteLimit: webPerformanceRequestBodyMaxBytes,
error: errorSummary(error),
eventCount: null,
events: [],
valuesRedacted: true,
};
}
if (raw === null || raw === undefined || raw === "") {
return {
captureStatus: "missing-body",
parseStatus: "missing-body",
byteCount: 0,
byteLimit: webPerformanceRequestBodyMaxBytes,
eventCount: 0,
events: [],
valuesRedacted: true,
};
}
const byteCount = Buffer.byteLength(raw, "utf8");
const bodyHash = sha256Text(raw);
if (byteCount > webPerformanceRequestBodyMaxBytes) {
return {
captureStatus: "skipped-over-limit",
parseStatus: "not-parsed-over-limit",
byteCount,
byteLimit: webPerformanceRequestBodyMaxBytes,
bodyHash,
truncated: true,
eventCount: null,
events: [],
valuesRedacted: true,
};
}
try {
const parsed = JSON.parse(raw);
const schemaVersion = typeof parsed?.schemaVersion === "string" ? parsed.schemaVersion : null;
if (schemaVersion !== "hwlab-web-performance-v2") {
return {
captureStatus: "captured",
parseStatus: "unsupported-schema",
schemaVersion,
byteCount,
byteLimit: webPerformanceRequestBodyMaxBytes,
bodyHash,
eventCount: Array.isArray(parsed?.events) ? parsed.events.length : null,
events: [],
valuesRedacted: true,
};
}
const events = (Array.isArray(parsed.events) ? parsed.events : []).slice(0, 80).map(compactWebPerformancePayloadEvent).filter(Boolean);
return {
captureStatus: "captured",
parseStatus: "parsed",
schemaVersion,
byteCount,
byteLimit: webPerformanceRequestBodyMaxBytes,
bodyHash,
truncated: false,
eventCount: Array.isArray(parsed.events) ? parsed.events.length : 0,
storedEventCount: events.length,
events,
valuesRedacted: true,
};
} catch (error) {
return {
captureStatus: "captured",
parseStatus: "invalid-json",
byteCount,
byteLimit: webPerformanceRequestBodyMaxBytes,
bodyHash,
parseError: truncate(error?.message || String(error), 180),
eventCount: null,
events: [],
valuesRedacted: true,
};
}
}
function isSameOriginWebPerformanceRequestUrl(value) {
try {
const url = new URL(String(value || ""), baseUrl);
return url.origin === baseUrl && url.pathname === "/v1/web-performance";
} catch {
return false;
}
}
function compactWebPerformancePayloadEvent(event) {
if (!event || typeof event !== "object") return null;
const detail = firstObject(event.detail, event.details, event.payload, event.data, event.metrics);
const read = (key) => event?.[key] ?? detail?.[key] ?? null;
const diagnosticCode = compactString(read("diagnosticCode") ?? read("code") ?? read("name"), 96);
const reason = compactString(read("reason"), 96);
const module = compactString(read("module"), 96);
const eventType = compactString(read("eventType") ?? read("type") ?? read("kind"), 96);
const traceId = compactTraceId(read("traceId"));
const sessionId = compactString(read("sessionId"), 120);
const replacedByKey = compactScalar(read("replacedByKey"));
const eventId = compactString(read("eventId") ?? read("id"), 120);
return {
ts: compactString(read("ts") ?? read("timestamp") ?? read("time"), 64),
eventType,
diagnosticCode,
reason,
module,
traceId,
sessionIdHash: sessionId ? sha256Text(sessionId) : null,
eventIdHash: eventId ? sha256Text(eventId) : null,
eventCount: numberOrNull(read("eventCount")),
chunkCount: numberOrNull(read("chunkCount")),
flushDurationMs: numberOrNull(read("flushDurationMs")),
droppedCount: numberOrNull(read("droppedCount")),
maxItemsPerChunk: numberOrNull(read("maxItemsPerChunk")),
maxChunkMs: numberOrNull(read("maxChunkMs")),
replacedByKey: typeof replacedByKey === "boolean" || typeof replacedByKey === "number" ? replacedByKey : null,
replacedByKeyHash: typeof replacedByKey === "string" ? sha256Text(replacedByKey) : null,
valuesRedacted: true,
};
}
function firstObject(...values) {
for (const value of values) if (value && typeof value === "object" && !Array.isArray(value)) return value;
return {};
}
function compactString(value, limit) {
if (value === null || value === undefined) return null;
return truncate(String(value), limit);
}
function compactTraceId(value) {
const text = String(value || "");
return /^trc_[A-Za-z0-9_-]+$/u.test(text) ? text : null;
}
function compactScalar(value) {
if (value === null || value === undefined) return null;
if (typeof value === "boolean") return value;
const numeric = Number(value);
if (Number.isFinite(numeric) && String(value).trim() !== "") return numeric;
return truncate(String(value), 120);
}
`;
}