diff --git a/web/hwlab-cloud-web/scripts/workbench-e2e-server.ts b/web/hwlab-cloud-web/scripts/workbench-e2e-server.ts index 309e1c78..6684e0c8 100644 --- a/web/hwlab-cloud-web/scripts/workbench-e2e-server.ts +++ b/web/hwlab-cloud-web/scripts/workbench-e2e-server.ts @@ -227,6 +227,12 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse) if (path === "/v1/hwpod-node-ops") return json(response, 200, hwpodNodeOpsPayload()); if (path === "/v1/web-performance/summary") { if (state.scenarioId === "performance-summary-error") return errorDiagnosticResponse(response, 502, "/v1/web-performance/summary", "upstream_unavailable", "暂时无法连接上游。"); + const matrixStatus = performanceSummaryErrorStatus(state.scenarioId); + if (matrixStatus !== null) return errorDiagnosticResponse(response, matrixStatus, "/v1/web-performance/summary", `e2e_http_${matrixStatus}`, `模拟 ${matrixStatus} 错误。`, { traceId: statusMatrixTraceId(matrixStatus), requestId: `req_e2e_api_status_${matrixStatus}`, category: matrixStatus >= 500 ? "server" : "client" }); + if (state.scenarioId === "performance-network-error") { + response.destroy(new Error("e2e performance network reset")); + return; + } if (state.scenarioId === "performance-empty") return json(response, 200, webPerformanceEmptyPayload(url)); if (state.scenarioId === "performance-stale-window") return json(response, 200, webPerformanceStaleWindowPayload(url)); return json(response, 200, webPerformanceSummaryPayload(url)); @@ -1887,6 +1893,15 @@ function authLoginResponse(response: ServerResponse): void { return json(response, 401, { ok: false, status: "failed", error: { code: "invalid_credentials", message: "invalid credentials" } }); } +function performanceSummaryErrorStatus(scenarioId: string): number | null { + const match = /^performance-summary-error-(400|401|403|404|409|500)$/u.exec(scenarioId); + return match ? Number(match[1]) : null; +} + +function statusMatrixTraceId(status: number): string { + return String(status).repeat(11).slice(0, 32); +} + function redactRequestBody(value: JsonRecord): JsonRecord { const output: JsonRecord = {}; for (const [key, item] of Object.entries(value)) output[key] = /secret|token|key|password|authorization/iu.test(key) ? "" : item; diff --git a/web/hwlab-cloud-web/tests/workbench-e2e/specs/admin-hwpod-performance.spec.ts b/web/hwlab-cloud-web/tests/workbench-e2e/specs/admin-hwpod-performance.spec.ts index a8eaa708..cecce3ee 100644 --- a/web/hwlab-cloud-web/tests/workbench-e2e/specs/admin-hwpod-performance.spec.ts +++ b/web/hwlab-cloud-web/tests/workbench-e2e/specs/admin-hwpod-performance.spec.ts @@ -3,6 +3,7 @@ import type { Page } from "@playwright/test"; import { expect, fakeServerState, test } from "../fixtures/test"; type RequestLedgerEntry = { path?: string; query?: Record }; +const performanceErrorStatuses = [400, 401, 403, 404, 409, 500] as const; async function performanceSummaryRequests(page: Page): Promise { const state = await fakeServerState(page); @@ -159,3 +160,53 @@ test.describe("performance dashboard summary error", () => { await expect(page.locator(".performance-contract-item")).toHaveCount(0); }); }); + +for (const status of performanceErrorStatuses) { + test.describe(`performance dashboard HTTP ${status} diagnostic`, () => { + test.use({ scenarioId: `performance-summary-error-${status}` }); + + test(`surfaces HTTP ${status} trace diagnostics without stale dashboard data`, async ({ page }) => { + await page.goto("/performance"); + + await expect(page.getByRole("heading", { name: "性能监控" })).toBeVisible(); + await expect(page.getByText("性能监控加载失败")).toBeVisible(); + const diagnostic = page.locator(".api-error-diagnostic"); + await expect(diagnostic).toContainText("trace_id"); + await expect(diagnostic).toContainText(statusTraceId(status)); + await expect(diagnostic).toContainText("requestId"); + await expect(diagnostic).toContainText(`req_e2e_api_status_${status}`); + await expect(diagnostic).toContainText(`e2e_http_${status}`); + await expect(diagnostic).toContainText("/v1/web-performance/summary"); + await expect(diagnostic).toContainText(String(status)); + await expect(diagnostic).toContainText("valuesPrinted"); + await expect(diagnostic).toContainText("false"); + await expect(page.getByRole("button", { name: "重试" })).toBeVisible(); + await expect(page.locator(".performance-contract-item")).toHaveCount(0); + }); + }); +} + +test.describe("performance dashboard browser network diagnostic", () => { + test.use({ scenarioId: "performance-network-error" }); + + test("surfaces browser generated trace diagnostics when the request has no response", async ({ page }) => { + await page.goto("/performance"); + + await expect(page.getByRole("heading", { name: "性能监控" })).toBeVisible(); + await expect(page.getByText("性能监控加载失败")).toBeVisible(); + const diagnostic = page.locator(".api-error-diagnostic"); + await expect(diagnostic).toContainText("trace_id"); + await expect(diagnostic).toContainText("requestId"); + await expect(diagnostic).toContainText("browser_network_error"); + await expect(diagnostic).toContainText("web / network"); + await expect(diagnostic).toContainText("/v1/web-performance/summary"); + await expect(diagnostic).toContainText("browser"); + await expect(diagnostic).toContainText("valuesPrinted"); + await expect(diagnostic).toContainText("false"); + await expect(page.locator(".performance-contract-item")).toHaveCount(0); + }); +}); + +function statusTraceId(status: number): string { + return String(status).repeat(11).slice(0, 32); +}