Merge pull request #1091 from pikasTech/issue1087-monitor-visibility
feat(monitor): surface Workbench sentinel root causes
This commit is contained in:
@@ -1084,6 +1084,24 @@ select {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.finding-root-cause {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding: 7px 8px;
|
||||
border-left: 3px solid #2563eb;
|
||||
border-radius: 4px;
|
||||
background: #f5f9ff;
|
||||
color: #1f2937;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.finding-root-cause strong {
|
||||
color: #1d4ed8;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.finding-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -523,6 +523,9 @@ function renderFindingItemCollapsed(item) {
|
||||
const latestRunId = item.latestRunId || "-";
|
||||
const hasLatestRun = latestRunId !== "-";
|
||||
const codeLabel = displayFindingCode(code);
|
||||
const rootCause = findingRootCauseText(item);
|
||||
const evidence = findingEvidenceText(item);
|
||||
const nextAction = item.nextAction || findingNextAction(code);
|
||||
return `<article class="finding-item is-collapsed" data-finding-run-id="${escapeAttr(latestRunId)}">
|
||||
<div class="finding-row" data-finding-toggle>
|
||||
<span class="finding-title">${escapeHtml(codeLabel)}${codeLabel === code ? "" : ` <small class="mono">${escapeHtml(code)}</small>`}<small> · ${formatNumber(item.count ?? 0)} 次</small></span>
|
||||
@@ -536,7 +539,9 @@ function renderFindingItemCollapsed(item) {
|
||||
</div>
|
||||
<div class="finding-meta">次数=${escapeHtml(String(item.count ?? 0))} · 运行=${escapeHtml(String(item.runCount ?? 0))} · 最近=${escapeHtml(item.latestAt ? formatRelative(item.latestAt) : "-")}</div>
|
||||
<div class="finding-meta mono">run=${escapeHtml(latestRunId)} report=${escapeHtml(item.latestReportJsonSha256 || "-")}</div>
|
||||
<div class="finding-meta">${escapeHtml(findingNextAction(code))}</div>
|
||||
${rootCause ? `<div class="finding-root-cause"><strong>根因</strong><span>${escapeHtml(rootCause)}</span></div>` : ""}
|
||||
${evidence ? `<div class="finding-meta mono">evidence=${escapeHtml(evidence)}</div>` : ""}
|
||||
<div class="finding-meta">${escapeHtml(nextAction)}</div>
|
||||
<button type="button" class="link-button" data-open-finding-run="${escapeAttr(latestRunId)}"${hasLatestRun ? "" : " disabled"}>打开最近运行</button>
|
||||
</div>
|
||||
</article>`;
|
||||
@@ -732,12 +737,13 @@ function detailFindings(findings) {
|
||||
}
|
||||
return `<article class="detail-block"><strong>运行发现项 · ${formatNumber(findings.length)} 条</strong>
|
||||
<div class="detail-table-frame"><table class="detail-table"><thead><tr>
|
||||
<th>严重级别</th><th>代码</th><th>次数</th><th>摘要</th><th>报告</th>
|
||||
<th>严重级别</th><th>代码</th><th>次数</th><th>摘要</th><th>根因</th><th>报告</th>
|
||||
</tr></thead><tbody>${findings.map((item) => `<tr>
|
||||
<td><span class="severity-pill ${severityClass(item.severity)}">${escapeHtml(displaySeverity(item.severity))}</span></td>
|
||||
<td class="mono">${escapeHtml(item.finding_id || item.findingId || "-")}</td>
|
||||
<td>${escapeHtml(String(item.count ?? 0))}</td>
|
||||
<td>${escapeHtml(shortText(displayFindingSummary(item.finding_id || item.findingId || "", item.summary || ""), 220))}</td>
|
||||
<td>${escapeHtml(shortText(findingRootCauseText(item) || item.nextAction || "-", 240))}</td>
|
||||
<td class="mono">${escapeHtml(shortText(item.report_json_sha256 || item.reportJsonSha256 || "-", 24))}</td>
|
||||
</tr>`).join("")}</tbody></table></div>
|
||||
</article>`;
|
||||
@@ -1252,6 +1258,10 @@ function displayFindingCode(code) {
|
||||
const normalized = String(code || "").toLowerCase();
|
||||
const labels = {
|
||||
"quick-verify-no-business-turn": "quick verify 未触达业务 turn",
|
||||
"session-rail-title-fallback-root-cause": "INV-02 会话标题 fallback 根因",
|
||||
"trace-events-page-read-404-root-cause": "INV-07 trace events 404 根因",
|
||||
"trace-events-page-read-http-error-root-cause": "trace events HTTP 错误根因",
|
||||
"trace-events-page-read-requestfailed-root-cause": "trace events 网络失败根因",
|
||||
"observer-command-failed": "观察器控制命令失败",
|
||||
"runtime-requestfailed": "运行时请求失败",
|
||||
"runtime-console-alerts": "运行时控制台告警",
|
||||
@@ -1267,6 +1277,10 @@ function displayFindingSummary(code, summary) {
|
||||
const normalized = String(code || "").toLowerCase();
|
||||
const summaries = {
|
||||
"quick-verify-no-business-turn": "quick verify 没有形成 sendPrompt、session、trace rows 或 Final Response;不能把公开 dashboard 200 当作 HWLAB 恢复证据。",
|
||||
"session-rail-title-fallback-root-cause": "会话列表可见 Session ses_* fallback 标题;根因定位到 session list projection/read model 或 rail 数据绑定缺稳定 title/preview。",
|
||||
"trace-events-page-read-404-root-cause": "trace events page read 返回 404;根因定位到 /v1/workbench/traces/:traceId/events API 分页/read-model 合约,早于 DOM 渲染。",
|
||||
"trace-events-page-read-http-error-root-cause": "trace events page read 返回 HTTP 错误;根因定位到 trace-events API 路径,不是通用前端渲染失败。",
|
||||
"trace-events-page-read-requestfailed-root-cause": "trace events page read 在浏览器网络层失败;monitor 已定位路径,但需要 OTel/API 字段确认后端根因。",
|
||||
"observer-command-failed": "observe control command 失败,需要查看 observer timeline 和 failed command 文件确认是 readiness、session API、超时还是 runner shutdown。",
|
||||
"runtime-requestfailed": "页面运行时存在请求失败,需要按路径聚合确认是 asset/provenance 噪声、public origin、auth/session 还是 Workbench API。",
|
||||
"runtime-console-alerts": "页面控制台出现告警,需要结合 run detail 的 network/console 证据判断是否影响业务 turn。",
|
||||
@@ -1284,6 +1298,10 @@ function findingNextAction(code) {
|
||||
const normalized = String(code || "").toLowerCase();
|
||||
const actions = {
|
||||
"quick-verify-no-business-turn": "下一步: 打开该 run 的 turn-summary/trace-frame,并用 CLI 对照命令确认没有业务 turn。",
|
||||
"session-rail-title-fallback-root-cause": "下一步: 查同 run 的 OTel session_list_read fallbackTitleCount/fallbackTitleRatio,修 session list projection/read model title/preview。",
|
||||
"trace-events-page-read-404-root-cause": "下一步: 查同 trace 的 OTel trace_events_read sinceSeq/range/hasMore/fullTraceLoaded,修 trace-events 分页合约或 instrumentation。",
|
||||
"trace-events-page-read-http-error-root-cause": "下一步: 按 traceId/afterProjectedSeq 对齐 OTel trace_events_read 和 HTTP route span。",
|
||||
"trace-events-page-read-requestfailed-root-cause": "下一步: 先排除 observer refresh/navigation;若非刷新噪声,补 HTTP route span 的 traceId/status/afterProjectedSeq 字段。",
|
||||
"observer-command-failed": "下一步: 查看 observe collect timeline 和 failed command 文件,定位失败阶段。",
|
||||
"runtime-requestfailed": "下一步: 按请求路径聚合失败,区分网络、auth/session、API 或静态资源问题。",
|
||||
"runtime-console-alerts": "下一步: 结合 console 样本与业务 trace 判断是否为阻塞级。",
|
||||
@@ -1294,6 +1312,23 @@ function findingNextAction(code) {
|
||||
return actions[normalized] || "下一步: 打开最近运行详情,并用 CLI 对照命令复核同一 run/observer/report。";
|
||||
}
|
||||
|
||||
function findingRootCauseText(item) {
|
||||
const rootCause = item.rootCause || "";
|
||||
const status = item.rootCauseStatus || "";
|
||||
const confidence = item.rootCauseConfidence || "";
|
||||
if (!rootCause && !status && !confidence) return "";
|
||||
return [
|
||||
rootCause ? `rootCause=${rootCause}` : "",
|
||||
status ? `status=${status}` : "",
|
||||
confidence ? `confidence=${confidence}` : "",
|
||||
].filter(Boolean).join(" · ");
|
||||
}
|
||||
|
||||
function findingEvidenceText(item) {
|
||||
const text = item.evidenceSummary || "";
|
||||
return text ? shortText(String(text), 220) : "";
|
||||
}
|
||||
|
||||
function formatSeveritySummary(counts) {
|
||||
const entries = Object.entries(counts || {}).filter(([, value]) => Number(value || 0) > 0);
|
||||
if (entries.length === 0) return "无";
|
||||
|
||||
@@ -1810,6 +1810,115 @@ function groupApiDomLagCandidates(candidates) {
|
||||
.sort((a, b) => Number(b.maxDomChangeDeltaMs ?? -1) - Number(a.maxDomChangeDeltaMs ?? -1) || b.count - a.count || String(a.path).localeCompare(String(b.path)));
|
||||
}
|
||||
|
||||
function detectTraceEventsPageReadIssues(network) {
|
||||
const events = (Array.isArray(network) ? network : [])
|
||||
.filter((item) => item?.observerInitiated !== true && (item?.type === "response" || item?.type === "requestfailed"))
|
||||
.map(compactTraceEventsPageReadEvent)
|
||||
.filter((item) => item !== null);
|
||||
const http404 = events.filter((item) => item.type === "response" && Number(item.status) === 404);
|
||||
const httpErrors = events.filter((item) => item.type === "response" && Number(item.status) >= 400 && Number(item.status) !== 404);
|
||||
const requestFailed = events.filter((item) => item.type === "requestfailed");
|
||||
return {
|
||||
events,
|
||||
http404,
|
||||
httpErrors,
|
||||
requestFailed,
|
||||
summary: traceEventsPageReadIssueSummary(events),
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function compactTraceEventsPageReadEvent(item) {
|
||||
const parsed = parseTraceEventsPageReadUrl(item?.url);
|
||||
if (!parsed.match) return null;
|
||||
const failureText = item?.failureKind ?? item?.failure ?? item?.errorText ?? null;
|
||||
return {
|
||||
ts: item?.ts ?? null,
|
||||
pageRole: item?.pageRole ?? null,
|
||||
pageId: item?.pageId ?? null,
|
||||
commandId: item?.commandId ?? null,
|
||||
method: String(item?.method || "GET").toUpperCase(),
|
||||
type: item?.type ?? null,
|
||||
status: Number.isFinite(Number(item?.status)) ? Number(item.status) : null,
|
||||
path: "/v1/workbench/traces/:traceId/events",
|
||||
rawPath: parsed.rawPath,
|
||||
traceId: parsed.traceId,
|
||||
afterProjectedSeq: parsed.afterProjectedSeq,
|
||||
sinceSeq: parsed.sinceSeq,
|
||||
limit: parsed.limit,
|
||||
tail: parsed.tail,
|
||||
queryKeys: parsed.queryKeys,
|
||||
failureKind: failureText ? limitText(String(failureText), 120) : null,
|
||||
urlHash: item?.url ? sha256(item.url) : null,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function parseTraceEventsPageReadUrl(value) {
|
||||
const fallback = {
|
||||
match: false,
|
||||
rawPath: urlPath(value),
|
||||
traceId: firstIdInText(String(value || ""), /\btrc_[A-Za-z0-9_-]+\b/u),
|
||||
afterProjectedSeq: null,
|
||||
sinceSeq: null,
|
||||
limit: null,
|
||||
tail: null,
|
||||
queryKeys: [],
|
||||
};
|
||||
try {
|
||||
const parsed = new URL(String(value || ""), "http://invalid.local/");
|
||||
const rawPath = parsed.pathname || "-";
|
||||
const match = rawPath.match(/^\/v1\/workbench\/traces\/([^/]+)\/events$/u);
|
||||
return {
|
||||
match: Boolean(match),
|
||||
rawPath,
|
||||
traceId: match ? decodeURIComponent(match[1]) : fallback.traceId,
|
||||
afterProjectedSeq: numericSearchParam(parsed.searchParams, "afterProjectedSeq"),
|
||||
sinceSeq: numericSearchParam(parsed.searchParams, "sinceSeq") ?? numericSearchParam(parsed.searchParams, "afterSeq"),
|
||||
limit: numericSearchParam(parsed.searchParams, "limit"),
|
||||
tail: numericSearchParam(parsed.searchParams, "tail"),
|
||||
queryKeys: Array.from(parsed.searchParams.keys()).sort().slice(0, 12),
|
||||
};
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function numericSearchParam(searchParams, key) {
|
||||
const raw = searchParams?.get?.(key);
|
||||
if (raw === null || raw === undefined || raw === "") return null;
|
||||
const parsed = Number(raw);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function traceEventsPageReadIssueSummary(events) {
|
||||
const items = Array.isArray(events) ? events : [];
|
||||
const statuses = uniqueSorted(items.map((item) => item.status).filter((item) => item !== null && item !== undefined).map(String));
|
||||
const traceIds = uniqueSorted(items.map((item) => item.traceId).filter(Boolean).map(String)).slice(0, 8);
|
||||
const afterProjectedSeqs = uniqueSorted(items.map((item) => item.afterProjectedSeq).filter((item) => item !== null && item !== undefined).map(String)).slice(0, 8);
|
||||
const sinceSeqs = uniqueSorted(items.map((item) => item.sinceSeq).filter((item) => item !== null && item !== undefined).map(String)).slice(0, 8);
|
||||
const failureKinds = uniqueSorted(items.map((item) => item.failureKind).filter(Boolean).map(String)).slice(0, 6);
|
||||
return {
|
||||
eventCount: items.length,
|
||||
responseErrorCount: items.filter((item) => item.type === "response" && Number(item.status) >= 400).length,
|
||||
http404Count: items.filter((item) => item.type === "response" && Number(item.status) === 404).length,
|
||||
requestFailedCount: items.filter((item) => item.type === "requestfailed").length,
|
||||
statuses,
|
||||
traceIds,
|
||||
afterProjectedSeqs,
|
||||
sinceSeqs,
|
||||
failureKinds,
|
||||
firstAt: items.reduce((value, item) => minIso(value, item.ts ?? null), null),
|
||||
lastAt: items.reduce((value, item) => maxIso(value, item.ts ?? null), null),
|
||||
rootCauseVisibility: "browser network rows identify trace-events page-read path; OTel trace_events_read should confirm backend paging fields",
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function uniqueSorted(values) {
|
||||
return Array.from(new Set((values || []).filter((item) => item !== null && item !== undefined).map(String))).sort();
|
||||
}
|
||||
|
||||
function compactApiDomLagForOutput(report) {
|
||||
if (!report || typeof report !== "object") return null;
|
||||
return {
|
||||
@@ -1954,7 +2063,70 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN
|
||||
if (Number(loadingSummary.longestContinuousSeconds ?? 0) > visibleLoadingSlowSeconds) findings.push({ id: "page-loading-visible-over-budget", severity: "red", summary: "visible 加载中 stayed on screen longer than configured YAML budget; fix real loading latency instead of revealing incomplete content early", count: loadingSummary.overBudgetSegmentCount ?? loadingSummary.overFiveSecondSegmentCount ?? 1, longestContinuousSeconds: loadingSummary.longestContinuousSeconds, budgetSeconds: visibleLoadingSlowSeconds, segments: sampleMetrics.loading.segments.slice(0, 20), owners: sampleMetrics.loading.owners.slice(0, 20) });
|
||||
if (Number(loadingSummary.maxSimultaneousCount ?? 0) > 1) findings.push({ id: "page-loading-concurrent", severity: "info", summary: "multiple 加载中 indicators were visible in the same sampled DOM point", count: loadingSummary.concurrentLoadingSampleCount ?? 0, maxSimultaneousCount: loadingSummary.maxSimultaneousCount, owners: sampleMetrics.loading.owners.slice(0, 20) });
|
||||
const sessionRailTitleSummary = sampleMetrics?.sessionRailTitles?.summary || {};
|
||||
if (Number(sessionRailTitleSummary.overThresholdSampleCount ?? sessionRailTitleSummary.majorityFallbackSampleCount ?? 0) > 0) findings.push({ id: "session-rail-title-fallback-over-threshold", severity: "red", summary: "visible session list rows exceeded configured YAML fallback-title ratio", count: sessionRailTitleSummary.overThresholdSampleCount ?? sessionRailTitleSummary.majorityFallbackSampleCount, thresholdRatio: alertThresholds.sessionRailFallbackRatio, maxFallbackRatio: sessionRailTitleSummary.maxFallbackRatio, maxFallbackTitleCount: sessionRailTitleSummary.maxFallbackTitleCount, samples: sampleMetrics.sessionRailTitles.samples.slice(0, 20), examples: sampleMetrics.sessionRailTitles.examples.slice(0, 20) });
|
||||
if (Number(sessionRailTitleSummary.overThresholdSampleCount ?? sessionRailTitleSummary.majorityFallbackSampleCount ?? 0) > 0) findings.push({
|
||||
id: "session-rail-title-fallback-root-cause",
|
||||
severity: "red",
|
||||
summary: "INV-02 root cause visible: session rail is rendering fallback Session ses_* titles, so list projection/read model or rail binding is missing stable title/preview data before DOM render",
|
||||
rootCause: "session-list-title-projection-missing-or-not-bound",
|
||||
rootCauseStatus: "confirmed-from-dom-session-rail",
|
||||
rootCauseConfidence: "high",
|
||||
nextAction: "Check OTel session_list_read fallbackTitleCount/fallbackTitleRatio for the same run; fix session list projection/read model title/firstUserMessagePreview before changing DOM fallback text.",
|
||||
count: sessionRailTitleSummary.overThresholdSampleCount ?? sessionRailTitleSummary.majorityFallbackSampleCount,
|
||||
thresholdRatio: alertThresholds.sessionRailFallbackRatio,
|
||||
maxFallbackRatio: sessionRailTitleSummary.maxFallbackRatio,
|
||||
maxFallbackTitleCount: sessionRailTitleSummary.maxFallbackTitleCount,
|
||||
evidence: {
|
||||
thresholdRatio: alertThresholds.sessionRailFallbackRatio,
|
||||
maxFallbackRatio: sessionRailTitleSummary.maxFallbackRatio,
|
||||
maxFallbackTitleCount: sessionRailTitleSummary.maxFallbackTitleCount,
|
||||
overThresholdSampleCount: sessionRailTitleSummary.overThresholdSampleCount ?? null,
|
||||
majorityFallbackSampleCount: sessionRailTitleSummary.majorityFallbackSampleCount ?? null,
|
||||
valuesRedacted: true
|
||||
},
|
||||
samples: sampleMetrics.sessionRailTitles.samples.slice(0, 20),
|
||||
examples: sampleMetrics.sessionRailTitles.examples.slice(0, 20),
|
||||
valuesRedacted: true
|
||||
});
|
||||
const traceEventsPageReadIssues = detectTraceEventsPageReadIssues(network);
|
||||
if (traceEventsPageReadIssues.http404.length > 0) findings.push({
|
||||
id: "trace-events-page-read-404-root-cause",
|
||||
severity: "red",
|
||||
summary: "INV-07 root cause visible: /v1/workbench/traces/:traceId/events returned HTTP 404 for a trace event page read, so the failure is in the trace-events API paging/read-model contract before DOM rendering",
|
||||
rootCause: "trace-events-api-page-read-returned-404",
|
||||
rootCauseStatus: "confirmed-from-browser-network",
|
||||
rootCauseConfidence: "high",
|
||||
nextAction: "Use OTel trace_events_read for the same trace to compare sinceSeq/afterProjectedSeq, returnedEvents, range, totalEvents, hasMore and fullTraceLoaded; fix backend paging contract or add missing instrumentation before changing renderer behavior.",
|
||||
count: traceEventsPageReadIssues.http404.length,
|
||||
evidence: traceEventsPageReadIssues.summary,
|
||||
samples: traceEventsPageReadIssues.http404.slice(0, 20),
|
||||
valuesRedacted: true
|
||||
});
|
||||
if (traceEventsPageReadIssues.httpErrors.length > 0) findings.push({
|
||||
id: "trace-events-page-read-http-error-root-cause",
|
||||
severity: "red",
|
||||
summary: "trace events page read returned HTTP error status; monitor can localize this to the trace-events API instead of a generic Workbench render failure",
|
||||
rootCause: "trace-events-api-page-read-http-error",
|
||||
rootCauseStatus: "confirmed-from-browser-network",
|
||||
rootCauseConfidence: "high",
|
||||
nextAction: "Drill down by traceId and afterProjectedSeq, then compare with OTel trace_events_read paging fields.",
|
||||
count: traceEventsPageReadIssues.httpErrors.length,
|
||||
evidence: traceEventsPageReadIssues.summary,
|
||||
samples: traceEventsPageReadIssues.httpErrors.slice(0, 20),
|
||||
valuesRedacted: true
|
||||
});
|
||||
if (traceEventsPageReadIssues.requestFailed.length > 0) findings.push({
|
||||
id: "trace-events-page-read-requestfailed-root-cause",
|
||||
severity: "amber",
|
||||
summary: "trace events page read failed at browser/network level; monitor localized the failure path, but HTTP status is unavailable so OTel/API instrumentation is needed to confirm the backend root cause",
|
||||
rootCause: "trace-events-api-page-read-network-failed",
|
||||
rootCauseStatus: "network-signal-needs-otel-confirmation",
|
||||
rootCauseConfidence: "medium",
|
||||
nextAction: "Check whether this happened during observer refresh/navigation; if not, query OTel by /v1/workbench/traces/:traceId/events and add route span fields when status/afterProjectedSeq are missing.",
|
||||
count: traceEventsPageReadIssues.requestFailed.length,
|
||||
evidence: traceEventsPageReadIssues.summary,
|
||||
samples: traceEventsPageReadIssues.requestFailed.slice(0, 20),
|
||||
valuesRedacted: true
|
||||
});
|
||||
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?.significantRequestFailedCount ?? runtimeAlerts?.summary?.requestFailedCount ?? 0) > 0) findings.push({ id: "runtime-requestfailed", severity: "amber", summary: "browser requestfailed events were captured during observation", count: runtimeAlerts.summary.significantRequestFailedCount ?? runtimeAlerts.summary.requestFailedCount, groups: (runtimeAlerts.networkSignificantRequestFailedByPath ?? 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, groupCount: runtimeAlerts.summary.domDiagnosticGroupCount ?? 0, groups: runtimeAlerts.domDiagnosticsByText.slice(0, 12), samples: runtimeAlerts.domDiagnostics.slice(0, 12) });
|
||||
|
||||
@@ -355,10 +355,11 @@ function renderWebObserveCollectTable(value: Record<string, unknown>): string {
|
||||
if (jsonFindings.length > 0) {
|
||||
lines.push(
|
||||
"Findings:",
|
||||
webObserveTable(["KIND", "SEVERITY", "COUNT", "SUMMARY"], jsonFindings.map((item) => [
|
||||
webObserveTable(["KIND", "SEVERITY", "COUNT", "ROOT_CAUSE", "SUMMARY"], jsonFindings.map((item) => [
|
||||
webObserveShort(webObserveText(item.kind ?? item.id ?? item.code), 48),
|
||||
item.severity ?? item.level,
|
||||
item.count ?? item.sampleCount,
|
||||
webObserveShort(webObserveText(item.rootCause ?? item.rootCauseStatus), 48),
|
||||
webObserveShort(webObserveText(item.summary ?? item.message), 96),
|
||||
])),
|
||||
"",
|
||||
@@ -550,12 +551,13 @@ function renderWebObserveProjectCollectTable(value: Record<string, unknown>, col
|
||||
]) : [["-", "-", "-", "-", "-", "-", "-"]]),
|
||||
"",
|
||||
"Findings:",
|
||||
webObserveTable(["SEVERITY", "ID", "COUNT", "SUMMARY"], findings.length > 0 ? findings.map((item) => [
|
||||
webObserveTable(["SEVERITY", "ID", "COUNT", "ROOT_CAUSE", "SUMMARY"], findings.length > 0 ? findings.map((item) => [
|
||||
webObserveShort(webObserveText(item.severity ?? item.level), 12),
|
||||
webObserveShort(webObserveText(item.id ?? item.kind ?? item.code), 48),
|
||||
item.count ?? item.sampleCount,
|
||||
webObserveShort(webObserveText(item.rootCause ?? item.rootCauseStatus), 48),
|
||||
webObserveShort(webObserveText(item.summary ?? item.message), 96),
|
||||
]) : [["-", "-", "-", "-"]]),
|
||||
]) : [["-", "-", "-", "-", "-"]]),
|
||||
"",
|
||||
"Disclosure:",
|
||||
" collect project views are rendered from existing samples/control/analysis artifacts; they do not create a new sampling source.",
|
||||
|
||||
@@ -2633,11 +2633,41 @@ function compactQuickVerifyRecordFinding(value: unknown): Record<string, unknown
|
||||
level: stringAtNullable(item, "level"),
|
||||
count: numberAtNullable(item, "count"),
|
||||
summary: boundQuickVerifyRecordText(item.summary ?? item.message, 220),
|
||||
rootCause: boundQuickVerifyRecordText(item.rootCause, 140),
|
||||
rootCauseStatus: boundQuickVerifyRecordText(item.rootCauseStatus, 90),
|
||||
rootCauseConfidence: boundQuickVerifyRecordText(item.rootCauseConfidence, 40),
|
||||
nextAction: boundQuickVerifyRecordText(item.nextAction, 240),
|
||||
evidenceSummary: stringAtNullable(item, "evidenceSummary") ?? compactQuickVerifyFindingEvidence(item.evidence),
|
||||
blocking: item.blocking === true,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function compactQuickVerifyFindingEvidence(value: unknown): string | null {
|
||||
const item = record(value);
|
||||
if (Object.keys(item).length === 0) return null;
|
||||
const keys = [
|
||||
"http404Count",
|
||||
"responseErrorCount",
|
||||
"requestFailedCount",
|
||||
"statuses",
|
||||
"afterProjectedSeqs",
|
||||
"sinceSeqs",
|
||||
"traceIds",
|
||||
"maxFallbackRatio",
|
||||
"maxFallbackTitleCount",
|
||||
"overThresholdSampleCount",
|
||||
"majorityFallbackSampleCount",
|
||||
];
|
||||
const compact: Record<string, unknown> = {};
|
||||
for (const key of keys) {
|
||||
const raw = item[key];
|
||||
if (raw === null || raw === undefined) continue;
|
||||
compact[key] = Array.isArray(raw) ? raw.slice(0, 6) : raw;
|
||||
}
|
||||
return Object.keys(compact).length === 0 ? null : boundQuickVerifyRecordText(JSON.stringify(compact), 240);
|
||||
}
|
||||
|
||||
function compactQuickVerifyRecordStep(value: unknown): Record<string, unknown> {
|
||||
const item = record(value);
|
||||
return {
|
||||
@@ -3230,7 +3260,7 @@ function readAnalysisSummaryFromWorkspace(state: SentinelCicdState, stateDir: st
|
||||
"let artifactCount=0; let screenshot=null;",
|
||||
"function walk(dir){let entries=[]; try{entries=fs.readdirSync(dir,{withFileTypes:true})}catch{return}; for(const e of entries){const p=path.join(dir,e.name); if(e.isDirectory()) walk(p); else { artifactCount++; if(/\\.png$/i.test(e.name)){const b=read(p); screenshot={path:p,sha256:sha(b),bytes:b?b.length:0}; } } }}",
|
||||
"walk(stateDir);",
|
||||
"const findings=arr(report?.findings ?? report?.archiveSummary?.redFindings).slice(0,20).map((item)=>{const v=rec(item); return {id:clip(v.id??v.kind??v.code,80),kind:clip(v.kind??v.id??v.code,80),code:clip(v.code??v.kind??v.id,80),severity:clip(v.severity??v.level,32),level:clip(v.level??v.severity,32),count:Number(v.count??v.sampleCount??1),summary:clip(v.summary??v.message,220),message:clip(v.message??v.summary,220),blocking:v.blocking===true,afterRound:v.afterRound??null,canarySessionId:clip(v.canarySessionId,80),routeSessionId:clip(v.routeSessionId,80),activeSessionId:clip(v.activeSessionId,80),consecutiveUserMessageCount:v.consecutiveUserMessageCount??null,sentinelRange:clip(v.sentinelRange,80),sampleSeq:v.sampleSeq??null,traceIds:arr(v.traceIds).slice(0,8).map((x)=>clip(x,80)),pageRole:clip(v.pageRole,32),pageId:clip(v.pageId,80),observerId:clip(v.observerId,80),stateDir:clip(v.stateDir,160),commandId:clip(v.commandId,80),valuesRedacted:true};});",
|
||||
"const findings=arr(report?.findings ?? report?.archiveSummary?.redFindings).slice(0,20).map((item)=>{const v=rec(item); return {id:clip(v.id??v.kind??v.code,80),kind:clip(v.kind??v.id??v.code,80),code:clip(v.code??v.kind??v.id,80),severity:clip(v.severity??v.level,32),level:clip(v.level??v.severity,32),count:Number(v.count??v.sampleCount??1),summary:clip(v.summary??v.message,220),message:clip(v.message??v.summary,220),rootCause:clip(v.rootCause,140),rootCauseStatus:clip(v.rootCauseStatus,90),rootCauseConfidence:clip(v.rootCauseConfidence,40),nextAction:clip(v.nextAction,240),evidenceSummary:v.evidence?clip(JSON.stringify(rec(v.evidence)),220):clip(v.evidenceSummary,220),blocking:v.blocking===true,afterRound:v.afterRound??null,canarySessionId:clip(v.canarySessionId,80),routeSessionId:clip(v.routeSessionId,80),activeSessionId:clip(v.activeSessionId,80),consecutiveUserMessageCount:v.consecutiveUserMessageCount??null,sentinelRange:clip(v.sentinelRange,80),sampleSeq:v.sampleSeq??null,traceIds:arr(v.traceIds).slice(0,8).map((x)=>clip(x,80)),pageRole:clip(v.pageRole,32),pageId:clip(v.pageId,80),observerId:clip(v.observerId,80),stateDir:clip(v.stateDir,160),commandId:clip(v.commandId,80),valuesRedacted:true};});",
|
||||
"const slow=arr(report?.pagePerformanceSlowApi ?? report?.archivePagePerformanceSlowApi).slice(0,8).map((item)=>{const v=rec(item); return {path:clip(v.path??v.route,120),sampleCount:v.sampleCount??null,p95Ms:v.p95Ms??null,maxMs:v.maxMs??null,overFiveSecondCount:v.overFiveSecondCount??null};});",
|
||||
"console.log(JSON.stringify({ok:!!report,reportOk:!!report&&report.ok!==false,stateDir,reportJsonPath:reportPath,reportJsonSha256:sha(jsonBuf),reportMdPath,reportMdSha256:sha(read(reportMdPath)),findingCount:Number(report?.findingCount??findings.length),artifactCount,screenshot,findings,counts:rec(report?.counts),analysisWindow:rec(report?.analysisWindow??report?.windows?.recent?.summary),pagePerformanceSlowApi:slow,valuesRedacted:true}));",
|
||||
"NODE",
|
||||
|
||||
@@ -650,6 +650,7 @@ function dashboardFindings(config: WebProbeSentinelServiceConfig, db: Database,
|
||||
`).all(...where.params, limit) as Record<string, unknown>[];
|
||||
const items = rows.map((row) => {
|
||||
const latestRun = latestRunForFinding(db, row);
|
||||
const latestDetail = latestRun === null ? null : storedFindingDetailForRow(db, row, stringOrNull(latestRun.id));
|
||||
return {
|
||||
code: stringOrNull(row.finding_id),
|
||||
findingId: stringOrNull(row.finding_id),
|
||||
@@ -661,6 +662,11 @@ function dashboardFindings(config: WebProbeSentinelServiceConfig, db: Database,
|
||||
latestRunId: latestRun === null ? null : stringOrNull(latestRun.id),
|
||||
latestReportJsonSha256: latestRun === null ? null : stringOrNull(latestRun.report_json_sha256),
|
||||
summary: stringOrNull(row.summary),
|
||||
rootCause: stringOrNull(latestDetail?.rootCause),
|
||||
rootCauseStatus: stringOrNull(latestDetail?.rootCauseStatus),
|
||||
rootCauseConfidence: stringOrNull(latestDetail?.rootCauseConfidence),
|
||||
nextAction: stringOrNull(latestDetail?.nextAction),
|
||||
evidenceSummary: stringOrNull(latestDetail?.evidenceSummary),
|
||||
traceability: latestRun === null ? null : runTraceability(config, latestRun),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
@@ -910,8 +916,88 @@ function readRunRow(db: Database, runId: string): Record<string, unknown> | null
|
||||
}
|
||||
|
||||
function findingsForRun(db: Database, runId: string, limit: number): readonly Record<string, unknown>[] {
|
||||
return db.query("SELECT finding_id, severity, count, summary, report_json_sha256, created_at FROM findings WHERE run_id = ? ORDER BY created_at DESC LIMIT ?")
|
||||
const rows = db.query("SELECT finding_id, severity, count, summary, report_json_sha256, created_at FROM findings WHERE run_id = ? ORDER BY created_at DESC LIMIT ?")
|
||||
.all(runId, limit) as Record<string, unknown>[];
|
||||
return rows.map((row) => enrichFindingRowWithStoredDetail(db, runId, row));
|
||||
}
|
||||
|
||||
function enrichFindingRowWithStoredDetail(db: Database, runId: string, row: Record<string, unknown>): Record<string, unknown> {
|
||||
const detail = storedFindingDetailForRow(db, row, runId);
|
||||
return {
|
||||
...row,
|
||||
code: stringOrNull(row.finding_id),
|
||||
findingId: stringOrNull(row.finding_id),
|
||||
rootCause: stringOrNull(detail?.rootCause),
|
||||
rootCauseStatus: stringOrNull(detail?.rootCauseStatus),
|
||||
rootCauseConfidence: stringOrNull(detail?.rootCauseConfidence),
|
||||
nextAction: stringOrNull(detail?.nextAction),
|
||||
evidenceSummary: stringOrNull(detail?.evidenceSummary),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function storedFindingDetailForRow(db: Database, row: Record<string, unknown>, runId: string | null): Record<string, unknown> | null {
|
||||
if (runId === null) return null;
|
||||
const stored = readMetadata(db, `run.report.${runId}`) ?? {};
|
||||
const details = arrayRecords(stored.findings);
|
||||
if (details.length === 0) return null;
|
||||
const findingId = stringOrNull(row.finding_id) ?? stringOrNull(row.findingId) ?? stringOrNull(row.code);
|
||||
const severity = stringOrNull(row.severity);
|
||||
return details.find((item) => {
|
||||
const itemId = stringOrNull(item.finding_id) ?? stringOrNull(item.findingId) ?? stringOrNull(item.id) ?? stringOrNull(item.code);
|
||||
if (itemId !== findingId) return false;
|
||||
const itemSeverity = stringOrNull(item.severity) ?? stringOrNull(item.level);
|
||||
return severity === null || itemSeverity === null || itemSeverity === severity;
|
||||
}) ?? null;
|
||||
}
|
||||
|
||||
function compactStoredFinding(value: unknown): Record<string, unknown> {
|
||||
const item = record(value);
|
||||
const findingId = stringOrNull(item.id) ?? stringOrNull(item.kind) ?? stringOrNull(item.code) ?? "finding";
|
||||
const severity = stringOrNull(item.severity) ?? stringOrNull(item.level) ?? "unknown";
|
||||
const summary = stringOrNull(item.summary) ?? stringOrNull(item.message) ?? findingId;
|
||||
return {
|
||||
id: findingId,
|
||||
finding_id: findingId,
|
||||
findingId,
|
||||
code: findingId,
|
||||
severity,
|
||||
count: numberOr(item.count, numberOr(item.sampleCount, 1)),
|
||||
summary: summary.slice(0, 500),
|
||||
rootCause: stringOrNull(item.rootCause),
|
||||
rootCauseStatus: stringOrNull(item.rootCauseStatus),
|
||||
rootCauseConfidence: stringOrNull(item.rootCauseConfidence),
|
||||
nextAction: stringOrNull(item.nextAction),
|
||||
evidenceSummary: stringOrNull(item.evidenceSummary) ?? compactFindingEvidenceSummary(item.evidence),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function compactFindingEvidenceSummary(value: unknown): string | null {
|
||||
if (value === null || value === undefined) return null;
|
||||
if (typeof value === "string") return value.slice(0, 240);
|
||||
const item = record(value);
|
||||
const keys = [
|
||||
"http404Count",
|
||||
"responseErrorCount",
|
||||
"requestFailedCount",
|
||||
"statuses",
|
||||
"afterProjectedSeqs",
|
||||
"sinceSeqs",
|
||||
"traceIds",
|
||||
"maxFallbackRatio",
|
||||
"maxFallbackTitleCount",
|
||||
"overThresholdSampleCount",
|
||||
"majorityFallbackSampleCount",
|
||||
];
|
||||
const compact: Record<string, unknown> = {};
|
||||
for (const key of keys) {
|
||||
const raw = item[key];
|
||||
if (raw === null || raw === undefined) continue;
|
||||
compact[key] = Array.isArray(raw) ? raw.slice(0, 6) : raw;
|
||||
}
|
||||
const text = Object.keys(compact).length > 0 ? JSON.stringify(compact) : JSON.stringify(item).slice(0, 240);
|
||||
return text.length > 240 ? `${text.slice(0, 239)}…` : text;
|
||||
}
|
||||
|
||||
function globalSeverityCounts(db: Database): Record<string, number> {
|
||||
@@ -1111,6 +1197,7 @@ function recordRunResult(config: WebProbeSentinelServiceConfig, db: Database, in
|
||||
reportJsonSha256,
|
||||
summary: record(input.summary),
|
||||
views: record(input.views),
|
||||
findings: findings.map(compactStoredFinding),
|
||||
publicOrigin: stringOrNull(input.publicOrigin),
|
||||
screenshot: record(input.screenshot),
|
||||
artifactCount,
|
||||
@@ -1131,8 +1218,7 @@ function reportRunView(config: WebProbeSentinelServiceConfig, db: Database, view
|
||||
const selectedRunId = stringOrNull(row.id);
|
||||
if (selectedRunId === null) return { ok: false, error: "report-run-id-missing", view, valuesRedacted: true };
|
||||
const stored = readMetadata(db, `run.report.${selectedRunId}`) ?? {};
|
||||
const findings = db.query("SELECT finding_id, severity, count, summary, report_json_sha256, created_at FROM findings WHERE run_id = ? ORDER BY created_at DESC LIMIT 50")
|
||||
.all(selectedRunId) as Record<string, unknown>[];
|
||||
const findings = findingsForRun(db, selectedRunId, 50);
|
||||
const views = record(stored.views);
|
||||
const storedView = record(views[view]);
|
||||
const renderedText = typeof storedView.renderedText === "string" ? storedView.renderedText : view === "summary" ? renderStoredSummary(row, stored, findings) : view === "findings" ? renderStoredFindings(row, findings) : null;
|
||||
@@ -1150,6 +1236,19 @@ function reportRunView(config: WebProbeSentinelServiceConfig, db: Database, view
|
||||
};
|
||||
}
|
||||
|
||||
function formatStoredFindingLine(item: Record<string, unknown>): string {
|
||||
const rootCause = stringOrNull(item.rootCause);
|
||||
const status = stringOrNull(item.rootCauseStatus);
|
||||
const nextAction = stringOrNull(item.nextAction);
|
||||
const evidence = stringOrNull(item.evidenceSummary);
|
||||
return [
|
||||
`${item.severity ?? "-"} ${item.finding_id ?? item.findingId ?? item.code ?? "-"} count=${item.count ?? "-"} ${item.summary ?? ""}`,
|
||||
rootCause === null ? null : `rootCause=${rootCause}${status === null ? "" : ` status=${status}`}`,
|
||||
evidence === null ? null : `evidence=${evidence}`,
|
||||
nextAction === null ? null : `next=${nextAction}`,
|
||||
].filter((part) => part !== null).join(" | ");
|
||||
}
|
||||
|
||||
function renderStoredSummary(row: Record<string, unknown>, stored: Record<string, unknown>, findings: readonly Record<string, unknown>[]): string {
|
||||
const summary = record(stored.summary);
|
||||
return [
|
||||
@@ -1162,7 +1261,7 @@ function renderStoredSummary(row: Record<string, unknown>, stored: Record<string
|
||||
`analysisWindow=${JSON.stringify(record(summary.analysisWindow))}`,
|
||||
"",
|
||||
"Findings",
|
||||
findings.length === 0 ? "-" : findings.slice(0, 12).map((item) => `${item.severity ?? "-"} ${item.finding_id ?? "-"} count=${item.count ?? "-"} ${item.summary ?? ""}`).join("\n"),
|
||||
findings.length === 0 ? "-" : findings.slice(0, 12).map(formatStoredFindingLine).join("\n"),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -1171,7 +1270,7 @@ function renderStoredFindings(row: Record<string, unknown>, findings: readonly R
|
||||
"Web Probe Sentinel Findings",
|
||||
"=======================================================",
|
||||
`run=${stringOrNull(row.id) ?? "-"} report=${stringOrNull(row.report_json_sha256) ?? "-"}`,
|
||||
findings.length === 0 ? "-" : findings.map((item) => `${item.severity ?? "-"} ${item.finding_id ?? "-"} count=${item.count ?? "-"} ${item.summary ?? ""}`).join("\n"),
|
||||
findings.length === 0 ? "-" : findings.map(formatStoredFindingLine).join("\n"),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
|
||||
@@ -697,12 +697,13 @@ export function renderWebObserveAnalyzeTable(value: Record<string, unknown>): st
|
||||
webObserveTable(consoleAlertGroups.length > 0 ? ["COUNT", "TYPE", "STATUS", "PATH", "LAST", "TRACES"] : ["TS", "TYPE", "STATUS", "PATH", "TRACE", "TEXT"], consoleAlertRows.length > 0 ? consoleAlertRows : [["-", "-", "-", "-", "-", "-"]]),
|
||||
"",
|
||||
"Findings:",
|
||||
webObserveTable(["KIND", "SEVERITY", "COUNT", "SUMMARY"], findings.length > 0 ? findings.map((item) => [
|
||||
webObserveShort(webObserveText(item.kind ?? item.code), 32),
|
||||
webObserveTable(["KIND", "SEVERITY", "COUNT", "ROOT_CAUSE", "SUMMARY"], findings.length > 0 ? findings.map((item) => [
|
||||
webObserveShort(webObserveText(item.kind ?? item.id ?? item.code), 32),
|
||||
webObserveText(item.severity ?? item.level),
|
||||
webObserveText(item.count ?? item.sampleCount),
|
||||
webObserveShort(webObserveText(item.rootCause ?? item.rootCauseStatus), 48),
|
||||
webObserveShort(webObserveText(item.summary ?? item.message), 96),
|
||||
]) : [["-", "-", "-", "-"]]),
|
||||
]) : [["-", "-", "-", "-", "-"]]),
|
||||
"",
|
||||
"Archive red findings:",
|
||||
webObserveTable(["KIND", "COUNT", "SUMMARY"], archiveRedFindings.length > 0 ? archiveRedFindings.map((item) => [
|
||||
|
||||
@@ -195,11 +195,17 @@ const slimFinding=(item)=>{
|
||||
if(!item||typeof item!=='object') return null;
|
||||
const samples=Array.isArray(item.samples)?item.samples.length:null;
|
||||
const groups=Array.isArray(item.groups)?item.groups.length:null;
|
||||
const evidence=item.evidence&&typeof item.evidence==='object'?slimObject(item.evidence,10):null;
|
||||
return {
|
||||
kind:short(item.kind||item.code||item.id,80),
|
||||
severity:short(item.severity||item.level,24),
|
||||
count:item.count??item.sampleCount??null,
|
||||
summary:short(item.summary||item.message,240),
|
||||
rootCause:short(item.rootCause,140),
|
||||
rootCauseStatus:short(item.rootCauseStatus,90),
|
||||
rootCauseConfidence:short(item.rootCauseConfidence,40),
|
||||
nextAction:short(item.nextAction,240),
|
||||
evidenceSummary:evidence?short(JSON.stringify(evidence),240):null,
|
||||
sampleRows:samples,
|
||||
groupRows:groups
|
||||
};
|
||||
|
||||
@@ -1774,7 +1774,8 @@ export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOption
|
||||
"const slimTurnColumn = (item) => { const v = objectOrNull(item) || {}; return { label: clip(v.label, 24), pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), pageEpoch: v.pageEpoch ?? null, promptIndex: v.promptIndex ?? null, lastPromptIndex: v.lastPromptIndex ?? null, traceId: clip(v.traceId, 48), firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, source: clip(v.source, 48) }; };",
|
||||
"const slimSlowSample = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, seq: v.seq ?? null, path: clip(v.path ?? v.rawPath, 96), initiatorType: clip(v.initiatorType, 24), durationMs: v.durationMs ?? null, requestToResponseStartMs: v.requestToResponseStartMs ?? v.streamOpenMs ?? null, responseTransferMs: v.responseTransferMs ?? null, nextHopProtocol: clip(v.nextHopProtocol, 24), timingStatus: clip(v.timingStatus, 16), serverTimingNames: Array.isArray(v.serverTimingNames) ? v.serverTimingNames.slice(0, 4).map((x) => clip(x, 32)) : [], otelTraceId: clip(v.otelTraceId, 32) }; };",
|
||||
"const slimSlowApi = (item) => { const v = objectOrNull(item) || {}; return { path: clip(v.path ?? v.route, 96), route: clip(v.route ?? v.path, 96), sampleCount: v.sampleCount ?? null, p95Ms: v.p95Ms ?? v.p95 ?? null, maxMs: v.maxMs ?? v.max ?? null, budgetMs: v.budgetMs ?? null, overBudgetCount: v.overBudgetCount ?? null, overFiveSecondCount: v.overFiveSecondCount ?? null, slowSamples: Array.isArray(v.slowSamples) ? v.slowSamples.slice(0, 3).map(slimSlowSample) : [] }; };",
|
||||
"const slimFinding = (item) => { const v = objectOrNull(item) || {}; return { kind: clip(v.kind ?? v.id ?? v.code, 48), code: clip(v.code ?? v.id ?? v.kind, 48), severity: clip(v.severity ?? v.level, 24), level: clip(v.level ?? v.severity, 24), count: v.count ?? v.sampleCount ?? null, sampleCount: v.sampleCount ?? v.count ?? null, summary: clip(v.summary ?? v.message, 180), message: clip(v.message ?? v.summary, 180) }; };",
|
||||
"const evidenceSummary = (value) => { const v = objectOrNull(value); if (!v) return null; const out = {}; for (const key of ['http404Count','responseErrorCount','requestFailedCount','statuses','afterProjectedSeqs','sinceSeqs','traceIds','maxFallbackRatio','maxFallbackTitleCount','overThresholdSampleCount','majorityFallbackSampleCount']) if (v[key] !== undefined && v[key] !== null) out[key] = Array.isArray(v[key]) ? v[key].slice(0,6) : v[key]; const text = Object.keys(out).length > 0 ? JSON.stringify(out) : null; return text ? clip(text, 220) : null; };",
|
||||
"const slimFinding = (item) => { const v = objectOrNull(item) || {}; return { kind: clip(v.kind ?? v.id ?? v.code, 48), code: clip(v.code ?? v.id ?? v.kind, 48), severity: clip(v.severity ?? v.level, 24), level: clip(v.level ?? v.severity, 24), count: v.count ?? v.sampleCount ?? null, sampleCount: v.sampleCount ?? v.count ?? null, summary: clip(v.summary ?? v.message, 180), message: clip(v.message ?? v.summary, 180), rootCause: clip(v.rootCause, 120), rootCauseStatus: clip(v.rootCauseStatus, 80), rootCauseConfidence: clip(v.rootCauseConfidence, 40), nextAction: clip(v.nextAction, 220), evidenceSummary: evidenceSummary(v.evidence) ?? clip(v.evidenceSummary, 220) }; };",
|
||||
"const slimProjectManagement = (value) => { const v = objectOrNull(value); if (!v) return null; const s = objectOrNull(v.summary) || v; return { summary: { enabled: s.enabled === true, projectSampleCount: s.projectSampleCount ?? null, mdtodoSampleCount: s.mdtodoSampleCount ?? null, latestPageKind: clip(s.latestPageKind, 48), latestPath: clip(s.latestPath, 96), latestSourceCount: s.latestSourceCount ?? null, latestFileCount: s.latestFileCount ?? null, latestTaskCount: s.latestTaskCount ?? null, latestSelectedTaskRefHash: clip(s.latestSelectedTaskRefHash, 80), launchCommandCount: s.launchCommandCount ?? null, launchSuccessCount: s.launchSuccessCount ?? null, launchFailureCount: s.launchFailureCount ?? null, launchWithOtelTraceHeaderCount: s.launchWithOtelTraceHeaderCount ?? null, projectApiResponseCount: s.projectApiResponseCount ?? null, projectApiFailureCount: s.projectApiFailureCount ?? null, projectApiSlowPathCount: s.projectApiSlowPathCount ?? null, slowApiBudgetMs: s.slowApiBudgetMs ?? null }, commands: takeTail(v.commands, 8).map((item) => { const row = objectOrNull(item) || {}; return { ts: row.ts ?? null, phase: clip(row.phase, 16), type: clip(row.type, 32), commandId: clip(row.commandId, 80), launchStatus: row.launchStatus ?? null, sessionId: clip(row.sessionId, 80), workbenchUrl: clip(row.workbenchUrl, 120), otelTraceId: clip(row.otelTraceId, 32), selectedTaskRefHash: clip(row.selectedTaskRefHash, 80) }; }), samples: takeTail(v.samples, 8).map((item) => { const row = objectOrNull(item) || {}; return { seq: row.seq ?? null, ts: row.ts ?? null, pageRole: clip(row.pageRole, 24), path: clip(row.path, 96), pageKind: clip(row.pageKind, 48), sourceCount: row.sourceCount ?? null, fileCount: row.fileCount ?? null, taskCount: row.taskCount ?? null, selectedTaskRefHash: clip(row.selectedTaskRefHash, 80), launchButtonEnabled: row.launchButtonEnabled === true, workbenchLinkCount: row.workbenchLinkCount ?? null }; }), projectApiByPath: takeHead(v.projectApiByPath, 8).map(slimNetworkGroup), valuesRedacted: true }; };",
|
||||
"const slimDomGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, text: clip(v.text ?? v.preview, 180) }; };",
|
||||
"const slimNetworkGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, method: clip(v.method, 12), status: v.status ?? null, path: clip(v.path ?? v.urlPath, 96), firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, promptIndexes: Array.isArray(v.promptIndexes) ? v.promptIndexes.slice(0, 6) : [], failureKinds: Array.isArray(v.failureKinds) ? v.failureKinds.slice(0, 4).map((x) => clip(x, 48)) : [] }; };",
|
||||
@@ -2178,7 +2179,8 @@ export function recoverWebObserveAnalyzeFromArtifacts(options: NodeWebProbeObser
|
||||
"const arr = (value) => Array.isArray(value) ? value : [];",
|
||||
"const clip = (value, limit = 160) => value === null || value === undefined ? null : String(value).slice(0, limit);",
|
||||
"const numberish = (...values) => { for (const value of values) { const n = Number(value); if (Number.isFinite(n)) return value; } return null; };",
|
||||
"const slimFinding = (item) => { const v = objectOrNull(item) || {}; return { kind: clip(v.kind ?? v.id ?? v.code, 48), code: clip(v.code ?? v.id ?? v.kind, 48), severity: clip(v.severity ?? v.level, 24), level: clip(v.level ?? v.severity, 24), count: v.count ?? v.sampleCount ?? null, sampleCount: v.sampleCount ?? v.count ?? null, summary: clip(v.summary ?? v.message, 180), message: clip(v.message ?? v.summary, 180) }; };",
|
||||
"const evidenceSummary = (value) => { const v = objectOrNull(value); if (!v) return null; const out = {}; for (const key of ['http404Count','responseErrorCount','requestFailedCount','statuses','afterProjectedSeqs','sinceSeqs','traceIds','maxFallbackRatio','maxFallbackTitleCount','overThresholdSampleCount','majorityFallbackSampleCount']) if (v[key] !== undefined && v[key] !== null) out[key] = Array.isArray(v[key]) ? v[key].slice(0,6) : v[key]; const text = Object.keys(out).length > 0 ? JSON.stringify(out) : null; return text ? clip(text, 220) : null; };",
|
||||
"const slimFinding = (item) => { const v = objectOrNull(item) || {}; return { kind: clip(v.kind ?? v.id ?? v.code, 48), code: clip(v.code ?? v.id ?? v.kind, 48), severity: clip(v.severity ?? v.level, 24), level: clip(v.level ?? v.severity, 24), count: v.count ?? v.sampleCount ?? null, sampleCount: v.sampleCount ?? v.count ?? null, summary: clip(v.summary ?? v.message, 180), message: clip(v.message ?? v.summary, 180), rootCause: clip(v.rootCause, 120), rootCauseStatus: clip(v.rootCauseStatus, 80), rootCauseConfidence: clip(v.rootCauseConfidence, 40), nextAction: clip(v.nextAction, 220), evidenceSummary: evidenceSummary(v.evidence) ?? clip(v.evidenceSummary, 220) }; };",
|
||||
"const slimRound = (item) => { const v = objectOrNull(item) || {}; return { promptIndex: v.promptIndex ?? null, promptTextHash: clip(v.promptTextHash, 80), sampleCount: v.sampleCount ?? null, firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, lastTotalElapsedSeconds: v.lastTotalElapsedSeconds ?? null, lastRecentUpdateSeconds: v.lastRecentUpdateSeconds ?? null, loadingSamples: v.loadingSamples ?? null, maxLoadingCount: v.maxLoadingCount ?? null, loadingOwnerCount: v.loadingOwnerCount ?? null, diagnosticSamples: v.diagnosticSamples ?? null, terminalSamples: v.terminalSamples ?? null, finalTextSamples: v.finalTextSamples ?? null, turnTimingTotalElapsedZeroResetCount: v.turnTimingTotalElapsedZeroResetCount ?? null, turnTimingTotalElapsedForwardJumpCount: v.turnTimingTotalElapsedForwardJumpCount ?? null, turnTimingTotalElapsedForwardJumpMaxSeconds: v.turnTimingTotalElapsedForwardJumpMaxSeconds ?? null, turnTimingRecentUpdateJumpCount: v.turnTimingRecentUpdateJumpCount ?? null, turnTimingRecentUpdateMaxIncreaseSeconds: v.turnTimingRecentUpdateMaxIncreaseSeconds ?? null }; };",
|
||||
"const slimTurnColumn = (item) => { const v = objectOrNull(item) || {}; return { label: clip(v.label, 24), source: clip(v.source, 48), pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), pageEpoch: v.pageEpoch ?? null, promptIndex: v.promptIndex ?? null, lastPromptIndex: v.lastPromptIndex ?? null, firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, traceId: clip(v.traceId, 64), messageId: clip(v.messageId, 64) }; };",
|
||||
"const slimGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, method: clip(v.method, 12), status: v.status ?? null, path: clip(v.path ?? v.urlPath ?? v.route, 96), firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, text: clip(v.text ?? v.preview, 180), failureKinds: arr(v.failureKinds).slice(0, 4).map((x) => clip(x, 48)), traceIds: arr(v.traceIds).slice(0, 3).map((x) => clip(x, 64)) }; };",
|
||||
|
||||
Reference in New Issue
Block a user