|
|
|
@@ -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 ?? "-",
|
|
|
|
|