fix: stream web-probe observer analysis (#586)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-21 17:02:56 +08:00
committed by GitHub
parent c93375a1ea
commit c9f7f28242
@@ -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) {