diff --git a/scripts/src/hwlab-node-impl.ts b/scripts/src/hwlab-node-impl.ts index 989c274c..15ac603b 100644 --- a/scripts/src/hwlab-node-impl.ts +++ b/scripts/src/hwlab-node-impl.ts @@ -7475,6 +7475,7 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec "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 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)) : [] }; };", "const slimDomSample = (item) => { const v = objectOrNull(item) || {}; return { seq: v.seq ?? null, ts: v.ts ?? null, source: clip(v.source, 32), diagnosticCode: clip(v.diagnosticCode, 48), traceId: clip(v.traceId, 64), httpStatus: v.httpStatus ?? null, idleSeconds: v.idleSeconds ?? null, waitingFor: clip(v.waitingFor, 48), lastEventLabel: clip(v.lastEventLabel, 80), text: clip(v.text ?? v.preview, 180) }; };", "const slimConsoleGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, type: clip(v.type, 24), status: v.status ?? null, path: clip(v.path, 96), lastAt: v.lastAt ?? v.firstAt ?? null, firstAt: v.firstAt ?? null, traceIds: Array.isArray(v.traceIds) ? v.traceIds.slice(0, 3).map((x) => clip(x, 64)) : [] }; };", "const slimConsoleSample = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, type: clip(v.type, 24), status: v.status ?? null, path: clip(v.path, 96), traceId: clip(v.traceId, 64), text: clip(v.text, 180) }; };", @@ -7502,12 +7503,16 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec "const compact = source ? {", " ok: analyzerExit === 0 && source.ok !== false,", " counts: source.counts ?? null,", + " analysisWindow: objectOrNull(source.analysisWindow),", + " archiveSummary: objectOrNull(source.archiveSummary),", " sampleMetrics: metrics,", " 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),", " domDiagnosticSamples: takeHead(source.domDiagnosticSamples, 5).map(slimDomSample),", " consoleAlertGroups: takeHead(source.consoleAlertGroups, 5).map(slimConsoleGroup),", @@ -7571,6 +7576,8 @@ function renderWebObserveAnalyzeTable(value: Record): string { const analyzer = record(analysis?.analyzer); const result = record(value.result); const counts = record(analysis?.counts); + const analysisWindow = record(analysis?.analysisWindow); + const archiveSummary = record(analysis?.archiveSummary); const sampleMetrics = record(analysis?.sampleMetrics); const runtimeAlerts = record(analysis?.runtimeAlerts); const pagePerformance = record(analysis?.pagePerformance); @@ -7581,6 +7588,8 @@ function renderWebObserveAnalyzeTable(value: Record): string { const turnColumnCount = Array.isArray(sampleMetrics?.turnColumns) ? sampleMetrics.turnColumns.length : sampleMetrics?.turnColumns; const slowApis = Array.isArray(analysis?.pagePerformanceSlowApi) ? analysis.pagePerformanceSlowApi.map(record).filter((item): item is Record => item !== null).slice(0, 8) : []; const findings = Array.isArray(analysis?.findings) ? analysis.findings.map(record).filter((item): item is Record => item !== null).slice(0, 8) : []; + const httpErrorGroups = Array.isArray(analysis?.httpErrorGroups) ? analysis.httpErrorGroups.map(record).filter((item): item is Record => item !== null).slice(0, 8) : []; + const requestFailedGroups = Array.isArray(analysis?.requestFailedGroups) ? analysis.requestFailedGroups.map(record).filter((item): item is Record => item !== null).slice(0, 8) : []; const domDiagnostics = Array.isArray(analysis?.domDiagnosticGroups) ? analysis.domDiagnosticGroups.map(record).filter((item): item is Record => item !== null).slice(0, 5) : []; const domDiagnosticSamples = Array.isArray(analysis?.domDiagnosticSamples) ? analysis.domDiagnosticSamples.map(record).filter((item): item is Record => item !== null).slice(0, 8) : []; const consoleAlertGroups = Array.isArray(analysis?.consoleAlertGroups) ? analysis.consoleAlertGroups.map(record).filter((item): item is Record => item !== null).slice(0, 8) : []; @@ -7635,6 +7644,22 @@ function renderWebObserveAnalyzeTable(value: Record): string { webObserveText(item.allowedIncreaseSeconds), webObserveShort(webObserveText(item.traceId), 24), ]); + const httpErrorRows = httpErrorGroups.map((item) => [ + webObserveText(item.count), + webObserveText(item.method), + webObserveText(item.status), + webObserveShort(webObserveText(item.path), 52), + webObserveShort(webObserveText(item.lastAt ?? item.firstAt), 24), + webObserveShort(webObserveArray(item.failureKinds).join(",") || "-", 40), + ]); + const requestFailedRows = requestFailedGroups.map((item) => [ + webObserveText(item.count), + webObserveText(item.method), + webObserveText(item.status), + webObserveShort(webObserveText(item.path), 52), + webObserveShort(webObserveText(item.lastAt ?? item.firstAt), 24), + webObserveShort(webObserveArray(item.failureKinds).join(",") || "-", 40), + ]); const lines = [ `hwlab nodes web-probe observe analyze (${webObserveText(value.status)})`, "", @@ -7665,6 +7690,18 @@ function renderWebObserveAnalyzeTable(value: Record): string { webObserveText(counts?.artifacts), ]]), "", + "Analysis window:", + webObserveTable(["WINDOW", "FROM", "TO", "SAMPLES", "NETWORK", "CONSOLE", "ARCHIVE_SAMPLES", "ARCHIVE_RED"], [[ + webObserveText(analysisWindow?.name ?? "recent-5m"), + webObserveShort(webObserveText(analysisWindow?.fromAt), 24), + webObserveShort(webObserveText(analysisWindow?.toAt), 24), + webObserveText(analysisWindow?.samples), + webObserveText(analysisWindow?.network), + webObserveText(analysisWindow?.console), + webObserveText(record(archiveSummary?.sampleMetrics)?.sampleCount ?? counts?.samples), + webObserveText(archiveSummary?.redFindingCount), + ]]), + "", "Reports:", webObserveTable(["KIND", "PATH", "SHA256"], [ ["json", webObserveShort(webObserveText(analysis?.reportJsonPath), 96), webObserveShort(webObserveText(analysis?.reportJsonSha256), 24)], @@ -7717,6 +7754,12 @@ function renderWebObserveAnalyzeTable(value: Record): string { webObserveText(promptNetwork?.promptSegments), ]]), "", + "HTTP error groups:", + webObserveTable(["COUNT", "METHOD", "STATUS", "PATH", "LAST", "FAILURE"], httpErrorRows.length > 0 ? httpErrorRows : [["-", "-", "-", "-", "-", "-"]]), + "", + "Request failed groups:", + webObserveTable(["COUNT", "METHOD", "STATUS", "PATH", "LAST", "FAILURE"], requestFailedRows.length > 0 ? requestFailedRows : [["-", "-", "-", "-", "-", "-"]]), + "", "Slow API (>5s):", webObserveTable(["PATH", "SAMPLES", "P95", "MAX", "OVER5S"], slowApis.length > 0 ? slowApis.map((item) => [ webObserveShort(webObserveText(item.path ?? item.route), 52), diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 4e089097..e80ff286 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -1114,6 +1114,7 @@ const promptNetwork = buildPromptNetworkReport(control, network); const runtimeAlerts = buildRuntimeAlerts(samples, control, network, consoleEvents, errors); const findings = buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance); if (jsonlReadIssues.length > 0) findings.unshift({ id: "jsonl-read-issues", severity: "red", summary: "observer analyzer hit JSONL read/parse issues", count: jsonlReadIssues.length, issues: jsonlReadIssues.slice(0, 20) }); +const recentWindow = buildRecentAnalysisWindow({ samples, control, network, consoleEvents, errors, manifest }); const commandTimeline = control.filter((item) => item.phase === "completed" || item.phase === "failed").map((item) => ({ ts: item.ts, phase: item.phase, commandId: item.commandId, type: item.type, input: item.input, afterUrl: item.afterUrl })); const report = { ok: findings.filter((item) => item.severity === "red").length === 0, @@ -1131,6 +1132,7 @@ const report = { promptNetwork, runtimeAlerts, findings, + windows: { recent: recentWindow }, readIssues: jsonlReadIssues, artifactSummary: await artifactSummary(artifacts), safety: { offlineOnly: true, browserDriven: false, apiFetch: false, valuesRedacted: true }, @@ -1147,18 +1149,46 @@ console.log(JSON.stringify({ reportMdPath, reportMdSha256: mdMeta.sha256, counts: report.counts, + archiveSummary: { + sampleMetrics: sampleMetrics.summary, + pagePerformance: pagePerformance.summary, + runtimeAlerts: runtimeAlerts.summary, + findingCount: findings.length, + redFindingCount: findings.filter((item) => item.severity === "red").length, + }, + analysisWindow: recentWindow.summary, jsonlReadIssues: jsonlReadIssues.slice(0, 3).map((item) => ({ file: item.file, line: item.line ?? null, error: String(item.error ?? "").slice(0, 160) })), readIssues: jsonlReadIssues.slice(0, 3).map((item) => ({ file: item.file, line: item.line ?? null, error: String(item.error ?? "").slice(0, 160) })), sampleMetrics: { - ...sampleMetrics.summary, - rounds: sampleMetrics.rounds.slice(-8).map((item) => ({ promptIndex: item.promptIndex, promptTextHash: item.promptTextHash, sampleCount: item.sampleCount, firstSeq: item.firstSeq, lastSeq: item.lastSeq, lastTotalElapsedSeconds: item.lastTotalElapsedSeconds, lastRecentUpdateSeconds: item.lastRecentUpdateSeconds, diagnosticSamples: item.diagnosticSamples, terminalSamples: item.terminalSamples, finalTextSamples: item.finalTextSamples, turnTimingRecentUpdateJumpCount: item.turnTimingRecentUpdateJumpCount, turnTimingRecentUpdateMaxIncreaseSeconds: item.turnTimingRecentUpdateMaxIncreaseSeconds })), - turnColumns: sampleMetrics.turnColumns.slice(-12).map((item) => ({ label: item.label, source: item.source, promptIndex: item.promptIndex, lastPromptIndex: item.lastPromptIndex, firstSeq: item.firstSeq, lastSeq: item.lastSeq, traceId: item.traceId, messageId: item.messageId })), + ...recentWindow.sampleMetrics.summary, + rounds: recentWindow.sampleMetrics.rounds.slice(-8).map((item) => ({ promptIndex: item.promptIndex, promptTextHash: item.promptTextHash, sampleCount: item.sampleCount, firstSeq: item.firstSeq, lastSeq: item.lastSeq, lastTotalElapsedSeconds: item.lastTotalElapsedSeconds, lastRecentUpdateSeconds: item.lastRecentUpdateSeconds, diagnosticSamples: item.diagnosticSamples, terminalSamples: item.terminalSamples, finalTextSamples: item.finalTextSamples, turnTimingRecentUpdateJumpCount: item.turnTimingRecentUpdateJumpCount, turnTimingRecentUpdateMaxIncreaseSeconds: item.turnTimingRecentUpdateMaxIncreaseSeconds })), + turnColumns: recentWindow.sampleMetrics.turnColumns.slice(-12).map((item) => ({ label: item.label, source: item.source, promptIndex: item.promptIndex, lastPromptIndex: item.lastPromptIndex, firstSeq: item.firstSeq, lastSeq: item.lastSeq, traceId: item.traceId, messageId: item.messageId })), }, - pageProvenance: pageProvenance.summary, - pagePerformance: pagePerformance.summary, - promptNetwork: promptNetwork.summary, - runtimeAlerts: runtimeAlerts.summary, - domDiagnosticGroups: runtimeAlerts.domDiagnosticsByFingerprint.slice(0, 5).map((item) => ({ + pageProvenance: recentWindow.pageProvenance.summary, + pagePerformance: recentWindow.pagePerformance.summary, + promptNetwork: recentWindow.promptNetwork.summary, + runtimeAlerts: recentWindow.runtimeAlerts.summary, + httpErrorGroups: recentWindow.runtimeAlerts.networkHttpErrorsByPath.slice(0, 8).map((item) => ({ + method: item.method ?? null, + status: item.status ?? null, + path: item.urlPath ?? null, + count: item.count, + firstAt: item.firstAt ?? null, + lastAt: item.lastAt ?? null, + promptIndexes: Array.isArray(item.promptIndexes) ? item.promptIndexes.slice(0, 6) : [], + failureKinds: Array.isArray(item.failureKinds) ? item.failureKinds.slice(0, 4) : [], + })), + requestFailedGroups: recentWindow.runtimeAlerts.networkRequestFailedByPath.slice(0, 8).map((item) => ({ + method: item.method ?? null, + status: item.status ?? null, + path: item.urlPath ?? null, + count: item.count, + firstAt: item.firstAt ?? null, + lastAt: item.lastAt ?? null, + promptIndexes: Array.isArray(item.promptIndexes) ? item.promptIndexes.slice(0, 6) : [], + failureKinds: Array.isArray(item.failureKinds) ? item.failureKinds.slice(0, 4) : [], + })), + domDiagnosticGroups: recentWindow.runtimeAlerts.domDiagnosticsByFingerprint.slice(0, 5).map((item) => ({ diagnosticCode: item.diagnosticCode ?? null, count: item.count, firstAt: item.firstAt, @@ -1167,7 +1197,7 @@ console.log(JSON.stringify({ traceIds: Array.isArray(item.traceIds) ? item.traceIds.slice(0, 4) : [], text: String(item.preview ?? item.normalizedPreview ?? item.text ?? "").slice(0, 160), })), - domDiagnosticSamples: runtimeAlerts.domDiagnostics.slice(-8).map((item) => ({ + domDiagnosticSamples: recentWindow.runtimeAlerts.domDiagnostics.slice(-8).map((item) => ({ seq: item.seq ?? null, ts: item.ts ?? null, promptIndex: item.promptIndex ?? null, @@ -1180,7 +1210,7 @@ console.log(JSON.stringify({ lastEventLabel: item.lastEventLabel ?? null, text: String(item.preview ?? item.text ?? "").slice(0, 180), })), - consoleAlertGroups: runtimeAlerts.consoleAlertsByPath.slice(0, 8).map((item) => ({ + consoleAlertGroups: recentWindow.runtimeAlerts.consoleAlertsByPath.slice(0, 8).map((item) => ({ type: item.type ?? null, status: item.status ?? null, path: item.urlPath ?? null, @@ -1190,7 +1220,7 @@ console.log(JSON.stringify({ promptIndexes: Array.isArray(item.promptIndexes) ? item.promptIndexes.slice(0, 6) : [], traceIds: Array.isArray(item.traceIds) ? item.traceIds.slice(0, 4) : [], })), - consoleAlertSamples: runtimeAlerts.consoleAlerts.slice(-8).map((item) => ({ + consoleAlertSamples: recentWindow.runtimeAlerts.consoleAlerts.slice(-8).map((item) => ({ ts: item.ts ?? null, promptIndex: item.promptIndex ?? null, type: item.type ?? null, @@ -1199,7 +1229,7 @@ console.log(JSON.stringify({ traceId: item.traceId ?? null, text: String(item.preview ?? item.text ?? "").slice(0, 180), })), - turnTimingRecentUpdateJumps: sampleMetrics.turnTimingRecentUpdateSawtoothJumps.slice(0, 8).map((item) => ({ + turnTimingRecentUpdateJumps: recentWindow.sampleMetrics.turnTimingRecentUpdateSawtoothJumps.slice(0, 8).map((item) => ({ columnLabel: item.columnLabel ?? item.columnId ?? null, promptIndex: item.promptIndex ?? null, traceId: item.traceId ?? null, @@ -1214,8 +1244,8 @@ console.log(JSON.stringify({ allowedIncreaseSeconds: item.allowedIncreaseSeconds ?? null, excessiveIncreaseSeconds: item.excessiveIncreaseSeconds ?? null, })), - pagePerformanceSlowApi: pagePerformance.sameOriginApiByPath.filter((item) => item.overFiveSecondCount > 0).slice(0, 5).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, p95Ms: item.p95Ms, maxMs: item.maxMs, overFiveSecondCount: item.overFiveSecondCount })), - findings: findings.slice(0, 8).map((item) => ({ kind: item.id ?? item.kind ?? item.code, severity: item.severity, count: item.count ?? item.sampleCount ?? null, summary: String(item.summary ?? item.message ?? "").slice(0, 180) })), + pagePerformanceSlowApi: recentWindow.pagePerformance.sameOriginApiByPath.filter((item) => item.overFiveSecondCount > 0).slice(0, 5).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, p95Ms: item.p95Ms, maxMs: item.maxMs, overFiveSecondCount: item.overFiveSecondCount })), + findings: recentWindow.findings.slice(0, 8).map((item) => ({ kind: item.id ?? item.kind ?? item.code, severity: item.severity, count: item.count ?? item.sampleCount ?? null, summary: String(item.summary ?? item.message ?? "").slice(0, 180) })), valuesRedacted: true, })); @@ -1389,6 +1419,58 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN return findings; } +function buildRecentAnalysisWindow({ samples, control, network, consoleEvents, errors, manifest }) { + const latestSampleMs = latestTimestampMs(samples); + const windowMs = 5 * 60 * 1000; + const fromMs = Number.isFinite(latestSampleMs) ? latestSampleMs - windowMs : Number.NEGATIVE_INFINITY; + const toMs = Number.isFinite(latestSampleMs) ? latestSampleMs : Number.POSITIVE_INFINITY; + const inWindow = (item) => { + const tsMs = Date.parse(item?.ts); + return Number.isFinite(tsMs) && tsMs >= fromMs && tsMs <= toMs; + }; + const windowSamples = samples.filter(inWindow); + const windowControl = control.filter(inWindow); + const windowNetwork = network.filter(inWindow); + const windowConsole = consoleEvents.filter(inWindow); + const windowErrors = errors.filter(inWindow); + const sampleMetrics = buildSampleMetrics(windowSamples, control); + const pageProvenance = buildPageProvenanceReport(windowSamples, windowControl, manifest); + const pagePerformance = buildPagePerformanceReport(windowSamples, manifest); + const promptNetwork = buildPromptNetworkReport(control, windowNetwork); + const runtimeAlerts = buildRuntimeAlerts(windowSamples, control, windowNetwork, windowConsole, windowErrors); + const findings = buildFindings(windowSamples, control, windowNetwork, windowErrors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance); + return { + summary: { + name: "recent-5m", + windowMs, + fromAt: Number.isFinite(fromMs) ? new Date(fromMs).toISOString() : null, + toAt: Number.isFinite(toMs) ? new Date(toMs).toISOString() : null, + samples: windowSamples.length, + control: windowControl.length, + network: windowNetwork.length, + console: windowConsole.length, + errors: windowErrors.length, + valuesRedacted: true + }, + sampleMetrics, + pageProvenance, + pagePerformance, + promptNetwork, + runtimeAlerts, + findings, + valuesRedacted: true + }; +} + +function latestTimestampMs(items) { + let latest = Number.NEGATIVE_INFINITY; + for (const item of items || []) { + const tsMs = Date.parse(item?.ts); + if (Number.isFinite(tsMs) && tsMs > latest) latest = tsMs; + } + return latest; +} + function buildPageProvenanceReport(samples, control, manifest) { const groups = new Map(); for (const sample of samples) {