diff --git a/scripts/src/hwlab-node-impl.ts b/scripts/src/hwlab-node-impl.ts index 664751ff..120e4d83 100644 --- a/scripts/src/hwlab-node-impl.ts +++ b/scripts/src/hwlab-node-impl.ts @@ -7264,7 +7264,7 @@ function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOptions, spec }; } -function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record { +function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record | RenderedCliResult { const analyzerB64 = Buffer.from(nodeWebObserveAnalyzerSource(), "utf8").toString("base64"); const script = [ "set -eu", @@ -7276,7 +7276,7 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec ].join("\n"); const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds); const analysis = parseJsonObject(result.stdout); - return { + return withWebObserveAnalyzeRendered({ ok: result.exitCode === 0 && analysis?.ok === true, status: result.exitCode === 0 && analysis?.ok === true ? "analyzed" : "blocked", command: webObserveCommandLabel("analyze", options), @@ -7287,9 +7287,109 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec analysis, result: compactCommandResult(result), valuesRedacted: true, + }); +} + +function withWebObserveAnalyzeRendered(value: Record): RenderedCliResult { + return { + ok: value.ok !== false, + command: typeof value.command === "string" ? value.command : "hwlab nodes web-probe observe analyze", + contentType: "text/plain", + renderedText: renderWebObserveAnalyzeTable(value), }; } +function renderWebObserveAnalyzeTable(value: Record): string { + const analysis = record(value.analysis); + const counts = record(analysis?.counts); + const sampleMetrics = record(analysis?.sampleMetrics); + const runtimeAlerts = record(analysis?.runtimeAlerts); + const pagePerformance = record(analysis?.pagePerformance); + const promptNetwork = record(analysis?.promptNetwork); + 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 domDiagnostics = Array.isArray(analysis?.domDiagnosticGroups) ? analysis.domDiagnosticGroups.map(record).filter((item): item is Record => item !== null).slice(0, 5) : []; + const lines = [ + `hwlab nodes web-probe observe analyze (${webObserveText(value.status)})`, + "", + webObserveTable(["ID", "NODE", "LANE", "SAMPLES", "CONTROL", "NETWORK", "CONSOLE", "ARTIFACTS"], [[ + webObserveText(value.id), + webObserveText(value.node), + webObserveText(value.lane), + webObserveText(counts?.samples), + webObserveText(counts?.control), + webObserveText(counts?.network), + webObserveText(counts?.console), + webObserveText(counts?.artifacts), + ]]), + "", + "Reports:", + webObserveTable(["KIND", "PATH", "SHA256"], [ + ["json", webObserveShort(webObserveText(analysis?.reportJsonPath), 96), webObserveShort(webObserveText(analysis?.reportJsonSha256), 24)], + ["md", webObserveShort(webObserveText(analysis?.reportMdPath), 96), webObserveShort(webObserveText(analysis?.reportMdSha256), 24)], + ]), + "", + "Turn timing:", + webObserveTable(["ROUNDS", "TURNS", "ROWS", "NON_MONO", "ELAPSED_DEC", "RECENT_JUMP", "MAX_RECENT_STEP"], [[ + webObserveText(sampleMetrics?.rounds), + webObserveText(sampleMetrics?.turnColumns), + webObserveText(sampleMetrics?.turnTimingRows), + webObserveText(sampleMetrics?.turnTimingNonMonotonicCount), + webObserveText(sampleMetrics?.turnTimingTotalElapsedDecreaseCount), + webObserveText(sampleMetrics?.turnTimingRecentUpdateJumpCount ?? sampleMetrics?.turnTimingRecentUpdateSawtoothJumpCount), + webObserveText(sampleMetrics?.turnTimingRecentUpdateMaxIncreaseSeconds), + ]]), + "", + "Runtime alerts:", + webObserveTable(["HTTP_ERR", "REQUEST_FAILED", "DOM_DIAG", "CONSOLE_ALERT", "PROMPT_SEGMENTS", "PROMPT_NETWORK"], [[ + webObserveText(runtimeAlerts?.httpErrorCount), + webObserveText(runtimeAlerts?.requestFailedCount), + webObserveText(runtimeAlerts?.domDiagnosticSampleCount), + webObserveText(runtimeAlerts?.consoleAlertCount), + webObserveText(sampleMetrics?.promptSegments), + webObserveText(promptNetwork?.promptSegments), + ]]), + "", + "Slow API (>5s):", + webObserveTable(["PATH", "SAMPLES", "P95", "MAX", "OVER5S"], slowApis.length > 0 ? slowApis.map((item) => [ + webObserveShort(webObserveText(item.path ?? item.route), 52), + webObserveText(item.sampleCount), + webObserveText(item.p95Ms ?? item.p95), + webObserveText(item.maxMs ?? item.max), + webObserveText(item.overFiveSecondCount), + ]) : [["-", "-", "-", "-", "-"]]), + "", + "DOM diagnostics:", + webObserveTable(["COUNT", "FIRST", "LAST", "TEXT"], domDiagnostics.length > 0 ? domDiagnostics.map((item) => [ + webObserveText(item.count), + webObserveShort(webObserveText(item.firstAt), 24), + webObserveShort(webObserveText(item.lastAt), 24), + webObserveShort(webObserveText(item.text), 96), + ]) : [["-", "-", "-", "-"]]), + "", + "Findings:", + webObserveTable(["KIND", "SEVERITY", "COUNT", "SUMMARY"], findings.length > 0 ? findings.map((item) => [ + webObserveShort(webObserveText(item.kind ?? item.code), 32), + webObserveText(item.severity ?? item.level), + webObserveText(item.count ?? item.sampleCount), + webObserveShort(webObserveText(item.summary ?? item.message), 96), + ]) : [["-", "-", "-", "-"]]), + ]; + if (pagePerformance !== null && Object.keys(pagePerformance).length > 0) { + lines.push("", "Page performance:", ` ${webObserveShort(JSON.stringify(pagePerformance), 180)}`); + } + lines.push( + "", + "Next:", + ` cat ${webObserveText(analysis?.reportMdPath)}`, + ` cat ${webObserveText(analysis?.reportJsonPath)}`, + "", + "Disclosure:", + " default view is a bounded report summary; use report.md/report.json paths for full offline analysis.", + ); + return lines.join("\n"); +} + function nodeWebObserveResolveStateDirShell(options: Pick): string { if (options.stateDir !== null) { return [