fix(web-probe): expose slow API samples in observe analysis (#644)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-22 09:59:40 +08:00
committed by GitHub
parent 49f2926731
commit 1780cb8980
3 changed files with 143 additions and 21 deletions
+28 -7
View File
@@ -7487,10 +7487,12 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec
"const stdoutJson = readJson(stdoutPath);",
"const reportJson = readJson(reportJsonPath);",
"const source = (stdoutJson && stdoutJson.ok !== false ? stdoutJson : null) || reportJson || stdoutJson || null;",
"const fullSource = reportJson || source;",
"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, lastTotalElapsedSeconds: v.lastTotalElapsedSeconds ?? null, lastRecentUpdateSeconds: v.lastRecentUpdateSeconds ?? null, diagnosticSamples: v.diagnosticSamples ?? null, terminalSamples: v.terminalSamples ?? 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 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 }; };",
"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 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.code, 48), code: clip(v.code ?? 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) }; };",
"const slimNetworkGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, method: clip(v.method, 12), status: v.status ?? null, path: clip(v.path, 96), firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, promptIndexes: Array.isArray(v.promptIndexes) ? v.promptIndexes.slice(0, 6) : [], failureKinds: Array.isArray(v.failureKinds) ? v.failureKinds.slice(0, 4).map((x) => clip(x, 48)) : [] }; };",
@@ -7516,7 +7518,11 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec
"const srcRuntimeAlerts = objectOrNull(source?.runtimeAlerts);",
"const runtimeAlerts = srcRuntimeAlerts ? { httpErrorCount: srcRuntimeAlerts.httpErrorCount ?? null, requestFailedCount: srcRuntimeAlerts.requestFailedCount ?? null, domDiagnosticSampleCount: srcRuntimeAlerts.domDiagnosticSampleCount ?? null, consoleAlertCount: srcRuntimeAlerts.consoleAlertCount ?? null } : null;",
"const srcPagePerformance = objectOrNull(source?.pagePerformance);",
"const pagePerformance = srcPagePerformance ? { sameOriginApiPaths: srcPagePerformance.sameOriginApiPaths ?? null, slowPathCount: srcPagePerformance.slowPathCount ?? null, slowSampleCount: srcPagePerformance.slowSampleCount ?? null, worstP95Ms: srcPagePerformance.worstP95Ms ?? null } : null;",
"const srcPagePerformanceSummary = objectOrNull(srcPagePerformance?.summary);",
"const pagePerformance = srcPagePerformance ? { sameOriginApiPaths: srcPagePerformance.sameOriginApiPaths ?? srcPagePerformanceSummary?.sameOriginApiPathCount ?? null, slowPathCount: srcPagePerformance.slowPathCount ?? srcPagePerformanceSummary?.slowPathCount ?? null, slowSampleCount: srcPagePerformance.slowSampleCount ?? srcPagePerformanceSummary?.slowSampleCount ?? null, worstP95Ms: srcPagePerformance.worstP95Ms ?? srcPagePerformanceSummary?.worstP95Ms ?? null } : null;",
"const fullRecentWindow = objectOrNull(fullSource?.windows?.recent);",
"const fullPagePerformance = objectOrNull(fullRecentWindow?.pagePerformance) || objectOrNull(fullSource?.pagePerformance);",
"const sourceSlowApi = Array.isArray(source?.pagePerformanceSlowApi) && source.pagePerformanceSlowApi.some((item) => Array.isArray(item?.slowSamples) && item.slowSamples.length > 0) ? source.pagePerformanceSlowApi : (Array.isArray(fullPagePerformance?.sameOriginApiByPath) ? fullPagePerformance.sameOriginApiByPath.filter((item) => Number(item?.overFiveSecondCount ?? 0) > 0) : []);",
"const srcPromptNetwork = objectOrNull(source?.promptNetwork);",
"const promptNetwork = srcPromptNetwork ? { promptSegments: srcPromptNetwork.promptSegments ?? null } : null;",
"const compact = source ? {",
@@ -7528,11 +7534,11 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec
" runtimeAlerts,",
" pagePerformance,",
" promptNetwork,",
" pagePerformanceSlowApi: takeHead(source.pagePerformanceSlowApi, 5).map(slimSlowApi),",
" findings: takeHead(source.findings, 8).map(slimFinding),",
" httpErrorGroups: takeHead(source.httpErrorGroups, 8).map(slimNetworkGroup),",
" requestFailedGroups: takeHead(source.requestFailedGroups, 8).map(slimNetworkGroup),",
" domDiagnosticGroups: takeHead(source.domDiagnosticGroups, 5).map(slimDomGroup),",
" pagePerformanceSlowApi: takeHead(sourceSlowApi, 4).map(slimSlowApi),",
" findings: takeHead(source.findings, 6).map(slimFinding),",
" httpErrorGroups: takeHead(source.httpErrorGroups, 4).map(slimNetworkGroup),",
" requestFailedGroups: takeHead(source.requestFailedGroups, 5).map(slimNetworkGroup),",
" domDiagnosticGroups: takeHead(source.domDiagnosticGroups, 3).map(slimDomGroup),",
" domDiagnosticSamples: takeHead(source.domDiagnosticSamples, 5).map(slimDomSample),",
" consoleAlertGroups: takeHead(source.consoleAlertGroups, 5).map(slimConsoleGroup),",
" consoleAlertSamples: takeHead(source.consoleAlertSamples, 5).map(slimConsoleSample),",
@@ -7799,6 +7805,21 @@ function renderWebObserveAnalyzeTable(value: Record<string, unknown>): string {
webObserveText(item.overFiveSecondCount),
]) : [["-", "-", "-", "-", "-"]]),
"",
"Slow API samples (>5s):",
webObserveTable(["TS", "SEQ", "PATH", "DURATION", "REQ_WAIT", "RESP_XFER", "INIT", "PROTO"], (() => {
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),
webObserveShort(webObserveText(sample.path), 44),
webObserveText(sample.durationMs),
webObserveText(sample.requestToResponseStartMs ?? sample.streamOpenMs),
webObserveText(sample.responseTransferMs),
webObserveShort(webObserveText(sample.initiatorType), 12),
webObserveShort(webObserveText(sample.nextHopProtocol), 12),
]);
return rows.length > 0 ? rows : [["-", "-", "-", "-", "-", "-", "-", "-"]];
})()),
"",
"DOM diagnostics:",
webObserveTable(domDiagnostics.length > 0 ? ["COUNT", "FIRST", "LAST", "TEXT"] : ["SEQ", "TS", "META", "TEXT"], domDiagnosticRows.length > 0 ? domDiagnosticRows : [["-", "-", "-", "-"]]),
"",
@@ -1488,7 +1488,23 @@ function compactPerformanceItems(items) {
name: item?.name ?? null,
initiatorType: item?.initiatorType ?? null,
startTime: item?.startTime ?? null,
duration: item?.duration ?? null
duration: item?.duration ?? null,
workerStart: item?.workerStart ?? null,
redirectStart: item?.redirectStart ?? null,
redirectEnd: item?.redirectEnd ?? null,
fetchStart: item?.fetchStart ?? null,
domainLookupStart: item?.domainLookupStart ?? null,
domainLookupEnd: item?.domainLookupEnd ?? null,
connectStart: item?.connectStart ?? null,
connectEnd: item?.connectEnd ?? null,
secureConnectionStart: item?.secureConnectionStart ?? null,
requestStart: item?.requestStart ?? null,
responseStart: item?.responseStart ?? null,
responseEnd: item?.responseEnd ?? null,
transferSize: item?.transferSize ?? null,
encodedBodySize: item?.encodedBodySize ?? null,
decodedBodySize: item?.decodedBodySize ?? null,
nextHopProtocol: item?.nextHopProtocol ?? null
}));
}
@@ -1707,10 +1723,12 @@ function buildPagePerformanceReport(samples, manifest) {
lastSeq: sample.seq ?? null,
initiatorTypes: [],
pageAssetFingerprints: [],
slowSamples: [],
valuesRedacted: true
};
group.sampleCount += 1;
group.durationsMs.push(durationMs);
let overBudget = false;
if (isLongLivedStream) {
if (durationMs > 5000) group.streamLifetimeOverFiveSecondCount += 1;
if (streamOpenMs !== null) {
@@ -1718,11 +1736,14 @@ function buildPagePerformanceReport(samples, manifest) {
if (streamOpenMs > 5000) {
group.streamOpenOverFiveSecondCount += 1;
group.overFiveSecondCount += 1;
overBudget = true;
}
}
} else if (durationMs > 5000) {
group.overFiveSecondCount += 1;
overBudget = true;
}
if (overBudget && group.slowSamples.length < 80) group.slowSamples.push(compactPagePerformanceSlowSample({ sample, entry, normalizedPath, rawPath: parsed.path, durationMs, streamOpenMs }));
group.lastAt = sample.ts ?? null;
group.lastSeq = sample.seq ?? null;
if (parsed.path && !group.rawPathSamples.includes(parsed.path)) group.rawPathSamples.push(parsed.path);
@@ -1761,6 +1782,10 @@ function buildPagePerformanceReport(samples, manifest) {
initiatorTypes: group.initiatorTypes,
rawPathSamples: group.rawPathSamples.slice(0, 8),
pageAssetFingerprints: group.pageAssetFingerprints.slice(0, 8),
slowSamples: group.slowSamples
.slice()
.sort((a, b) => Number(b.durationMs ?? 0) - Number(a.durationMs ?? 0))
.slice(0, 12),
valuesRedacted: true
};
}).sort((a, b) => (b.overFiveSecondCount - a.overFiveSecondCount) || (Number(b.p95Ms ?? 0) - Number(a.p95Ms ?? 0)) || a.path.localeCompare(b.path));
@@ -1786,6 +1811,45 @@ function buildPagePerformanceReport(samples, manifest) {
};
}
function compactPagePerformanceSlowSample({ sample, entry, normalizedPath, rawPath, durationMs, streamOpenMs }) {
return {
ts: sample?.ts ?? null,
seq: sample?.seq ?? null,
path: normalizedPath ?? null,
rawPath: rawPath ?? null,
initiatorType: entry?.initiatorType ?? null,
durationMs: roundFinite(durationMs),
startTimeMs: roundFinite(entry?.startTime),
fetchStartMs: roundFinite(entry?.fetchStart),
requestStartMs: roundFinite(entry?.requestStart),
responseStartMs: roundFinite(entry?.responseStart),
responseEndMs: roundFinite(entry?.responseEnd),
streamOpenMs: roundFinite(streamOpenMs),
dnsMs: phaseDeltaMs(entry, "domainLookupEnd", "domainLookupStart"),
tcpMs: phaseDeltaMs(entry, "connectEnd", "connectStart"),
tlsStartMs: roundFinite(entry?.secureConnectionStart),
requestToResponseStartMs: phaseDeltaMs(entry, "responseStart", "requestStart"),
responseTransferMs: phaseDeltaMs(entry, "responseEnd", "responseStart"),
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,
valuesRedacted: true
};
}
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;
return Math.round(end - start);
}
function roundFinite(value) {
const numeric = Number(value);
return Number.isFinite(numeric) ? Math.round(numeric) : null;
}
function classifyApiPerformanceRoute(normalizedPath, entry = {}) {
if (normalizedPath === "/v1/workbench/events") return "same-origin-api-stream";
if (String(entry?.initiatorType ?? "").toLowerCase() === "eventsource") return "same-origin-api-stream";
+50 -13
View File
@@ -139,6 +139,7 @@ interface SearchOptions extends CommonOptions {
limit: number;
candidateLimit: number;
lookbackMinutes: number;
endAt: string | null;
}
interface DiagnoseCodeAgentOptions extends CommonOptions {
@@ -273,6 +274,7 @@ function parseSearchOptions(args: string[]): SearchOptions {
let limit = 20;
let candidateLimit = 80;
let lookbackMinutes = 360;
let endAt: string | null = null;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--grep") {
@@ -319,6 +321,13 @@ function parseSearchOptions(args: string[]): SearchOptions {
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 10080) throw new Error(`${arg} must be an integer from 1 to 10080`);
lookbackMinutes = parsed;
index += 1;
} else if (arg === "--end-at") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--end-at requires an ISO timestamp value");
const parsed = Date.parse(value);
if (!Number.isFinite(parsed)) throw new Error("--end-at must be an ISO timestamp parseable by Date.parse");
endAt = new Date(parsed).toISOString();
index += 1;
} else {
commonArgs.push(arg);
if (arg === "--target") {
@@ -328,7 +337,7 @@ function parseSearchOptions(args: string[]): SearchOptions {
}
}
if (grep === null && query === null && path === null && status === null) throw new Error("observability search requires --grep <text>, --query <tempo-query>, --path <route>, or --status <code>");
return { ...parseCommonOptions(commonArgs), grep, query, path, status, limit, candidateLimit, lookbackMinutes };
return { ...parseCommonOptions(commonArgs), grep, query, path, status, limit, candidateLimit, lookbackMinutes, endAt };
}
function parseDiagnoseCodeAgentOptions(args: string[]): DiagnoseCodeAgentOptions {
@@ -687,7 +696,9 @@ async function search(config: UniDeskConfig, options: SearchOptions): Promise<Re
query: effectiveTempoQuery,
lookbackMinutes: effectiveLookbackMinutes,
};
const endSeconds = Math.floor(Date.now() / 1000);
const endMillis = options.endAt === null ? Date.now() : Date.parse(options.endAt);
if (!Number.isFinite(endMillis)) throw new Error("--end-at must be an ISO timestamp parseable by Date.parse");
const endSeconds = Math.floor(endMillis / 1000);
const startSeconds = endSeconds - (effectiveLookbackMinutes * 60);
const params = new URLSearchParams({
start: String(startSeconds),
@@ -710,6 +721,7 @@ async function search(config: UniDeskConfig, options: SearchOptions): Promise<Re
statusFilter: options.status,
lookbackMinutes: effectiveLookbackMinutes,
requestedLookbackMinutes: options.lookbackMinutes,
endAt: new Date(endSeconds * 1000).toISOString(),
businessTraceId,
mode: businessTraceId === null ? "candidate-grep" : "business-trace-exact",
candidateLimit: options.candidateLimit,
@@ -882,23 +894,30 @@ function renderSearchTable(input: {
result: Record<string, unknown>;
}): RenderedCliResult {
const traces = asArray(input.result.traces).map((item) => asPlainRecord(item) ?? {});
const rows = traces.slice(0, 12).map((trace) => [
shortenMiddle(textValue(trace.traceId), 20),
searchTraceStatus(trace),
numberValue(trace.spanCount),
numberValue(trace.errorSpanCount),
numberValue(trace.matchedSpanCount),
joinValues(trace.services, 46),
]);
const requestedLimit = numberFromUnknown(input.options.limit);
const rowLimit = Math.max(1, Math.min(Number.isFinite(requestedLimit) ? Math.round(requestedLimit) : 12, 40));
const rows = traces.slice(0, rowLimit).map((trace) => {
const meta = asPlainRecord(trace.meta) ?? {};
return [
shortenMiddle(textValue(trace.traceId), 32),
shortenEnd(textValue(trace.startAt ?? isoFromUnixNano(meta.startTimeUnixNano)), 20),
numberValue(trace.durationMs ?? meta.durationMs),
searchTraceStatus(trace),
numberValue(trace.spanCount),
numberValue(trace.errorSpanCount),
numberValue(trace.matchedSpanCount),
joinValues(trace.services, 38),
];
});
const lines = [
`platform-infra observability search (${input.ok ? "ok" : "not-ok"})`,
"",
formatTable(["TRACE", "STATUS", "SPANS", "ERR", "MATCH", "SERVICES"], rows.length > 0 ? rows : [["-", "no-match", "-", "-", "-", "-"]]),
formatTable(["TRACE", "START", "DUR_MS", "STATUS", "SPANS", "ERR", "MATCH", "SERVICES"], rows.length > 0 ? rows : [["-", "-", "-", "no-match", "-", "-", "-", "-"]]),
"",
"Summary:",
` target=${input.target.id} backend=${textValue(input.query.backend)} service=${textValue(input.query.service)}`,
` grep=${textValue(input.query.grep)} path=${textValue(input.query.pathFilter)} status=${textValue(input.query.statusFilter)} tempoQuery=${shortenMiddle(textValue(input.query.tempoQuery), 80)}`,
` mode=${textValue(input.query.mode)} businessTraceId=${textValue(input.query.businessTraceId)} lookbackMinutes=${textValue(input.query.lookbackMinutes)} requestedLookbackMinutes=${textValue(input.query.requestedLookbackMinutes)}`,
` mode=${textValue(input.query.mode)} businessTraceId=${textValue(input.query.businessTraceId)} lookbackMinutes=${textValue(input.query.lookbackMinutes)} requestedLookbackMinutes=${textValue(input.query.requestedLookbackMinutes)} endAt=${textValue(input.query.endAt)}`,
` candidateLimit=${textValue(input.query.candidateLimit)} limit=${textValue(input.query.limit)}`,
` candidates=${textValue(input.result.candidateTraceCount)} scanned=${textValue(input.result.scannedTraceCount)} matched=${textValue(input.result.matchedTraceCount)} stopped=${textValue(input.result.scanStopped)}`,
];
@@ -1090,6 +1109,7 @@ function buildSearchCommand(target: ObservabilityTarget, options: SearchOptions,
if (options.query !== null) parts.push("--query", options.query);
if (options.path !== null) parts.push("--path", options.path);
if (options.status !== null) parts.push("--status", String(options.status));
if (options.endAt !== null) parts.push("--end-at", options.endAt);
parts.push("--lookback-minutes", String(options.lookbackMinutes), "--candidate-limit", String(options.candidateLimit), "--limit", String(options.limit));
if (full) parts.push("--full");
return parts.map(cliArg).join(" ");
@@ -1138,6 +1158,19 @@ function numberFromUnknown(value: unknown): number {
return Number.NaN;
}
function isoFromUnixNano(value: unknown): string {
if (value === null || value === undefined || value === "") return "-";
try {
const text = String(value);
if (!/^\d+$/u.test(text)) return "-";
const millis = Number(BigInt(text) / 1000000n);
if (!Number.isFinite(millis) || millis <= 0) return "-";
return new Date(millis).toISOString().replace(/\.000Z$/u, "Z");
} catch {
return "-";
}
}
function joinValues(value: unknown, maxLength: number): string {
const joined = asArray(value).map(textValue).filter((item) => item !== "-").join(",");
return shortenEnd(joined.length > 0 ? joined : textValue(value), maxLength);
@@ -1188,11 +1221,15 @@ function compactSearchResult(value: unknown): Record<string, unknown> {
function compactSearchTrace(value: unknown): Record<string, unknown> {
const trace = asPlainRecord(value) ?? {};
const meta = compactMetaRecord(trace.meta);
const startTimeUnixNano = trace.startTimeUnixNano ?? meta?.startTimeUnixNano ?? null;
return {
traceId: trace.traceId ?? null,
startAt: isoFromUnixNano(startTimeUnixNano),
durationMs: trace.durationMs ?? meta?.durationMs ?? null,
traceCommand: trace.traceCommand ?? null,
grepCommand: trace.grepCommand ?? null,
meta: compactMetaRecord(trace.meta),
meta,
queryOk: trace.queryOk ?? null,
parseOk: trace.parseOk ?? null,
rawMatched: trace.rawMatched ?? null,