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
@@ -458,6 +458,9 @@ function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) {
const significantRequestFailed = requestFailed.filter(
(item) => !isBenignLongLivedStreamClosureAlert(item) && !isObserverRefreshClosureAlert(item, observerRefreshTimes),
);
const webPerformanceDiagnostics = extractWebPerformanceRuntimeDiagnostics(naturalNetwork, promptTimes);
const webPerformancePayloadStates = summarizeWebPerformancePayloadStates(naturalNetwork);
const webPerformanceDiagnosticGroups = groupWebPerformanceRuntimeDiagnostics(webPerformanceDiagnostics);
const domDiagnostics = [];
const executionErrors = [];
const baselineExecutionErrors = [];
@@ -588,6 +591,11 @@ function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) {
workbenchSessionListReadCount,
workbenchTraceEventsReadCount,
webPerformanceBeaconFailureCount,
webPerformancePayloadRequestCount: webPerformancePayloadStates.total,
webPerformancePayloadParsedCount: webPerformancePayloadStates.parsed,
webPerformancePayloadParseIssueCount: webPerformancePayloadStates.parseIssue,
webPerformanceRuntimeDiagnosticCount: webPerformanceDiagnostics.length,
webPerformanceRuntimeDiagnosticGroupCount: webPerformanceDiagnosticGroups.length,
workbenchEventSourceFailureCount,
benignLongLivedStreamClosureCount: requestFailed.length - significantRequestFailed.length,
domDiagnosticSampleCount: domDiagnostics.length,
@@ -608,6 +616,9 @@ function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) {
networkHttpErrorsByPath: groupNetworkAlerts(httpErrors),
networkRequestFailedByPath: groupNetworkAlerts(requestFailed),
networkSignificantRequestFailedByPath: groupNetworkAlerts(significantRequestFailed),
webPerformancePayloadStates,
webPerformanceRuntimeDiagnostics: webPerformanceDiagnostics.slice(0, 120),
webPerformanceRuntimeDiagnosticsByCode: webPerformanceDiagnosticGroups,
domDiagnostics: domDiagnostics.slice(-80),
domDiagnosticsByText: groupDomDiagnostics(domDiagnostics),
domDiagnosticsByFingerprint: groupDomDiagnostics(domDiagnostics).slice(0, 80),
@@ -674,6 +685,173 @@ function groupDomDiagnostics(events) {
.sort((a, b) => (b.count - a.count) || String(a.firstAt || "").localeCompare(String(b.firstAt || "")));
}
function extractWebPerformanceRuntimeDiagnostics(network, promptTimes) {
const rows = [];
for (const item of Array.isArray(network) ? network : []) {
const payload = item?.webPerformancePayload && typeof item.webPerformancePayload === "object" ? item.webPerformancePayload : null;
if (!payload || payload.parseStatus !== "parsed" || payload.schemaVersion !== "hwlab-web-performance-v2") continue;
const events = Array.isArray(payload.events) ? payload.events : [];
for (const event of events) {
if (!event || typeof event !== "object") continue;
const diagnosticCode = limitText(event.diagnosticCode || event.code || "", 120);
const reason = limitText(event.reason || "", 120);
const eventType = limitText(event.eventType || event.type || event.kind || "", 120);
if (!diagnosticCode && !reason && !eventType) continue;
rows.push({
ts: item.ts ?? event.ts ?? null,
promptIndex: promptIndexForTs(promptTimes, item.ts ?? event.ts),
pageRole: item.pageRole ?? null,
pageId: item.pageId ?? null,
commandId: item.commandId ?? null,
method: item.method ?? null,
urlPath: urlPath(item.url),
schemaVersion: payload.schemaVersion,
captureStatus: payload.captureStatus ?? null,
parseStatus: payload.parseStatus ?? null,
byteCount: numberOrNull(payload.byteCount),
bodyHash: payload.bodyHash ?? null,
eventType,
diagnosticCode,
reason,
module: limitText(event.module || "", 120),
traceId: event.traceId ?? null,
sessionIdHash: event.sessionIdHash ?? null,
eventIdHash: event.eventIdHash ?? null,
eventCount: numberOrNull(event.eventCount),
chunkCount: numberOrNull(event.chunkCount),
flushDurationMs: numberOrNull(event.flushDurationMs),
droppedCount: numberOrNull(event.droppedCount),
maxItemsPerChunk: numberOrNull(event.maxItemsPerChunk),
maxChunkMs: numberOrNull(event.maxChunkMs),
replacedByKey: typeof event.replacedByKey === "boolean" || typeof event.replacedByKey === "number" ? event.replacedByKey : null,
replacedByKeyHash: event.replacedByKeyHash ?? null,
valuesRedacted: true,
});
}
}
return rows.sort((left, right) => String(left.ts || "").localeCompare(String(right.ts || ""))).slice(-200);
}
function summarizeWebPerformancePayloadStates(network) {
const stateCounts = new Map();
let total = 0;
let parsed = 0;
let parseIssue = 0;
let overLimit = 0;
let invalidJson = 0;
let missingBody = 0;
let unsupportedSchema = 0;
let capturedEventCount = 0;
let storedEventCount = 0;
for (const item of Array.isArray(network) ? network : []) {
const payload = item?.webPerformancePayload && typeof item.webPerformancePayload === "object" ? item.webPerformancePayload : null;
if (!payload) continue;
total += 1;
const state = String(payload.parseStatus || payload.captureStatus || "unknown");
stateCounts.set(state, (stateCounts.get(state) || 0) + 1);
if (payload.parseStatus === "parsed") parsed += 1;
else parseIssue += 1;
if (payload.parseStatus === "not-parsed-over-limit" || payload.captureStatus === "skipped-over-limit") overLimit += 1;
if (payload.parseStatus === "invalid-json") invalidJson += 1;
if (payload.parseStatus === "missing-body") missingBody += 1;
if (payload.parseStatus === "unsupported-schema") unsupportedSchema += 1;
if (Number.isFinite(Number(payload.eventCount))) capturedEventCount += Number(payload.eventCount);
if (Number.isFinite(Number(payload.storedEventCount))) storedEventCount += Number(payload.storedEventCount);
}
return {
total,
parsed,
parseIssue,
overLimit,
invalidJson,
missingBody,
unsupportedSchema,
capturedEventCount,
storedEventCount,
states: Array.from(stateCounts.entries()).map(([state, count]) => ({ state, count })).sort((a, b) => b.count - a.count || a.state.localeCompare(b.state)),
valuesRedacted: true,
};
}
function groupWebPerformanceRuntimeDiagnostics(events) {
const groups = new Map();
for (const item of Array.isArray(events) ? events : []) {
const key = [item.diagnosticCode || item.eventType || "-", item.reason || "-", item.module || "-"].join("|");
const group = groups.get(key) || {
diagnosticCode: item.diagnosticCode || null,
reason: item.reason || null,
module: item.module || null,
eventType: item.eventType || null,
count: 0,
firstAt: item.ts || null,
lastAt: item.ts || null,
promptIndexes: new Set(),
traceIds: new Set(),
eventCount: 0,
chunkCount: 0,
droppedCount: 0,
maxFlushDurationMs: null,
maxItemsPerChunk: null,
maxChunkMs: null,
replacedByKeyCount: 0,
examples: [],
};
group.count += 1;
group.firstAt = minIso(group.firstAt, item.ts || null);
group.lastAt = maxIso(group.lastAt, item.ts || null);
if (Number.isFinite(Number(item.promptIndex))) group.promptIndexes.add(Number(item.promptIndex));
if (item.traceId) group.traceIds.add(String(item.traceId));
group.eventCount += Number(item.eventCount || 0);
group.chunkCount += Number(item.chunkCount || 0);
group.droppedCount += Number(item.droppedCount || 0);
group.maxFlushDurationMs = maxNumber(group.maxFlushDurationMs, item.flushDurationMs);
group.maxItemsPerChunk = maxNumber(group.maxItemsPerChunk, item.maxItemsPerChunk);
group.maxChunkMs = maxNumber(group.maxChunkMs, item.maxChunkMs);
if (item.replacedByKey === true || item.replacedByKeyHash) group.replacedByKeyCount += 1;
if (group.examples.length < 6) group.examples.push({
ts: item.ts || null,
traceId: item.traceId || null,
eventCount: item.eventCount ?? null,
chunkCount: item.chunkCount ?? null,
flushDurationMs: item.flushDurationMs ?? null,
droppedCount: item.droppedCount ?? null,
maxItemsPerChunk: item.maxItemsPerChunk ?? null,
maxChunkMs: item.maxChunkMs ?? null,
replacedByKey: item.replacedByKey ?? null,
replacedByKeyHash: item.replacedByKeyHash ?? null,
valuesRedacted: true,
});
groups.set(key, group);
}
return Array.from(groups.values()).map((item) => ({
diagnosticCode: item.diagnosticCode,
reason: item.reason,
module: item.module,
eventType: item.eventType,
count: item.count,
firstAt: item.firstAt,
lastAt: item.lastAt,
promptIndexes: Array.from(item.promptIndexes).sort((a, b) => a - b),
traceIds: Array.from(item.traceIds).sort().slice(0, 12),
eventCount: item.eventCount,
chunkCount: item.chunkCount,
droppedCount: item.droppedCount,
maxFlushDurationMs: item.maxFlushDurationMs,
maxItemsPerChunk: item.maxItemsPerChunk,
maxChunkMs: item.maxChunkMs,
replacedByKeyCount: item.replacedByKeyCount,
examples: item.examples,
valuesRedacted: true,
})).sort((left, right) => right.count - left.count || String(left.firstAt || "").localeCompare(String(right.firstAt || "")));
}
function maxNumber(current, candidate) {
const value = Number(candidate);
if (!Number.isFinite(value)) return current ?? null;
const existing = Number(current);
return Number.isFinite(existing) ? Math.max(existing, value) : value;
}
function isReportableDomDiagnostic(item, preview) {
if (item?.source === "diagnostic-node" || item?.source === "execution-row") return true;
return /trace_id=|HTTP\s+\d{3}\b|Failed to load resource|ERR_[A-Z_]+|provider-unavailable|AgentRun error|超过\s*\d+\s*ms\s*无新活动|代理暂时无法连接上游|Trace 更新超时|加载失败/iu.test(String(preview || ""));