From defc39bc513370945a229b9047a2f51e3b98819c Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 8 Jul 2026 07:46:35 +0200 Subject: [PATCH] Fix monitor-web error detail catalog display --- .../monitor-web.js | 7 +- .../hwlab-node-web-sentinel-cicd-shared.ts | 1 + scripts/src/hwlab-node-web-sentinel-cicd.ts | 1 + scripts/src/hwlab-node-web-sentinel-p5.ts | 84 +++++++++++++++++-- scripts/src/hwlab-node/web-probe-observe.ts | 3 + 5 files changed, 87 insertions(+), 9 deletions(-) diff --git a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js index 46c09501..01cc693c 100644 --- a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js +++ b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js @@ -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) || "查看详情后处理", }; } diff --git a/scripts/src/hwlab-node-web-sentinel-cicd-shared.ts b/scripts/src/hwlab-node-web-sentinel-cicd-shared.ts index e78c9e43..679ea318 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd-shared.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd-shared.ts @@ -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; diff --git a/scripts/src/hwlab-node-web-sentinel-cicd.ts b/scripts/src/hwlab-node-web-sentinel-cicd.ts index 69d9ee5d..1e4b162e 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd.ts @@ -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))), diff --git a/scripts/src/hwlab-node-web-sentinel-p5.ts b/scripts/src/hwlab-node-web-sentinel-p5.ts index 65d006e7..9bb4db05 100644 --- a/scripts/src/hwlab-node-web-sentinel-p5.ts +++ b/scripts/src/hwlab-node-web-sentinel-p5.ts @@ -300,7 +300,7 @@ export function runSentinelInspect(state: SentinelCicdState, options: Extract[] } { const ids: Record[] = []; @@ -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 { : /^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[] }): Record { 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 --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): 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): Record { 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 { 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 { 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 ?? "-", diff --git a/scripts/src/hwlab-node/web-probe-observe.ts b/scripts/src/hwlab-node/web-probe-observe.ts index 41d7de6d..ccf60929 100644 --- a/scripts/src/hwlab-node/web-probe-observe.ts +++ b/scripts/src/hwlab-node/web-probe-observe.ts @@ -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,