Fix monitor-web error detail catalog display

This commit is contained in:
Codex
2026-07-08 07:46:35 +02:00
parent fc329e3b31
commit defc39bc51
5 changed files with 87 additions and 9 deletions
@@ -1767,12 +1767,13 @@ function checkDisplay(item) {
const rawCode = rawCheckCode(item);
const rawId = rawFindingId(item);
const serviceCode = publicCheckCode(item?.checkCode || item?.check?.code);
const dynamicTitle = safeUserText(item?.displayTitleZh || item?.errorTitleZh || item?.timeoutDisplay?.titleZh);
const detailEvidence = item?.detail?.sentinelEvidence && typeof item.detail.sentinelEvidence === "object" ? item.detail.sentinelEvidence : {};
const dynamicTitle = safeUserText(item?.displayTitleZh || item?.errorTitleZh || item?.timeoutDisplay?.titleZh || detailEvidence.displayTitleZh);
return {
code: serviceCode || publicCheckCode(rawId || rawCode) || stableCheckCode(rawCode || rawId),
title: dynamicTitle || safeUserText(item?.checkTitleZh || item?.check?.titleZh) || "未登记监测项",
summary: safeUserText(item?.checkSummaryZh || item?.check?.summaryZh) || "已记录监测项详情,见报告原文。",
action: safeUserText(item?.checkActionZh || item?.check?.actionZh) || "查看详情后处理",
summary: safeUserText(item?.checkSummaryZh || item?.check?.summaryZh || detailEvidence.summary || detailEvidence.evidenceSummary) || "已记录监测项详情,见报告原文。",
action: safeUserText(item?.checkActionZh || item?.check?.actionZh || detailEvidence.nextAction) || "查看详情后处理",
};
}
@@ -150,6 +150,7 @@ export type WebProbeSentinelOptions =
readonly localDir: string;
readonly name: string | null;
readonly runId: string | null;
readonly errorId: string | null;
readonly timeoutMs: number;
readonly waitTimeoutMs: number;
readonly timeoutSeconds: number;
@@ -996,6 +996,7 @@ function publishCurrentDashboardOptions(state: SentinelCicdState, timeoutSeconds
localDir: "/tmp",
name: null,
runId: null,
errorId: null,
timeoutMs,
waitTimeoutMs,
timeoutSeconds: Math.min(numberAt(dashboard, "commandTimeoutSeconds"), Math.max(1, Math.trunc(timeoutSeconds))),
+78 -6
View File
@@ -300,7 +300,7 @@ export function runSentinelInspect(state: SentinelCicdState, options: Extract<We
return rendered(payload.ok, command, renderMonitorInspect(payload));
}
type MonitorEvidenceKind = "run" | "observer" | "trace" | "sample-seq" | "turn" | "command" | "artifact" | "report-sha" | "finding" | "unknown";
type MonitorEvidenceKind = "run" | "observer" | "trace" | "sample-seq" | "turn" | "command" | "artifact" | "report-sha" | "finding" | "error" | "unknown";
function parseMonitorEvidenceInput(input: string): { readonly inputKind: "url" | "id"; readonly ids: readonly Record<string, unknown>[] } {
const ids: Record<string, unknown>[] = [];
@@ -323,6 +323,8 @@ function parseMonitorEvidenceInput(input: string): { readonly inputKind: "url" |
addParam("reportSha", "report-sha", "reportSha");
addParam("finding", "finding");
addParam("findingId", "finding", "finding");
addParam("error", "error", "errorId");
addParam("errorId", "error", "errorId");
return { inputKind: "url", ids: ids.length > 0 ? ids : [{ kind: "unknown", value: input, source: "url", valuesRedacted: true }] };
}
return { inputKind: "id", ids: [classifyMonitorEvidenceId(input)] };
@@ -342,6 +344,7 @@ function classifyMonitorEvidenceId(input: string): Record<string, unknown> {
: /^webobs-[A-Za-z0-9_-]+$/u.test(value) ? "observer"
: /^trc[_-][A-Za-z0-9_-]+$/u.test(value) ? "trace"
: /^cmd[_-][A-Za-z0-9_-]+$/u.test(value) || /^command[_-][A-Za-z0-9_-]+$/u.test(value) ? "command"
: /^wbe_[a-f0-9]{20}$/u.test(value) ? "error"
: /^sha256[:_-]?[0-9a-f]{64}$/iu.test(value) ? "report-sha"
: /^[0-9]+$/u.test(value) ? "sample-seq"
: "unknown";
@@ -399,16 +402,25 @@ function commandsForEvidenceId(state: SentinelCicdState, item: Record<string, un
if (kind === "finding") {
return [{ kind, view: "report-findings", command: `${base} --view findings --raw`, valuesRedacted: true }];
}
if (kind === "error") {
return [
{ kind, view: "error-detail", command: `bun scripts/cli.ts web-probe sentinel error get --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)} --error-id ${shellQuote(value)}`, valuesRedacted: true },
{ kind, view: "dashboard-detail", command: `bun scripts/cli.ts web-probe sentinel dashboard verify --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)} --error-id ${shellQuote(value)}`, valuesRedacted: true },
];
}
return [{ kind, view: "classification-needed", command: `bun scripts/cli.ts web-probe sentinel inspect-id ${shellQuote(value)} --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)} --raw`, valuesRedacted: true }];
}
function monitorInspectNext(state: SentinelCicdState, evidence: { readonly ids: readonly Record<string, unknown>[] }): Record<string, unknown> {
const runId = firstEvidenceValue(evidence, "run");
const observerId = firstEvidenceValue(evidence, "observer");
const errorId = firstEvidenceValue(evidence, "error");
return {
reportSummary: runId === null ? null : `bun scripts/cli.ts web-probe sentinel report --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)} --run ${shellQuote(runId)} --view summary`,
reportFindings: runId === null ? null : `bun scripts/cli.ts web-probe sentinel report --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)} --run ${shellQuote(runId)} --view findings`,
observerTurnSummary: observerId === null ? null : `bun scripts/cli.ts web-probe observe collect ${shellQuote(observerId)} --node ${state.spec.nodeId} --lane ${state.spec.lane} --view turn-summary`,
errorDetail: errorId === null ? null : `bun scripts/cli.ts web-probe sentinel error get --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)} --error-id ${shellQuote(errorId)}`,
dashboardErrorVerify: errorId === null ? null : `bun scripts/cli.ts web-probe sentinel dashboard verify --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)} --error-id ${shellQuote(errorId)}`,
raw: `bun scripts/cli.ts web-probe sentinel inspect-id <id> --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)} --raw`,
valuesRedacted: true,
};
@@ -1001,14 +1013,14 @@ function reportText(value: unknown, maxChars: number): string | null {
}
export function runSentinelDashboard(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "dashboard" }>): RenderedCliResult {
const command = `web-probe sentinel dashboard ${options.action}${options.runId === null ? "" : ` --run ${options.runId}`}`;
const command = `web-probe sentinel dashboard ${options.action}${options.runId === null ? "" : ` --run ${options.runId}`}${options.errorId === null ? "" : ` --error-id ${options.errorId}`}`;
const result = probeSentinelDashboardBrowser(state, options);
return rendered(result.ok === true, command, options.raw ? JSON.stringify(result, null, 2) : renderDashboardResult(result));
}
export function probeSentinelDashboardBrowser(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "dashboard" }>): Record<string, unknown> {
const publicBaseUrl = stringAt(state.publicExposure, "publicBaseUrl").replace(/\/$/u, "");
const dashboardUrl = sentinelDashboardUrl(publicBaseUrl, options.runId);
const dashboardUrl = sentinelDashboardUrl(publicBaseUrl, options.runId, options.errorId);
const [widthRaw, heightRaw] = options.viewport.split("x");
const screenshotName = options.action === "screenshot" ? dashboardScreenshotName(options, state) : "";
const script = [
@@ -1024,6 +1036,7 @@ export function probeSentinelDashboardBrowser(state: SentinelCicdState, options:
`export UNIDESK_SENTINEL_DASHBOARD_EXPECTED_SENTINEL_ID=${shellQuote(state.sentinelId)}`,
`export UNIDESK_SENTINEL_DASHBOARD_EXPECTED_ROUTE_PREFIX=${shellQuote(stringAtNullable(state.publicExposure, "routePrefix") ?? "/")}`,
`export UNIDESK_SENTINEL_DASHBOARD_RUN_ID=${shellQuote(options.runId ?? "")}`,
`export UNIDESK_SENTINEL_DASHBOARD_ERROR_ID=${shellQuote(options.errorId ?? "")}`,
`export UNIDESK_SENTINEL_DASHBOARD_PLAYWRIGHT_MODULE=${shellQuote(`${state.spec.workspace}/node_modules/playwright/index.mjs`)}`,
"export PLAYWRIGHT_BROWSERS_PATH=0",
"if command -v chromium >/dev/null 2>&1; then",
@@ -1069,6 +1082,7 @@ export function probeSentinelDashboardBrowser(state: SentinelCicdState, options:
lane: state.spec.lane,
sentinelId: state.sentinelId,
runId: options.runId,
errorId: options.errorId,
publicUrl: `${publicBaseUrl}/`,
route,
viewport: options.viewport,
@@ -1097,12 +1111,16 @@ export function probeSentinelDashboardBrowser(state: SentinelCicdState, options:
};
}
function sentinelDashboardUrl(publicBaseUrl: string, runId: string | null): string {
function sentinelDashboardUrl(publicBaseUrl: string, runId: string | null, errorId: string | null): string {
const url = new URL(`${publicBaseUrl.replace(/\/$/u, "")}/`);
if (runId !== null && runId.length > 0) {
url.searchParams.set("run", runId);
url.searchParams.set("focus", "memory");
}
if (errorId !== null && errorId.length > 0) {
url.searchParams.set("error", errorId);
url.searchParams.set("panel", "detail");
}
return url.toString();
}
@@ -1125,6 +1143,7 @@ const executablePath = process.env.UNIDESK_SENTINEL_DASHBOARD_EXECUTABLE_PATH ||
const expectedSentinelId = process.env.UNIDESK_SENTINEL_DASHBOARD_EXPECTED_SENTINEL_ID || "";
const expectedRoutePrefix = process.env.UNIDESK_SENTINEL_DASHBOARD_EXPECTED_ROUTE_PREFIX || "/";
const requestedRunId = process.env.UNIDESK_SENTINEL_DASHBOARD_RUN_ID || "";
const expectedErrorId = process.env.UNIDESK_SENTINEL_DASHBOARD_ERROR_ID || "";
if (!url) throw new Error("missing dashboard URL");
@@ -1193,6 +1212,23 @@ if (requestedRunId) {
await page.waitForTimeout(150);
}
let requestedErrorSelection = { requestedErrorId: expectedErrorId, ok: expectedErrorId.length === 0, reason: expectedErrorId.length === 0 ? "not-requested" : "not-attempted" };
if (expectedErrorId) {
requestedErrorSelection = await page.evaluate(async (errorId) => {
const wait = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
for (let index = 0; index < 120; index += 1) {
const dialog = document.querySelector("[data-check-dialog='true']");
const dialogErrorId = dialog?.getAttribute("data-error-id") || "";
if (dialog instanceof HTMLElement && dialogErrorId === errorId) return { requestedErrorId: errorId, ok: true, reason: "dialog-open" };
const row = document.querySelector('[data-error-id="' + CSS.escape(errorId) + '"]');
if (row instanceof HTMLElement) row.click();
await wait(150);
}
return { requestedErrorId: errorId, ok: false, reason: "dialog-missing" };
}, expectedErrorId).catch((error) => ({ requestedErrorId: expectedErrorId, ok: false, reason: "selection-error", error: String(error?.message || error).slice(0, 300) }));
await page.waitForTimeout(200);
}
let manualTrigger = { requested: triggerManual, ok: !triggerManual, status: triggerManual ? "not-attempted" : "not-requested", jobName: null, statusText: "" };
if (triggerManual) {
for (let attempt = 1; attempt <= 2; attempt += 1) {
@@ -1226,7 +1262,7 @@ if (triggerManual) {
await page.waitForTimeout(300);
}
const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId, requestedRunId, requestedRunSelection, manualTrigger }) => {
const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId, requestedRunId, requestedRunSelection, expectedErrorId, requestedErrorSelection, manualTrigger }) => {
const numberValue = (value) => Number.isFinite(Number(value)) ? Number(value) : 0;
const objectOrNull = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
const text = (selector) => String(document.querySelector(selector)?.textContent || "").replace(/\s+/g, " ").trim();
@@ -1239,6 +1275,7 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
const memoryChart = document.querySelector("[data-run-memory-chart='true']");
const manualTriggerButton = document.querySelector("[data-monitor-manual-trigger='true']");
const manualTriggerStatus = document.querySelector("[data-monitor-manual-trigger-status='true']");
const checkDialog = document.querySelector("[data-check-dialog='true']");
const chartTiming = {
hasDurationCurve: Boolean(trend),
hasErrorCurve: Boolean(trendErrorCurve),
@@ -1403,6 +1440,28 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
requestMatched: !requestedRunId || targetRunId === requestedRunId,
},
requestedRunSelection,
requestedErrorSelection,
errorDetail: {
requested: Boolean(expectedErrorId),
requestedErrorId: expectedErrorId || null,
selectionOk: requestedErrorSelection.ok === true,
dialogPresent: Boolean(checkDialog),
dialogErrorId: checkDialog?.getAttribute("data-error-id") || null,
code: text("[data-check-dialog='true'] .check-code"),
title: text("#check-dialog-title"),
containsUnregistered: Boolean(checkDialog?.textContent?.includes("未登记监测项")),
textPreview: String(checkDialog?.textContent || "").replace(/\s+/g, " ").trim().slice(0, 500),
ok: !expectedErrorId || (
requestedErrorSelection.ok === true
&& Boolean(checkDialog)
&& checkDialog?.getAttribute("data-error-id") === expectedErrorId
&& !checkDialog?.textContent?.includes("未登记监测项")
&& text("[data-check-dialog='true'] .check-code") !== ""
&& text("[data-check-dialog='true'] .check-code") !== "-"
&& text("#check-dialog-title") !== ""
&& text("#check-dialog-title") !== "未登记监测项"
),
},
manualTriggerUi: {
requested: manualTrigger.requested,
ok: manualTrigger.ok,
@@ -1430,7 +1489,7 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
overflow: overflow.slice(0, 2),
},
};
}, { expectedRoutePrefix, expectedSentinelId, requestedRunId, requestedRunSelection, manualTrigger });
}, { expectedRoutePrefix, expectedSentinelId, requestedRunId, requestedRunSelection, expectedErrorId, requestedErrorSelection, manualTrigger });
if (captureScreenshot && screenshotPath) {
if (requestedRunId && dom.memorySummary?.present === true && dom.memorySummary?.matchesTargetRun === true) {
@@ -1461,6 +1520,7 @@ const ok = navigationOk
&& dom.sentinelBoundary?.routePrefixMatches === true
&& dom.errorVisible !== true
&& dom.requestedRunSelection?.ok === true
&& dom.errorDetail?.ok === true
&& manualTrigger.ok === true
&& dom.chartTiming?.ok === true
&& dom.memorySummary?.contractOk === true
@@ -2174,6 +2234,7 @@ function renderDashboardResult(result: Record<string, unknown>): string {
const chartTiming = record(dom.chartTiming);
const memorySummary = record(dom.memorySummary);
const requestedRunSelection = record(dom.requestedRunSelection);
const errorDetail = record(dom.errorDetail);
const serviceApi = record(result.serviceApi);
const manualTrigger = record(page.manualTrigger);
const manualTriggerUi = record(dom.manualTriggerUi);
@@ -2227,6 +2288,17 @@ function renderDashboardResult(result: Record<string, unknown>): string {
chartTiming.hasWarningCurve ?? "-",
]]),
"",
errorDetail.requested === true
? table(["ERROR_ID", "OPEN", "OK", "CODE", "TITLE", "UNREGISTERED"], [[
errorDetail.requestedErrorId ?? "-",
errorDetail.dialogPresent ?? "-",
errorDetail.ok ?? "-",
errorDetail.code ?? "-",
errorDetail.title ?? "-",
errorDetail.containsUnregistered ?? "-",
]])
: "ERROR_DETAIL\n-",
"",
manualTrigger.requested === true
? table(["TRIGGER", "STATUS", "JOB", "UI_STATE", "UI_TEXT"], [[
manualTrigger.ok ?? "-",
@@ -170,6 +170,8 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
const waitTimeoutMs = positiveIntegerOption(args, "--wait-timeout-ms", Math.max(60000, timeoutMs + 15000), 600000);
const commandTimeoutSeconds = positiveIntegerOption(args, "--command-timeout-seconds", Math.min(900, Math.max(120, Math.ceil(waitTimeoutMs / 1000) + 30)), 900);
const runId = optionValue(args, "--run") ?? optionValue(args, "--run-id") ?? null;
const errorId = optionValue(args, "--error-id") ?? null;
if (errorId !== null && !/^wbe_[a-f0-9]{20}$/u.test(errorId)) throw new Error(`web-probe sentinel dashboard --error-id must look like wbe_<20 hex>, got ${errorId}`);
sentinel = {
kind: "dashboard",
action: dashboardAction,
@@ -180,6 +182,7 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
localDir: optionValue(args, "--local-dir") ?? "/tmp",
name: optionValue(args, "--name") ?? null,
runId,
errorId,
timeoutMs,
waitTimeoutMs,
timeoutSeconds: commandTimeoutSeconds,