diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index fea29981..1be28a6e 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -1028,14 +1028,17 @@ function sleep(ms) { export function nodeWebObserveAnalyzerSource(): string { return String.raw`#!/usr/bin/env node import { createHash } from "node:crypto"; +import { createReadStream } from "node:fs"; import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises"; import path from "node:path"; +import { createInterface } from "node:readline"; const stateDir = path.resolve(process.env.UNIDESK_WEB_OBSERVE_STATE_DIR || process.argv[2] || ".state/web-observe/manual"); const analysisDir = path.join(stateDir, "analysis"); const reportJsonPath = path.join(analysisDir, "report.json"); const reportMdPath = path.join(analysisDir, "report.md"); -const samples = await readJsonl(path.join(stateDir, "samples.jsonl")); +const jsonlReadIssues = []; +const samples = await readJsonl(path.join(stateDir, "samples.jsonl"), { compact: compactSampleForAnalysis }); const control = await readJsonl(path.join(stateDir, "control.jsonl")); const network = await readJsonl(path.join(stateDir, "network.jsonl")); const consoleEvents = await readJsonl(path.join(stateDir, "console.jsonl")); @@ -1052,6 +1055,7 @@ const pagePerformance = buildPagePerformanceReport(samples, manifest); 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 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, @@ -1069,25 +1073,129 @@ const report = { promptNetwork, runtimeAlerts, findings, + readIssues: jsonlReadIssues, artifactSummary: await artifactSummary(artifacts), safety: { offlineOnly: true, browserDriven: false, apiFetch: false, valuesRedacted: true }, }; await writeFile(reportJsonPath, JSON.stringify(report, null, 2) + "\n", { mode: 0o600 }); await writeFile(reportMdPath, renderMarkdown(report), { mode: 0o600 }); const [jsonMeta, mdMeta] = await Promise.all([fileMeta(reportJsonPath), fileMeta(reportMdPath)]); -console.log(JSON.stringify({ ok: true, command: "web-probe-observe analyze", stateDir, reportJsonPath, reportJsonSha256: jsonMeta.sha256, reportMdPath, reportMdSha256: mdMeta.sha256, counts: report.counts, sampleMetrics: sampleMetrics.summary, pageProvenance: pageProvenance.summary, pagePerformance: pagePerformance.summary, promptNetwork: promptNetwork.summary, runtimeAlerts: runtimeAlerts.summary, domDiagnosticGroups: runtimeAlerts.domDiagnosticsByText.slice(0, 20), turnTimingRecentUpdateSawtoothJumps: sampleMetrics.turnTimingRecentUpdateSawtoothJumps.slice(0, 20), turnTimingRecentUpdateLargestSteps: sampleMetrics.turnTimingRecentUpdateLargestSteps.slice(0, 20), pagePerformanceSlowApi: pagePerformance.sameOriginApiByPath.filter((item) => item.overFiveSecondCount > 0).slice(0, 20), pagePerformanceLongLivedStreams: pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream).slice(0, 20), findings: findings.slice(0, 20), valuesRedacted: true }, null, 2)); +console.log(JSON.stringify({ ok: true, command: "web-probe-observe analyze", stateDir, reportJsonPath, reportJsonSha256: jsonMeta.sha256, reportMdPath, reportMdSha256: mdMeta.sha256, counts: report.counts, sampleMetrics: sampleMetrics.summary, pageProvenance: pageProvenance.summary, pagePerformance: pagePerformance.summary, promptNetwork: promptNetwork.summary, runtimeAlerts: runtimeAlerts.summary, readIssues: jsonlReadIssues.slice(0, 20), domDiagnosticGroups: runtimeAlerts.domDiagnosticsByText.slice(0, 20), turnTimingRecentUpdateSawtoothJumps: sampleMetrics.turnTimingRecentUpdateSawtoothJumps.slice(0, 20), turnTimingRecentUpdateLargestSteps: sampleMetrics.turnTimingRecentUpdateLargestSteps.slice(0, 20), pagePerformanceSlowApi: pagePerformance.sameOriginApiByPath.filter((item) => item.overFiveSecondCount > 0).slice(0, 20), pagePerformanceLongLivedStreams: pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream).slice(0, 20), findings: findings.slice(0, 20), valuesRedacted: true }, null, 2)); async function readJson(file) { try { return JSON.parse(await readFile(file, "utf8")); } catch { return null; } } -async function readJsonl(file) { +async function readJsonl(file, options = {}) { + const rows = []; try { - const text = await readFile(file, "utf8"); - return text.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map((line) => { - try { return JSON.parse(line); } catch { return { parseError: true, rawHash: sha256(line) }; } - }); - } catch { return []; } + const input = createReadStream(file, { encoding: "utf8" }); + const lines = createInterface({ input, crlfDelay: Infinity }); + let lineNo = 0; + for await (const rawLine of lines) { + lineNo += 1; + const line = String(rawLine || "").trim(); + if (!line) continue; + try { + const parsed = JSON.parse(line); + rows.push(typeof options.compact === "function" ? options.compact(parsed) : parsed); + } catch (error) { + const item = { parseError: true, lineNo, rawHash: sha256(line), errorMessage: limitText(error && error.message ? error.message : String(error), 240) }; + rows.push(item); + if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "parse-error", lineNo, rawHash: item.rawHash, errorMessage: item.errorMessage }); + } + } + return rows; + } catch (error) { + if (error && error.code === "ENOENT") return []; + if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "read-error", code: error && error.code ? String(error.code) : null, errorMessage: limitText(error && error.message ? error.message : String(error), 240) }); + return []; + } +} + +function compactSampleForAnalysis(sample) { + if (!sample || typeof sample !== "object") return sample; + return { + seq: sample.seq ?? null, + ts: sample.ts ?? null, + reason: sample.reason ?? null, + pageId: sample.pageId ?? null, + commandId: sample.commandId ?? null, + observerInitiated: sample.observerInitiated ?? null, + url: sample.url ?? null, + path: sample.path ?? null, + routeSessionId: sample.routeSessionId ?? null, + activeSessionId: sample.activeSessionId ?? null, + messages: compactDomItems(sample.messages), + traceRows: compactDomItems(sample.traceRows), + turns: compactDomItems(sample.turns), + diagnostics: compactDomItems(sample.diagnostics), + pageProvenance: compactSamplePageProvenance(sample.pageProvenance), + performance: compactPerformanceItems(sample.performance) + }; +} + +function compactDomItems(items) { + if (!Array.isArray(items)) return []; + return items.map(compactDomItem); +} + +function compactDomItem(item) { + if (!item || typeof item !== "object") return item; + const rawText = String(item.text ?? item.textPreview ?? ""); + const preview = String(item.textPreview ?? limitText(rawText, 240)); + return { + index: item.index ?? null, + tag: item.tag ?? null, + testId: item.testId ?? null, + role: item.role ?? null, + status: item.status ?? null, + sessionId: item.sessionId ?? null, + messageId: item.messageId ?? null, + traceId: item.traceId ?? null, + turnId: item.turnId ?? null, + durationText: item.durationText ?? null, + activityText: item.activityText ?? null, + className: item.className ?? null, + diagnosticCode: item.diagnosticCode ?? null, + source: item.source ?? null, + sources: Array.isArray(item.sources) ? item.sources.slice(0, 8) : undefined, + text: limitText(rawText, 4000), + textPreview: limitText(preview, 600), + textHash: item.textHash ?? sha256(rawText), + textBytes: item.textBytes ?? Buffer.byteLength(rawText) + }; +} + +function compactPerformanceItems(items) { + if (!Array.isArray(items)) return []; + return items.map((item) => ({ + name: item?.name ?? null, + initiatorType: item?.initiatorType ?? null, + startTime: item?.startTime ?? null, + duration: item?.duration ?? null + })); +} + +function compactSamplePageProvenance(value) { + if (!value || typeof value !== "object") return null; + return { + pageLoadSeq: value.pageLoadSeq ?? null, + reason: value.reason ?? null, + observedAt: value.observedAt ?? null, + urlPath: value.urlPath ?? null, + documentReadyState: value.documentReadyState ?? null, + timeOrigin: value.timeOrigin ?? null, + httpStatus: value.httpStatus ?? null, + assetFingerprint: value.assetFingerprint ?? null, + scriptCount: value.scriptCount ?? null, + stylesheetCount: value.stylesheetCount ?? null, + metaCount: value.metaCount ?? null, + scripts: Array.isArray(value.scripts) ? value.scripts.slice(0, 20) : [], + stylesheets: Array.isArray(value.stylesheets) ? value.stylesheets.slice(0, 20) : [], + error: value.error ?? null, + valuesRedacted: true + }; } function buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance) {