fix: validate web-probe resource timing phases (#704)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -7982,7 +7982,7 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec
|
||||
"const objectOrNull = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : null;",
|
||||
"const slimRound = (item) => { const v = objectOrNull(item) || {}; return { promptIndex: v.promptIndex ?? null, sampleCount: v.sampleCount ?? null, loadingSamples: v.loadingSamples ?? null, maxLoadingCount: v.maxLoadingCount ?? null, loadingOwnerCount: v.loadingOwnerCount ?? null, lastTotalElapsedSeconds: v.lastTotalElapsedSeconds ?? null, lastRecentUpdateSeconds: v.lastRecentUpdateSeconds ?? null, diagnosticSamples: v.diagnosticSamples ?? null, terminalSamples: v.terminalSamples ?? null, turnTimingTotalElapsedForwardJumpCount: v.turnTimingTotalElapsedForwardJumpCount ?? null, turnTimingTotalElapsedForwardJumpMaxSeconds: v.turnTimingTotalElapsedForwardJumpMaxSeconds ?? null, promptTextHash: clip(v.promptTextHash, 80) }; };",
|
||||
"const slimTurnColumn = (item) => { const v = objectOrNull(item) || {}; return { label: clip(v.label, 24), promptIndex: v.promptIndex ?? null, lastPromptIndex: v.lastPromptIndex ?? null, traceId: clip(v.traceId, 48), firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, source: clip(v.source, 48) }; };",
|
||||
"const slimSlowSample = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, seq: v.seq ?? null, path: clip(v.path ?? v.rawPath, 96), initiatorType: clip(v.initiatorType, 24), durationMs: v.durationMs ?? null, requestToResponseStartMs: v.requestToResponseStartMs ?? v.streamOpenMs ?? null, responseTransferMs: v.responseTransferMs ?? null, nextHopProtocol: clip(v.nextHopProtocol, 24) }; };",
|
||||
"const slimSlowSample = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, seq: v.seq ?? null, path: clip(v.path ?? v.rawPath, 96), initiatorType: clip(v.initiatorType, 24), durationMs: v.durationMs ?? null, requestToResponseStartMs: v.requestToResponseStartMs ?? v.streamOpenMs ?? null, responseTransferMs: v.responseTransferMs ?? null, nextHopProtocol: clip(v.nextHopProtocol, 24), timingStatus: clip(v.timingStatus, 16), serverTimingNames: Array.isArray(v.serverTimingNames) ? v.serverTimingNames.slice(0, 4).map((x) => clip(x, 32)) : [], otelTraceId: clip(v.otelTraceId, 32) }; };",
|
||||
"const slimSlowApi = (item) => { const v = objectOrNull(item) || {}; return { path: clip(v.path ?? v.route, 96), route: clip(v.route ?? v.path, 96), sampleCount: v.sampleCount ?? null, p95Ms: v.p95Ms ?? v.p95 ?? null, maxMs: v.maxMs ?? v.max ?? null, overFiveSecondCount: v.overFiveSecondCount ?? null, slowSamples: Array.isArray(v.slowSamples) ? v.slowSamples.slice(0, 3).map(slimSlowSample) : [] }; };",
|
||||
"const slimFinding = (item) => { const v = objectOrNull(item) || {}; return { kind: clip(v.kind ?? v.id ?? v.code, 48), code: clip(v.code ?? v.id ?? v.kind, 48), severity: clip(v.severity ?? v.level, 24), level: clip(v.level ?? v.severity, 24), count: v.count ?? v.sampleCount ?? null, sampleCount: v.sampleCount ?? v.count ?? null, summary: clip(v.summary ?? v.message, 180), message: clip(v.message ?? v.summary, 180) }; };",
|
||||
"const slimDomGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, text: clip(v.text ?? v.preview, 180) }; };",
|
||||
@@ -8507,7 +8507,7 @@ function renderWebObserveAnalyzeTable(value: Record<string, unknown>): string {
|
||||
]) : [["-", "-", "-", "-", "-"]]),
|
||||
"",
|
||||
"Slow API samples (>5s):",
|
||||
webObserveTable(["TS", "SEQ", "PATH", "DURATION", "REQ_WAIT", "RESP_XFER", "INIT", "PROTO"], (() => {
|
||||
webObserveTable(["TS", "SEQ", "PATH", "DURATION", "REQ_WAIT", "RESP_XFER", "TIMING", "INIT", "PROTO", "SERVER", "OTEL"], (() => {
|
||||
const rows = slowApis.flatMap((item) => (Array.isArray(item.slowSamples) ? item.slowSamples : []).map((sample) => ({ ...sample, path: sample.path ?? item.path ?? item.route }))).slice(0, 8).map((sample) => [
|
||||
webObserveShort(webObserveText(sample.ts), 24),
|
||||
webObserveText(sample.seq),
|
||||
@@ -8515,10 +8515,13 @@ function renderWebObserveAnalyzeTable(value: Record<string, unknown>): string {
|
||||
webObserveText(sample.durationMs),
|
||||
webObserveText(sample.requestToResponseStartMs ?? sample.streamOpenMs),
|
||||
webObserveText(sample.responseTransferMs),
|
||||
webObserveShort(webObserveText(sample.timingStatus), 12),
|
||||
webObserveShort(webObserveText(sample.initiatorType), 12),
|
||||
webObserveShort(webObserveText(sample.nextHopProtocol), 12),
|
||||
webObserveShort(Array.isArray(sample.serverTimingNames) ? sample.serverTimingNames.join(",") : "-", 24),
|
||||
webObserveShort(webObserveText(sample.otelTraceId), 16),
|
||||
]);
|
||||
return rows.length > 0 ? rows : [["-", "-", "-", "-", "-", "-", "-", "-"]];
|
||||
return rows.length > 0 ? rows : [["-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"]];
|
||||
})()),
|
||||
"",
|
||||
"Archive page performance:",
|
||||
|
||||
@@ -1408,6 +1408,11 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
|
||||
decodedBodySize: Number.isFinite(Number(entry.decodedBodySize)) ? Number(entry.decodedBodySize) : null,
|
||||
nextHopProtocol: entry.nextHopProtocol || null,
|
||||
responseStatus: Number.isFinite(Number(entry.responseStatus)) ? Number(entry.responseStatus) : null,
|
||||
serverTiming: Array.from(entry.serverTiming || []).slice(0, 8).map((item) => ({
|
||||
name: String(item.name || "").slice(0, 80),
|
||||
duration: Number.isFinite(Number(item.duration)) ? Math.round(Number(item.duration)) : null,
|
||||
description: String(item.description || "").slice(0, 120)
|
||||
})),
|
||||
});
|
||||
const url = location.href;
|
||||
const routeSessionMatch = url.match(/\/workbench\/sessions\/([^/?#]+)/u);
|
||||
@@ -2456,6 +2461,8 @@ function performanceEntryCompletedEpochMs(sample, entry) {
|
||||
}
|
||||
|
||||
function compactPagePerformanceSlowSample({ sample, entry, entryTs, normalizedPath, rawPath, durationMs, streamOpenMs }) {
|
||||
const timingStatus = resourceTimingPhaseStatus(entry);
|
||||
const serverTiming = compactServerTiming(entry?.serverTiming);
|
||||
return {
|
||||
ts: entryTs ?? sample?.ts ?? null,
|
||||
sampleTs: sample?.ts ?? null,
|
||||
@@ -2475,10 +2482,16 @@ function compactPagePerformanceSlowSample({ sample, entry, entryTs, normalizedPa
|
||||
tlsStartMs: roundFinite(entry?.secureConnectionStart),
|
||||
requestToResponseStartMs: phaseDeltaMs(entry, "responseStart", "requestStart"),
|
||||
responseTransferMs: phaseDeltaMs(entry, "responseEnd", "responseStart"),
|
||||
timingStatus: timingStatus.status,
|
||||
invalidTimingPhases: timingStatus.invalidPhases,
|
||||
partialTimingPhases: timingStatus.partialPhases,
|
||||
transferSize: Number.isFinite(Number(entry?.transferSize)) ? Number(entry.transferSize) : null,
|
||||
encodedBodySize: Number.isFinite(Number(entry?.encodedBodySize)) ? Number(entry.encodedBodySize) : null,
|
||||
decodedBodySize: Number.isFinite(Number(entry?.decodedBodySize)) ? Number(entry.decodedBodySize) : null,
|
||||
nextHopProtocol: entry?.nextHopProtocol ?? null,
|
||||
serverTiming,
|
||||
serverTimingNames: serverTiming.map((item) => item.name).filter(Boolean).slice(0, 8),
|
||||
otelTraceId: extractOtelTraceIdFromServerTiming(serverTiming),
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
@@ -2486,10 +2499,48 @@ function compactPagePerformanceSlowSample({ sample, entry, entryTs, normalizedPa
|
||||
function phaseDeltaMs(entry, endKey, startKey) {
|
||||
const end = Number(entry?.[endKey]);
|
||||
const start = Number(entry?.[startKey]);
|
||||
if (!Number.isFinite(end) || !Number.isFinite(start) || end <= 0 || start < 0 || end < start) return null;
|
||||
if (!Number.isFinite(end) || !Number.isFinite(start) || end <= 0 || start <= 0 || end < start) return null;
|
||||
return Math.round(end - start);
|
||||
}
|
||||
|
||||
function resourceTimingPhaseStatus(entry) {
|
||||
const pairs = [
|
||||
["requestToResponseStart", "requestStart", "responseStart"],
|
||||
["responseTransfer", "responseStart", "responseEnd"],
|
||||
];
|
||||
const invalidPhases = [];
|
||||
const partialPhases = [];
|
||||
for (const [label, startKey, endKey] of pairs) {
|
||||
const start = Number(entry?.[startKey]);
|
||||
const end = Number(entry?.[endKey]);
|
||||
if (!Number.isFinite(start) || !Number.isFinite(end) || start <= 0 || end <= 0) {
|
||||
partialPhases.push(label);
|
||||
} else if (end < start) {
|
||||
invalidPhases.push(label);
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: invalidPhases.length > 0 ? "invalid" : (partialPhases.length > 0 ? "partial" : "complete"),
|
||||
invalidPhases,
|
||||
partialPhases,
|
||||
};
|
||||
}
|
||||
|
||||
function compactServerTiming(value) {
|
||||
const items = Array.isArray(value) ? value : [];
|
||||
return items.slice(0, 8).map((item) => ({
|
||||
name: truncate(String(item?.name || ""), 80),
|
||||
duration: Number.isFinite(Number(item?.duration)) ? Math.round(Number(item.duration)) : null,
|
||||
description: truncate(String(item?.description || ""), 120),
|
||||
})).filter((item) => item.name || item.description || item.duration !== null);
|
||||
}
|
||||
|
||||
function extractOtelTraceIdFromServerTiming(items) {
|
||||
const text = (Array.isArray(items) ? items : []).map((item) => [item.name, item.description].filter(Boolean).join(" ")).join(" ");
|
||||
const match = text.match(/\b[0-9a-f]{32}\b/iu);
|
||||
return match ? match[0].toLowerCase() : null;
|
||||
}
|
||||
|
||||
function roundFinite(value) {
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? Math.round(numeric) : null;
|
||||
|
||||
Reference in New Issue
Block a user