fix(web-probe): capture web performance diagnostics payloads
This commit is contained in:
@@ -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);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user