diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 56d31ba1..8e8a151f 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -1075,7 +1075,7 @@ const report = { 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, 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) { 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 ((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?.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?.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) : []; @@ -1604,6 +1604,7 @@ function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) { httpErrorCount: httpErrors.length, requestFailedCount: requestFailed.length, domDiagnosticSampleCount: domDiagnostics.length, + domDiagnosticGroupCount: groupDomDiagnostics(domDiagnostics).length, executionErrorCount: executionErrors.length, baselineExecutionErrorCount: baselineExecutionErrors.length, consoleAlertCount: consoleAlerts.length, @@ -1617,6 +1618,7 @@ function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) { networkHttpErrorsByPath: groupNetworkAlerts(httpErrors), networkRequestFailedByPath: groupNetworkAlerts(requestFailed), domDiagnostics: domDiagnostics.slice(0, 80), + domDiagnosticsByText: groupDomDiagnostics(domDiagnostics), runtimeExecutionErrors: executionErrors.slice(0, 120), runtimeExecutionErrorsByCode: groupExecutionErrors(executionErrors), 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) { const candidates = []; const add = (source, items) => {