fix: web-probe 输出 DOM 诊断分组 (#584)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -1075,7 +1075,7 @@ const report = {
|
|||||||
await writeFile(reportJsonPath, JSON.stringify(report, null, 2) + "\n", { mode: 0o600 });
|
await writeFile(reportJsonPath, JSON.stringify(report, null, 2) + "\n", { mode: 0o600 });
|
||||||
await writeFile(reportMdPath, renderMarkdown(report), { mode: 0o600 });
|
await writeFile(reportMdPath, renderMarkdown(report), { mode: 0o600 });
|
||||||
const [jsonMeta, mdMeta] = await Promise.all([fileMeta(reportJsonPath), fileMeta(reportMdPath)]);
|
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, 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, 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) {
|
async function readJson(file) {
|
||||||
try { return JSON.parse(await readFile(file, "utf8")); } catch { return null; }
|
try { return JSON.parse(await readFile(file, "utf8")); } catch { return null; }
|
||||||
@@ -1129,7 +1129,7 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN
|
|||||||
if (recentUpdateSawtoothJumps.length > 0) findings.push({ id: "turn-timing-recent-update-sawtooth-jump", severity: "amber", summary: "最近更新 value jumped faster than sample interval; expected sawtooth increase-or-reset", count: recentUpdateSawtoothJumps.length, samples: recentUpdateSawtoothJumps.slice(0, 20) });
|
if (recentUpdateSawtoothJumps.length > 0) findings.push({ id: "turn-timing-recent-update-sawtooth-jump", severity: "amber", summary: "最近更新 value jumped faster than sample interval; expected sawtooth increase-or-reset", count: recentUpdateSawtoothJumps.length, samples: recentUpdateSawtoothJumps.slice(0, 20) });
|
||||||
if ((runtimeAlerts?.summary?.httpErrorCount ?? 0) > 0) findings.push({ id: "runtime-http-errors", severity: "amber", summary: "natural page requests returned HTTP error status during observation", count: runtimeAlerts.summary.httpErrorCount, groups: runtimeAlerts.networkHttpErrorsByPath.slice(0, 12) });
|
if ((runtimeAlerts?.summary?.httpErrorCount ?? 0) > 0) findings.push({ id: "runtime-http-errors", severity: "amber", summary: "natural page requests returned HTTP error status during observation", count: runtimeAlerts.summary.httpErrorCount, groups: runtimeAlerts.networkHttpErrorsByPath.slice(0, 12) });
|
||||||
if ((runtimeAlerts?.summary?.requestFailedCount ?? 0) > 0) findings.push({ id: "runtime-requestfailed", severity: "amber", summary: "browser requestfailed events were captured during observation", count: runtimeAlerts.summary.requestFailedCount, groups: runtimeAlerts.networkRequestFailedByPath.slice(0, 12) });
|
if ((runtimeAlerts?.summary?.requestFailedCount ?? 0) > 0) findings.push({ id: "runtime-requestfailed", severity: "amber", summary: "browser requestfailed events were captured during observation", count: runtimeAlerts.summary.requestFailedCount, groups: runtimeAlerts.networkRequestFailedByPath.slice(0, 12) });
|
||||||
if ((runtimeAlerts?.summary?.domDiagnosticSampleCount ?? 0) > 0) findings.push({ id: "runtime-dom-diagnostics", severity: "amber", summary: "diagnostic/error/warning-like text was visible in sampled DOM", count: runtimeAlerts.summary.domDiagnosticSampleCount, samples: runtimeAlerts.domDiagnostics.slice(0, 12) });
|
if ((runtimeAlerts?.summary?.domDiagnosticSampleCount ?? 0) > 0) findings.push({ id: "runtime-dom-diagnostics", severity: "amber", summary: "diagnostic/error/warning-like text was visible in sampled DOM", count: runtimeAlerts.summary.domDiagnosticSampleCount, groupCount: runtimeAlerts.summary.domDiagnosticGroupCount ?? 0, groups: runtimeAlerts.domDiagnosticsByText.slice(0, 12), samples: runtimeAlerts.domDiagnostics.slice(0, 12) });
|
||||||
if ((runtimeAlerts?.summary?.executionErrorCount ?? 0) > 0) findings.push({ id: "runtime-execution-errors", severity: "red", summary: "Workbench rendered execution failure/error rows during observation", count: runtimeAlerts.summary.executionErrorCount, groups: runtimeAlerts.runtimeExecutionErrorsByCode.slice(0, 12) });
|
if ((runtimeAlerts?.summary?.executionErrorCount ?? 0) > 0) findings.push({ id: "runtime-execution-errors", severity: "red", summary: "Workbench rendered execution failure/error rows during observation", count: runtimeAlerts.summary.executionErrorCount, groups: runtimeAlerts.runtimeExecutionErrorsByCode.slice(0, 12) });
|
||||||
if ((runtimeAlerts?.summary?.consoleAlertCount ?? 0) > 0) findings.push({ id: "runtime-console-alerts", severity: "amber", summary: "browser console warning/error entries were captured during observation", count: runtimeAlerts.summary.consoleAlertCount, groups: runtimeAlerts.consoleAlertsByPath.slice(0, 12) });
|
if ((runtimeAlerts?.summary?.consoleAlertCount ?? 0) > 0) findings.push({ id: "runtime-console-alerts", severity: "amber", summary: "browser console warning/error entries were captured during observation", count: runtimeAlerts.summary.consoleAlertCount, groups: runtimeAlerts.consoleAlertsByPath.slice(0, 12) });
|
||||||
const slowApi = Array.isArray(pagePerformance?.sameOriginApiByPath) ? pagePerformance.sameOriginApiByPath.filter((item) => item.overFiveSecondCount > 0) : [];
|
const slowApi = Array.isArray(pagePerformance?.sameOriginApiByPath) ? pagePerformance.sameOriginApiByPath.filter((item) => item.overFiveSecondCount > 0) : [];
|
||||||
@@ -1604,6 +1604,7 @@ function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) {
|
|||||||
httpErrorCount: httpErrors.length,
|
httpErrorCount: httpErrors.length,
|
||||||
requestFailedCount: requestFailed.length,
|
requestFailedCount: requestFailed.length,
|
||||||
domDiagnosticSampleCount: domDiagnostics.length,
|
domDiagnosticSampleCount: domDiagnostics.length,
|
||||||
|
domDiagnosticGroupCount: groupDomDiagnostics(domDiagnostics).length,
|
||||||
executionErrorCount: executionErrors.length,
|
executionErrorCount: executionErrors.length,
|
||||||
baselineExecutionErrorCount: baselineExecutionErrors.length,
|
baselineExecutionErrorCount: baselineExecutionErrors.length,
|
||||||
consoleAlertCount: consoleAlerts.length,
|
consoleAlertCount: consoleAlerts.length,
|
||||||
@@ -1617,6 +1618,7 @@ function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) {
|
|||||||
networkHttpErrorsByPath: groupNetworkAlerts(httpErrors),
|
networkHttpErrorsByPath: groupNetworkAlerts(httpErrors),
|
||||||
networkRequestFailedByPath: groupNetworkAlerts(requestFailed),
|
networkRequestFailedByPath: groupNetworkAlerts(requestFailed),
|
||||||
domDiagnostics: domDiagnostics.slice(0, 80),
|
domDiagnostics: domDiagnostics.slice(0, 80),
|
||||||
|
domDiagnosticsByText: groupDomDiagnostics(domDiagnostics),
|
||||||
runtimeExecutionErrors: executionErrors.slice(0, 120),
|
runtimeExecutionErrors: executionErrors.slice(0, 120),
|
||||||
runtimeExecutionErrorsByCode: groupExecutionErrors(executionErrors),
|
runtimeExecutionErrorsByCode: groupExecutionErrors(executionErrors),
|
||||||
baselineRuntimeExecutionErrors: baselineExecutionErrors.slice(0, 80),
|
baselineRuntimeExecutionErrors: baselineExecutionErrors.slice(0, 80),
|
||||||
@@ -1627,6 +1629,86 @@ function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function groupDomDiagnostics(events) {
|
||||||
|
const groups = new Map();
|
||||||
|
for (const item of events || []) {
|
||||||
|
const preview = String(item?.preview || "").trim();
|
||||||
|
const normalizedPreview = normalizeDiagnosticPreview(preview);
|
||||||
|
const key = [
|
||||||
|
item?.source || "",
|
||||||
|
item?.diagnosticCode || "",
|
||||||
|
normalizedPreview
|
||||||
|
].join("|");
|
||||||
|
const existing = groups.get(key) || {
|
||||||
|
source: item?.source || null,
|
||||||
|
diagnosticCode: item?.diagnosticCode || null,
|
||||||
|
textHash: item?.textHash || null,
|
||||||
|
normalizedPreview,
|
||||||
|
preview,
|
||||||
|
count: 0,
|
||||||
|
firstAt: item?.ts || null,
|
||||||
|
lastAt: item?.ts || null,
|
||||||
|
promptIndexes: new Set(),
|
||||||
|
traceIds: new Set(),
|
||||||
|
sampleSeqs: []
|
||||||
|
};
|
||||||
|
existing.count += 1;
|
||||||
|
existing.firstAt = minIso(existing.firstAt, item?.ts || null);
|
||||||
|
existing.lastAt = maxIso(existing.lastAt, item?.ts || null);
|
||||||
|
if (Number.isFinite(Number(item?.promptIndex))) existing.promptIndexes.add(Number(item.promptIndex));
|
||||||
|
for (const traceId of extractDiagnosticTraceIds(item, preview)) existing.traceIds.add(traceId);
|
||||||
|
if (existing.sampleSeqs.length < 12 && item?.seq !== undefined && item?.seq !== null) existing.sampleSeqs.push(item.seq);
|
||||||
|
groups.set(key, existing);
|
||||||
|
}
|
||||||
|
return Array.from(groups.values())
|
||||||
|
.map((item) => ({
|
||||||
|
source: item.source,
|
||||||
|
diagnosticCode: item.diagnosticCode,
|
||||||
|
textHash: item.textHash,
|
||||||
|
normalizedPreview: item.normalizedPreview,
|
||||||
|
preview: item.preview,
|
||||||
|
count: item.count,
|
||||||
|
firstAt: item.firstAt,
|
||||||
|
lastAt: item.lastAt,
|
||||||
|
promptIndexes: Array.from(item.promptIndexes).sort((a, b) => a - b),
|
||||||
|
traceIds: Array.from(item.traceIds).sort(),
|
||||||
|
sampleSeqs: item.sampleSeqs
|
||||||
|
}))
|
||||||
|
.sort((a, b) => (b.count - a.count) || String(a.firstAt || "").localeCompare(String(b.firstAt || "")));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDiagnosticPreview(text) {
|
||||||
|
return String(text || "")
|
||||||
|
.replace(/trace_id=[A-Za-z0-9_-]+/gu, "trace_id=:traceId")
|
||||||
|
.replace(/\btrc_[A-Za-z0-9_-]+\b/gu, "trc_:traceId")
|
||||||
|
.replace(/\bses_[A-Za-z0-9_-]+\b/gu, "ses_:sessionId")
|
||||||
|
.replace(/\brun_[A-Za-z0-9_-]+\b/gu, "run_:runId")
|
||||||
|
.replace(/\bcmd_[A-Za-z0-9_-]+\b/gu, "cmd_:commandId")
|
||||||
|
.replace(/\s+/gu, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDiagnosticTraceIds(item, preview) {
|
||||||
|
const ids = new Set();
|
||||||
|
if (item?.traceId) ids.add(String(item.traceId));
|
||||||
|
const text = String(preview || "");
|
||||||
|
for (const match of text.matchAll(/\btrc_[A-Za-z0-9_-]+\b/gu)) ids.add(match[0]);
|
||||||
|
for (const match of text.matchAll(/trace_id=([A-Za-z0-9_-]+)/gu)) ids.add(match[1]);
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
function minIso(a, b) {
|
||||||
|
if (!a) return b || null;
|
||||||
|
if (!b) return a || null;
|
||||||
|
return Date.parse(a) <= Date.parse(b) ? a : b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maxIso(a, b) {
|
||||||
|
if (!a) return b || null;
|
||||||
|
if (!b) return a || null;
|
||||||
|
return Date.parse(a) >= Date.parse(b) ? a : b;
|
||||||
|
}
|
||||||
|
|
||||||
function sampleExecutionErrorCandidates(sample) {
|
function sampleExecutionErrorCandidates(sample) {
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
const add = (source, items) => {
|
const add = (source, items) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user