fix: stream web-probe observer analysis (#586)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user