fix: add sentinel run memory detail curves
This commit is contained in:
@@ -408,7 +408,7 @@ 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}`;
|
||||
const command = `web-probe sentinel dashboard ${options.action}${options.runId === null ? "" : ` --run ${options.runId}`}`;
|
||||
const result = probeSentinelDashboardBrowser(state, options);
|
||||
return rendered(result.ok === true, command, options.raw ? JSON.stringify(result, null, 2) : renderDashboardResult(result));
|
||||
}
|
||||
@@ -428,6 +428,7 @@ export function probeSentinelDashboardBrowser(state: SentinelCicdState, options:
|
||||
`export UNIDESK_SENTINEL_DASHBOARD_FULL_PAGE=${shellQuote(options.fullPage ? "1" : "0")}`,
|
||||
`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_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",
|
||||
@@ -470,6 +471,7 @@ export function probeSentinelDashboardBrowser(state: SentinelCicdState, options:
|
||||
node: state.spec.nodeId,
|
||||
lane: state.spec.lane,
|
||||
sentinelId: state.sentinelId,
|
||||
runId: options.runId,
|
||||
publicUrl: `${publicBaseUrl}/`,
|
||||
route,
|
||||
viewport: options.viewport,
|
||||
@@ -514,6 +516,7 @@ const fullPage = process.env.UNIDESK_SENTINEL_DASHBOARD_FULL_PAGE !== "0";
|
||||
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 || "";
|
||||
|
||||
if (!url) throw new Error("missing dashboard URL");
|
||||
|
||||
@@ -565,18 +568,72 @@ for (let attempt = 1; attempt <= maxNavigationAttempts; attempt += 1) {
|
||||
await page.waitForTimeout(750 * attempt);
|
||||
}
|
||||
|
||||
let requestedRunSelection = { requestedRunId, ok: requestedRunId.length === 0, reason: requestedRunId.length === 0 ? "not-requested" : "not-attempted" };
|
||||
if (requestedRunId) {
|
||||
requestedRunSelection = await page.evaluate(async (runId) => {
|
||||
const wait = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
|
||||
const rowForRun = () => document.querySelector('.run-list .run-row[data-run-id="' + CSS.escape(runId) + '"]');
|
||||
const row = rowForRun();
|
||||
if (!(row instanceof HTMLElement)) return { requestedRunId: runId, ok: false, reason: "run-row-missing" };
|
||||
row.click();
|
||||
for (let index = 0; index < 40; index += 1) {
|
||||
const memoryChart = document.querySelector("[data-run-memory-chart='true']");
|
||||
const selected = rowForRun();
|
||||
if (memoryChart?.getAttribute("data-memory-run-id") === runId
|
||||
&& selected instanceof HTMLElement
|
||||
&& selected.classList.contains("selected")) {
|
||||
return { requestedRunId: runId, ok: true, reason: "selected" };
|
||||
}
|
||||
await wait(100);
|
||||
}
|
||||
return { requestedRunId: runId, ok: false, reason: "detail-switch-timeout" };
|
||||
}, requestedRunId).catch((error) => ({ requestedRunId, ok: false, reason: "selection-error", error: String(error?.message || error).slice(0, 300) }));
|
||||
await page.waitForTimeout(150);
|
||||
}
|
||||
|
||||
if (captureScreenshot && screenshotPath) {
|
||||
await page.screenshot({ path: screenshotPath, fullPage, animations: "disabled" }).catch((error) => {
|
||||
pageErrors.push({ message: "screenshot failed: " + String(error?.message || error).slice(0, 400) });
|
||||
});
|
||||
}
|
||||
|
||||
const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId }) => {
|
||||
const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId, requestedRunId, requestedRunSelection }) => {
|
||||
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();
|
||||
const root = document.querySelector("#monitor-web-root");
|
||||
const shell = document.querySelector("[data-monitor-shell='true']");
|
||||
const error = document.querySelector("#monitor-web-error");
|
||||
const trend = document.querySelector("[data-monitor-trend-curve]");
|
||||
const trendErrorCurve = document.querySelector("[data-monitor-trend-error-curve='true']");
|
||||
const trendWarningCurve = document.querySelector("[data-monitor-trend-warning-curve='true']");
|
||||
const memoryChart = document.querySelector("[data-run-memory-chart='true']");
|
||||
const legendTexts = Array.from(document.querySelectorAll(".trend-legend .legend-item")).map((item) => String(item.textContent || "").replace(/\s+/g, " ").trim());
|
||||
const legendNumber = (label) => {
|
||||
const row = legendTexts.find((item) => item.includes(label)) || "";
|
||||
const match = /(\d+(?:\.\d+)?)/u.exec(row);
|
||||
return match ? Number(match[1]) : null;
|
||||
};
|
||||
const chartTiming = {
|
||||
latestMinutes: legendNumber("最新运行耗时"),
|
||||
maxMinutes: legendNumber("最近最高耗时"),
|
||||
title: text("#trend-heading"),
|
||||
legendHasDuration: legendTexts.some((item) => item.includes("最新运行耗时")),
|
||||
legendHasError: legendTexts.some((item) => item.includes("独立错误")),
|
||||
legendHasWarning: legendTexts.some((item) => item.includes("独立告警")),
|
||||
hasDurationCurve: Boolean(trend),
|
||||
hasErrorCurve: Boolean(trendErrorCurve),
|
||||
hasWarningCurve: Boolean(trendWarningCurve),
|
||||
ok: false,
|
||||
};
|
||||
chartTiming.ok = chartTiming.title.includes("运行趋势")
|
||||
&& chartTiming.legendHasDuration === true
|
||||
&& chartTiming.legendHasError === true
|
||||
&& chartTiming.legendHasWarning === true
|
||||
&& chartTiming.hasDurationCurve === true
|
||||
&& chartTiming.hasErrorCurve === true
|
||||
&& chartTiming.hasWarningCurve === true
|
||||
&& Number.isFinite(chartTiming.latestMinutes);
|
||||
const dataset = root ? {
|
||||
node: root.getAttribute("data-node"),
|
||||
lane: root.getAttribute("data-lane"),
|
||||
@@ -624,6 +681,36 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
|
||||
durationMinutes: numberValue(latestRun?.runDurationMinutes ?? latestRun?.durationMinutes ?? latestRun?.timing?.durationMinutes),
|
||||
severityKeys: Object.keys(latestCounts).sort(),
|
||||
};
|
||||
const targetRunId = requestedRunId || latestRunCounts.runId;
|
||||
const targetRun = targetRunId ? runs.find((run) => (run?.id || run?.runId) === targetRunId) || latestRun : latestRun;
|
||||
const targetDetailResult = targetRunId === null ? { ok: false, httpStatus: null, body: null } : await fetchJson("/api/runs/" + encodeURIComponent(targetRunId));
|
||||
const targetDetailPayload = objectOrNull(targetDetailResult.body) || {};
|
||||
const targetMemory = objectOrNull(targetDetailPayload.memory) || {};
|
||||
const targetPageSeries = Array.isArray(targetMemory.pageSeries) ? targetMemory.pageSeries : [];
|
||||
const memorySummary = {
|
||||
present: Boolean(memoryChart),
|
||||
runId: memoryChart?.getAttribute("data-memory-run-id") || null,
|
||||
targetRunId,
|
||||
targetScenarioId: targetRun?.scenarioId || targetRun?.scenario_id || null,
|
||||
matchesTargetRun: memoryChart?.getAttribute("data-memory-run-id") === targetRunId,
|
||||
pageCount: numberValue(memoryChart?.getAttribute("data-memory-page-count")),
|
||||
sampleCount: numberValue(memoryChart?.getAttribute("data-memory-sample-count")),
|
||||
source: memoryChart?.getAttribute("data-memory-source") || null,
|
||||
curveCount: document.querySelectorAll(".memory-chart .memory-line").length,
|
||||
legendCount: document.querySelectorAll(".memory-legend .legend-item").length,
|
||||
apiOk: targetDetailResult.ok === true && targetDetailPayload.ok !== false,
|
||||
apiPageCount: targetPageSeries.length,
|
||||
apiSampleCount: numberValue(targetMemory.sampleCount),
|
||||
apiSource: targetMemory.source || null,
|
||||
perPageLines: false,
|
||||
};
|
||||
memorySummary.perPageLines = memorySummary.present === true
|
||||
&& memorySummary.matchesTargetRun === true
|
||||
&& memorySummary.pageCount > 0
|
||||
&& memorySummary.curveCount === memorySummary.pageCount
|
||||
&& memorySummary.legendCount === memorySummary.pageCount
|
||||
&& memorySummary.apiOk === true
|
||||
&& memorySummary.apiPageCount === memorySummary.pageCount;
|
||||
const datasetSentinelId = String(dataset.sentinelId || "");
|
||||
const finalPath = new URL(window.location.href).pathname.replace(/\/+$/u, "") || "/";
|
||||
const expectedPath = expectedRoutePrefix.replace(/\/+$/u, "") || "/";
|
||||
@@ -681,6 +768,8 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
|
||||
runCount: runs.length,
|
||||
latestRunId: latestRunCounts.runId,
|
||||
latestFindingTypeCount: latestRunCounts.typeCount,
|
||||
trendCurves: chartTiming.ok,
|
||||
memoryPerPageLines: memorySummary.perPageLines,
|
||||
},
|
||||
sentinelBoundary,
|
||||
title: document.title,
|
||||
@@ -688,9 +777,20 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
|
||||
runRows: runs.length,
|
||||
checkRows: document.querySelectorAll("[data-check-row='true']").length,
|
||||
latestRunCounts,
|
||||
targetRunCounts: {
|
||||
runId: targetRunId,
|
||||
scenarioId: targetRun?.scenarioId || targetRun?.scenario_id || null,
|
||||
durationMinutes: numberValue(targetRun?.runDurationMinutes ?? targetRun?.durationMinutes ?? targetRun?.timing?.durationMinutes),
|
||||
requested: Boolean(requestedRunId),
|
||||
requestMatched: !requestedRunId || targetRunId === requestedRunId,
|
||||
},
|
||||
requestedRunSelection,
|
||||
chartTiming,
|
||||
memorySummary,
|
||||
api: {
|
||||
overview: { ok: overviewResult.ok, httpStatus: overviewResult.httpStatus },
|
||||
runs: { ok: runsResult.ok, httpStatus: runsResult.httpStatus },
|
||||
targetDetail: { ok: targetDetailResult.ok, httpStatus: targetDetailResult.httpStatus },
|
||||
},
|
||||
errorVisible: Boolean(error && !error.hidden),
|
||||
errorText: error && !error.hidden ? String(error.textContent || "").replace(/\s+/g, " ").trim().slice(0, 500) : "",
|
||||
@@ -702,7 +802,7 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
|
||||
overflow: overflow.slice(0, 2),
|
||||
},
|
||||
};
|
||||
}, { expectedRoutePrefix, expectedSentinelId });
|
||||
}, { expectedRoutePrefix, expectedSentinelId, requestedRunId, requestedRunSelection });
|
||||
|
||||
const consoleErrors = consoleMessages.filter((item) => item.type === "error");
|
||||
const ok = !navigationError
|
||||
@@ -719,6 +819,9 @@ const ok = !navigationError
|
||||
&& dom.sentinelBoundary?.runRowsMatch === true
|
||||
&& dom.sentinelBoundary?.routePrefixMatches === true
|
||||
&& dom.errorVisible !== true
|
||||
&& dom.requestedRunSelection?.ok === true
|
||||
&& dom.chartTiming?.ok === true
|
||||
&& dom.memorySummary?.perPageLines === true
|
||||
&& dom.layout?.horizontalOverflow !== true
|
||||
&& pageErrors.length === 0;
|
||||
|
||||
@@ -1170,6 +1273,10 @@ function renderDashboardResult(result: Record<string, unknown>): string {
|
||||
const apiRuns = record(api.runs);
|
||||
const layout = record(dom.layout);
|
||||
const latestRunCounts = record(dom.latestRunCounts);
|
||||
const targetRunCounts = record(dom.targetRunCounts);
|
||||
const chartTiming = record(dom.chartTiming);
|
||||
const memorySummary = record(dom.memorySummary);
|
||||
const requestedRunSelection = record(dom.requestedRunSelection);
|
||||
const screenshot = record(result.screenshot);
|
||||
const remote = record(result.remote);
|
||||
const transport = record(result.transport);
|
||||
@@ -1210,6 +1317,27 @@ function renderDashboardResult(result: Record<string, unknown>): string {
|
||||
latestRunCounts.typeCount ?? "-",
|
||||
]]),
|
||||
"",
|
||||
table(["TARGET_RUN", "REQUESTED", "SELECTED", "TREND_OK", "ERR_CURVE", "WARN_CURVE"], [[
|
||||
targetRunCounts.runId ?? "-",
|
||||
targetRunCounts.requested ?? "-",
|
||||
requestedRunSelection.ok ?? "-",
|
||||
chartTiming.ok ?? "-",
|
||||
chartTiming.hasErrorCurve ?? "-",
|
||||
chartTiming.hasWarningCurve ?? "-",
|
||||
]]),
|
||||
"",
|
||||
table(["MEMORY_CHART", "MEMORY_RUN", "MEMORY_MATCH", "MEMORY_PAGES", "MEMORY_CURVES", "MEMORY_SAMPLES", "API_PAGES", "MEMORY_PAGE_LINES", "MEMORY_SOURCE"], [[
|
||||
memorySummary.present ?? "-",
|
||||
memorySummary.runId ?? "-",
|
||||
memorySummary.matchesTargetRun ?? "-",
|
||||
memorySummary.pageCount ?? "-",
|
||||
memorySummary.curveCount ?? "-",
|
||||
memorySummary.sampleCount ?? "-",
|
||||
memorySummary.apiPageCount ?? "-",
|
||||
memorySummary.perPageLines ?? "-",
|
||||
memorySummary.source ?? memorySummary.apiSource ?? "-",
|
||||
]]),
|
||||
"",
|
||||
table(["VIEWPORT", "DOC", "H_OVERFLOW", "OVERFLOW_COUNT"], [[
|
||||
result.viewport,
|
||||
`${record(layout.documentSize).width ?? "-"}x${record(layout.documentSize).height ?? "-"}`,
|
||||
|
||||
Reference in New Issue
Block a user