diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 48fe3328..40fa8781 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -471,8 +471,33 @@ async function samplePage(reason) { const activeSessionId = activeSession ? activeSession.getAttribute("data-session-id") : null; const messageSelector = '[data-testid*="message" i], [class*="message" i], article, [role="article"]'; const traceSelector = '[data-testid*="trace" i], [class*="trace" i], [data-trace-id], [data-testid*="event" i]'; + const diagnosticSelector = '.api-error-diagnostic, .message-diagnostic, .projection-diagnostic, [data-testid*="diagnostic" i], [class*="diagnostic" i], [role="alert"], .alert, .warning, .error'; const messages = summarize(messageSelector, 20); const traceRows = summarize(traceSelector, 30); + const diagnostics = Array.from(document.querySelectorAll(diagnosticSelector)).filter(visible).slice(-40).map((element, index) => { + const rect = element.getBoundingClientRect(); + const text = textHashInput(element); + const traceMatch = text.match(/\b(?:trace_id=)?(trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu); + const httpStatusMatch = text.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu); + const idleMatch = text.match(/\bidle\s+(\d+)s\b/iu); + const diagnosticCode = httpStatusMatch ? "http-" + httpStatusMatch[1] : /turn\s*超过|无新活动/iu.test(text) ? "turn-idle-no-activity" : /Failed to fetch/iu.test(text) ? "failed-to-fetch" : "diagnostic"; + return { + index, + tag: element.tagName.toLowerCase(), + className: String(element.className || "").slice(0, 240), + testId: element.getAttribute("data-testid"), + role: element.getAttribute("role"), + compact: element.getAttribute("data-compact"), + expanded: element.getAttribute("data-expanded") || element.getAttribute("aria-expanded"), + title: element.getAttribute("title"), + diagnosticCode, + traceId: traceMatch?.[1] || null, + httpStatus: httpStatusMatch ? Number(httpStatusMatch[1]) : null, + idleSeconds: idleMatch ? Number(idleMatch[1]) : null, + text, + rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) }, + }; + }); const turns = Array.from(document.querySelectorAll('article.message-card[data-role="agent"], .message-card[data-role="agent"], article[data-role="agent"]')).filter(visible).map((element, index) => { const rect = element.getBoundingClientRect(); const text = textHashInput(element); @@ -504,6 +529,7 @@ async function samplePage(reason) { scroll: { x: Math.round(window.scrollX), y: Math.round(window.scrollY), height: Math.round(document.documentElement.scrollHeight), width: Math.round(document.documentElement.scrollWidth) }, messages, traceRows, + diagnostics, turns, performance: performance.getEntriesByType("resource").slice(-30).map((entry) => ({ name: entry.name.split(/[?#]/u)[0].slice(0, 240), initiatorType: entry.initiatorType, startTime: Math.round(entry.startTime), duration: Math.round(entry.duration) })), }; @@ -528,8 +554,9 @@ function digestDom(dom) { if (dom && dom.error) return dom; const messages = Array.isArray(dom.messages) ? dom.messages.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || "") })) : []; const traceRows = Array.isArray(dom.traceRows) ? dom.traceRows.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || "") })) : []; + const diagnostics = Array.isArray(dom.diagnostics) ? dom.diagnostics.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 260), textBytes: Buffer.byteLength(item.text || "") })) : []; const turns = Array.isArray(dom.turns) ? dom.turns.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 200), textBytes: Buffer.byteLength(item.text || "") })) : []; - return { ...dom, messages, traceRows, turns }; + return { ...dom, messages, traceRows, diagnostics, turns }; } async function captureScreenshot(reason, imageType = "png") { @@ -678,6 +705,7 @@ const reportMdPath = path.join(analysisDir, "report.md"); const samples = await readJsonl(path.join(stateDir, "samples.jsonl")); 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")); const errors = await readJsonl(path.join(stateDir, "errors.jsonl")); const artifacts = await readJsonl(path.join(stateDir, "artifacts.jsonl")); const manifest = await readJson(path.join(stateDir, "manifest.json")); @@ -687,7 +715,8 @@ await mkdir(analysisDir, { recursive: true, mode: 0o700 }); const transitions = buildTransitions(samples); const sampleMetrics = buildSampleMetrics(samples, control); const promptNetwork = buildPromptNetworkReport(control, network); -const findings = buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork); +const runtimeAlerts = buildRuntimeAlerts(samples, control, network, consoleEvents, errors); +const findings = buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts); 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, @@ -696,11 +725,12 @@ const report = { stateDir, manifest: compactManifest(manifest), heartbeat: compactHeartbeat(heartbeat), - counts: { samples: samples.length, control: control.length, network: network.length, errors: errors.length, artifacts: artifacts.length }, + counts: { samples: samples.length, control: control.length, network: network.length, console: consoleEvents.length, errors: errors.length, artifacts: artifacts.length }, commandTimeline, transitions, sampleMetrics, promptNetwork, + runtimeAlerts, findings, artifactSummary: await artifactSummary(artifacts), safety: { offlineOnly: true, browserDriven: false, apiFetch: false, valuesRedacted: true }, @@ -708,7 +738,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, promptNetwork: promptNetwork.summary, 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, promptNetwork: promptNetwork.summary, runtimeAlerts: runtimeAlerts.summary, findings: findings.slice(0, 20), valuesRedacted: true }, null, 2)); async function readJson(file) { try { return JSON.parse(await readFile(file, "utf8")); } catch { return null; } @@ -723,7 +753,7 @@ async function readJsonl(file) { } catch { return []; } } -function buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork) { +function buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts) { const findings = []; const commandTimes = control.filter((item) => item.phase === "completed" || item.phase === "started").map((item) => Date.parse(item.ts)).filter(Number.isFinite); const routeSessions = new Set(samples.map((item) => item.routeSessionId).filter(Boolean)); @@ -753,6 +783,10 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN if (traceSeen && !traceTerminal) findings.push({ id: "trace-without-terminal", severity: "amber", summary: "trace rows were visible but no terminal status was sampled", firstTraceSample: ref(samples.find((item) => Array.isArray(item.traceRows) && item.traceRows.length > 0)) }); const promptFailures = Array.isArray(promptNetwork?.rounds) ? promptNetwork.rounds.filter((item) => item.chatPostOk === false) : []; if (promptFailures.length > 0) findings.push({ id: "prompt-chat-submit-failed", severity: "red", summary: "sendPrompt command had no successful /v1/agent/chat POST response in the sampling window", count: promptFailures.length, rounds: promptFailures.slice(0, 10) }); + 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?.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 naturalApi = network.filter((item) => item.observerInitiated === false && item.type === "response" && /\/v1\/|\/auth\//u.test(String(item.url || ""))); findings.push({ id: "natural-api-dom-lag-baseline", severity: "info", summary: "natural API responses and DOM samples are available for manual lag correlation", naturalApiResponses: naturalApi.length, sampleCount: samples.length }); if (errors.length > 0) findings.push({ id: "browser-console-or-page-errors", severity: "amber", summary: "pageerror/runner errors were captured", count: errors.length, first: errors.slice(0, 5) }); @@ -848,6 +882,172 @@ function buildPromptNetworkReport(control, network) { }; } +function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) { + const promptTimes = control + .filter((item) => item.type === "sendPrompt" && item.phase === "completed") + .map((item) => Date.parse(item.ts)) + .filter(Number.isFinite) + .sort((a, b) => a - b); + const naturalNetwork = network.filter((item) => item?.observerInitiated !== true); + const httpErrors = naturalNetwork + .filter((item) => item?.type === "response" && Number(item.status) >= 400) + .map((item) => networkAlertEvent(item, promptTimes)); + const requestFailed = naturalNetwork + .filter((item) => item?.type === "requestfailed") + .map((item) => networkAlertEvent(item, promptTimes)); + const domDiagnostics = []; + for (const sample of samples) { + const tsMs = Date.parse(sample?.ts); + const promptIndex = Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0; + if (Array.isArray(sample?.diagnostics)) { + for (const diagnostic of sample.diagnostics.slice(0, 12)) { + const text = diagnostic?.textPreview || diagnostic?.text || ""; + if (!String(text).trim()) continue; + domDiagnostics.push({ + seq: sample.seq ?? null, + ts: sample.ts ?? null, + promptIndex, + source: "diagnostic-node", + className: diagnostic.className ?? null, + diagnosticCode: diagnostic.diagnosticCode ?? null, + traceId: diagnostic.traceId ?? null, + httpStatus: diagnostic.httpStatus ?? null, + idleSeconds: diagnostic.idleSeconds ?? null, + compact: diagnostic.compact ?? null, + expanded: diagnostic.expanded ?? null, + routeSessionId: sample.routeSessionId ?? null, + activeSessionId: sample.activeSessionId ?? null, + textHash: diagnostic.textHash || sha256(text), + preview: limitText(text, 260) + }); + } + } + const texts = sampleTexts(sample).filter(isDiagnosticText); + for (const text of texts.slice(0, 4)) { + domDiagnostics.push({ + seq: sample.seq ?? null, + ts: sample.ts ?? null, + promptIndex, + source: "sample-text", + routeSessionId: sample.routeSessionId ?? null, + activeSessionId: sample.activeSessionId ?? null, + textHash: sha256(text), + preview: limitText(text, 220) + }); + } + } + const consoleAlerts = consoleEvents + .filter((item) => /error|warning|warn|assert/iu.test(String(item?.type || "")) || isDiagnosticText(item?.text)) + .map((item) => consoleAlertEvent(item, promptTimes)); + const pageErrors = errors.map((item) => ({ + ts: item.ts ?? null, + promptIndex: promptIndexForTs(promptTimes, item.ts), + type: item.type ?? null, + errorName: item.error?.name ?? item.name ?? null, + messageHash: item.error?.message ? sha256(item.error.message) : item.message ? sha256(item.message) : null, + preview: limitText(item.error?.message || item.message || item.error || "", 220) + })); + return { + summary: { + httpErrorCount: httpErrors.length, + requestFailedCount: requestFailed.length, + domDiagnosticSampleCount: domDiagnostics.length, + consoleAlertCount: consoleAlerts.length, + pageErrorCount: pageErrors.length, + networkErrorGroupCount: groupNetworkAlerts(httpErrors).length, + requestFailedGroupCount: groupNetworkAlerts(requestFailed).length, + consoleAlertGroupCount: groupConsoleAlerts(consoleAlerts).length + }, + networkHttpErrorsByPath: groupNetworkAlerts(httpErrors), + networkRequestFailedByPath: groupNetworkAlerts(requestFailed), + domDiagnostics: domDiagnostics.slice(0, 80), + consoleAlerts: consoleAlerts.slice(0, 80), + consoleAlertsByPath: groupConsoleAlerts(consoleAlerts), + pageErrors: pageErrors.slice(0, 40) + }; +} + +function consoleAlertEvent(item, promptTimes) { + const text = String(item?.text || ""); + const statusMatch = text.match(/\bstatus\s+of\s+([1-5][0-9]{2})\b/iu) || text.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu); + const location = compactLocation(item.location); + const traceMatch = (location?.urlPath || text).match(/\btrc_[A-Za-z0-9_-]+\b/u); + return { + ts: item.ts ?? null, + promptIndex: promptIndexForTs(promptTimes, item.ts), + type: item.type ?? null, + status: statusMatch ? Number(statusMatch[1]) : null, + urlPath: location?.urlPath || "-", + traceId: traceMatch?.[0] || null, + textHash: item.text ? sha256(item.text) : null, + preview: limitText(text, 220), + location + }; +} + +function groupConsoleAlerts(events) { + const groups = new Map(); + for (const event of events) { + const key = [event.type || "-", event.status ?? "-", event.urlPath || "-"].join(" "); + const group = groups.get(key) || { + type: event.type ?? null, + status: event.status ?? null, + urlPath: event.urlPath || "-", + count: 0, + firstAt: event.ts, + lastAt: event.ts, + promptIndexes: [], + traceIds: [] + }; + group.count += 1; + group.lastAt = event.ts; + if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex); + if (event.traceId && !group.traceIds.includes(event.traceId)) group.traceIds.push(event.traceId); + groups.set(key, group); + } + return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.urlPath).localeCompare(String(b.urlPath))); +} + +function networkAlertEvent(item, promptTimes) { + return { + ts: item.ts ?? null, + promptIndex: promptIndexForTs(promptTimes, item.ts), + method: String(item.method || "GET").toUpperCase(), + status: Number.isFinite(Number(item.status)) ? Number(item.status) : null, + type: item.type ?? null, + urlPath: urlPath(item.url), + urlHash: item.url ? sha256(item.url) : null, + failureKind: item.failureKind ?? null, + errorTextHash: item.errorText ? sha256(item.errorText) : null + }; +} + +function groupNetworkAlerts(events) { + const groups = new Map(); + for (const event of events) { + const key = [event.method, event.urlPath, event.status ?? "-", event.type].join(" "); + const group = groups.get(key) || { + method: event.method, + urlPath: event.urlPath, + status: event.status, + type: event.type, + count: 0, + firstAt: event.ts, + lastAt: event.ts, + promptIndexes: [] + }; + group.count += 1; + group.lastAt = event.ts; + if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex); + groups.set(key, group); + } + return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.urlPath).localeCompare(String(b.urlPath))); +} + +function isDiagnosticText(text) { + return /Failed to fetch|request failed|realtime-gap|durable projection store|turn 超过|无法连接|暂时|报警|告警|警告|失败|错误|异常|超时|timeout|error|failed|warning|502|503|504|provider-unavailable|projection-resume/iu.test(String(text || "")); +} + function buildSampleMetrics(samples, control) { const promptCommands = control .filter((item) => item.type === "sendPrompt" && item.phase === "completed") @@ -1082,7 +1282,7 @@ function buildRoundMetricSummaries(timeline, promptCommands) { function sampleTexts(sample) { const rows = []; - for (const group of [sample?.messages, sample?.traceRows]) { + for (const group of [sample?.messages, sample?.traceRows, sample?.diagnostics]) { if (!Array.isArray(group)) continue; for (const item of group) { const text = String(item?.textPreview || ""); @@ -1136,6 +1336,11 @@ function latestPromptIndex(promptTimes, tsMs) { return index; } +function promptIndexForTs(promptTimes, ts) { + const tsMs = Date.parse(ts); + return Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0; +} + function buildTransitions(samples) { const rows = []; let last = null; @@ -1166,7 +1371,8 @@ function detectFinalFlicker(samples) { function digestSample(sample) { const messages = Array.isArray(sample.messages) ? sample.messages.map((item) => item.textHash || item.textPreview || "").join("|") : ""; const trace = Array.isArray(sample.traceRows) ? sample.traceRows.map((item) => (item.status || "") + ":" + (item.textHash || item.textPreview || "")).join("|") : ""; - return sha256((sample.routeSessionId || "") + "|" + (sample.activeSessionId || "") + "|" + messages + "|" + trace); + const diagnostics = Array.isArray(sample.diagnostics) ? sample.diagnostics.map((item) => (item.className || "") + ":" + (item.textHash || item.textPreview || "")).join("|") : ""; + return sha256((sample.routeSessionId || "") + "|" + (sample.activeSessionId || "") + "|" + messages + "|" + trace + "|" + diagnostics); } function nearCommand(sample, commandTimes, windowMs) { @@ -1245,6 +1451,22 @@ function renderMarkdown(report) { const commandLines = report.commandTimeline.length === 0 ? "- 无控制命令。" : report.commandTimeline.map((item) => "- " + item.ts + " " + item.phase + " " + item.type + " " + item.commandId + " " + (item.afterUrl || "")).join("\n"); const transitionLines = report.transitions.length === 0 ? "- 无状态变化。" : report.transitions.slice(0, 80).map((item) => "- #" + item.seq + " " + item.ts + " messages=" + item.messageCount + " traceRows=" + item.traceRowCount + " route=" + (item.routeSessionId || "-") + " active=" + (item.activeSessionId || "-")).join("\n"); const metricSummary = report.sampleMetrics?.summary || {}; + const alertSummary = report.runtimeAlerts?.summary || {}; + const httpAlertLines = Array.isArray(report.runtimeAlerts?.networkHttpErrorsByPath) && report.runtimeAlerts.networkHttpErrorsByPath.length > 0 + ? report.runtimeAlerts.networkHttpErrorsByPath.slice(0, 40).map((item) => "- HTTP " + (item.status ?? "-") + " " + item.method + " " + item.urlPath + " count=" + item.count + " prompts=" + (item.promptIndexes?.join(",") || "-") + " first=" + (item.firstAt || "-") + " last=" + (item.lastAt || "-")).join("\n") + : "- 无 HTTP 错误。"; + const requestFailedLines = Array.isArray(report.runtimeAlerts?.networkRequestFailedByPath) && report.runtimeAlerts.networkRequestFailedByPath.length > 0 + ? report.runtimeAlerts.networkRequestFailedByPath.slice(0, 40).map((item) => "- requestfailed " + item.method + " " + item.urlPath + " count=" + item.count + " prompts=" + (item.promptIndexes?.join(",") || "-") + " first=" + (item.firstAt || "-") + " last=" + (item.lastAt || "-")).join("\n") + : "- 无 requestfailed。"; + const domDiagnosticLines = Array.isArray(report.runtimeAlerts?.domDiagnostics) && report.runtimeAlerts.domDiagnostics.length > 0 + ? report.runtimeAlerts.domDiagnostics.slice(0, 40).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " source=" + (item.source || "-") + " code=" + (item.diagnosticCode || "-") + " traceId=" + (item.traceId || "-") + " http=" + (item.httpStatus ?? "-") + " idle=" + (item.idleSeconds ?? "-") + " textHash=" + (item.textHash || "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n") + : "- 无 DOM 诊断文本。"; + const consoleAlertLines = Array.isArray(report.runtimeAlerts?.consoleAlerts) && report.runtimeAlerts.consoleAlerts.length > 0 + ? report.runtimeAlerts.consoleAlerts.slice(0, 40).map((item) => "- " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " type=" + (item.type || "-") + " status=" + (item.status ?? "-") + " path=" + (item.urlPath || "-") + " traceId=" + (item.traceId || "-") + " textHash=" + (item.textHash || "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n") + : "- 无 console warning/error。"; + const consoleAlertGroupLines = Array.isArray(report.runtimeAlerts?.consoleAlertsByPath) && report.runtimeAlerts.consoleAlertsByPath.length > 0 + ? report.runtimeAlerts.consoleAlertsByPath.slice(0, 40).map((item) => "- console " + (item.type || "-") + " status=" + (item.status ?? "-") + " path=" + (item.urlPath || "-") + " count=" + item.count + " prompts=" + (item.promptIndexes?.join(",") || "-") + " traces=" + (item.traceIds?.slice(0, 6).join(",") || "-")).join("\n") + : "- 无 console 分组。"; const promptNetworkLines = Array.isArray(report.promptNetwork?.rounds) && report.promptNetwork.rounds.length > 0 ? report.promptNetwork.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " chatPostOk=" + String(item.chatPostOk) + " failure=" + (item.failureKind || "-") + " statuses=" + (Array.isArray(item.responseStatuses) && item.responseStatuses.length > 0 ? item.responseStatuses.join(",") : "-") + " firstChat=" + (item.firstChatEventAt || "-") + " lastChat=" + (item.lastChatEventAt || "-")).join("\n") : "- 无 prompt 网络记录。"; @@ -1261,6 +1483,7 @@ function renderMarkdown(report) { + "- samples: " + report.counts.samples + "\n" + "- control: " + report.counts.control + "\n" + "- network: " + report.counts.network + "\n" + + "- console: " + (report.counts.console ?? 0) + "\n" + "- errors: " + report.counts.errors + "\n\n" + "## Findings\n\n" + findingLines + "\n\n" + "## Sample metrics\n\n" @@ -1271,6 +1494,17 @@ function renderMarkdown(report) { + "- promptSegments: " + (metricSummary.promptSegments ?? 0) + "\n\n" + "### Rounds\n\n" + roundLines + "\n\n" + "### Prompt network\n\n" + promptNetworkLines + "\n\n" + + "### Runtime alerts\n\n" + + "- httpErrorCount: " + (alertSummary.httpErrorCount ?? 0) + "\n" + + "- requestFailedCount: " + (alertSummary.requestFailedCount ?? 0) + "\n" + + "- domDiagnosticSampleCount: " + (alertSummary.domDiagnosticSampleCount ?? 0) + "\n" + + "- consoleAlertCount: " + (alertSummary.consoleAlertCount ?? 0) + "\n" + + "- pageErrorCount: " + (alertSummary.pageErrorCount ?? 0) + "\n\n" + + "#### HTTP errors\n\n" + httpAlertLines + "\n\n" + + "#### Request failed\n\n" + requestFailedLines + "\n\n" + + "#### DOM diagnostics\n\n" + domDiagnosticLines + "\n\n" + + "#### Console alerts\n\n" + consoleAlertLines + "\n\n" + + "#### Console alert groups\n\n" + consoleAlertGroupLines + "\n\n" + "### Turn timing table\n\n" + turnTimingTable + "\n\n" + "### Aggregate timeline\n\n" @@ -1296,5 +1530,16 @@ function urlPath(value) { return "-"; } } + +function compactLocation(value) { + if (!value || typeof value !== "object") return null; + return { urlPath: urlPath(value.url), lineNumber: value.lineNumber ?? null, columnNumber: value.columnNumber ?? null }; +} + +function limitText(value, limit) { + const text = String(value ?? ""); + if (text.length <= limit) return text; + return text.slice(0, Math.max(0, limit - 1)) + "…"; +} `; }