From 8c2724265677bdee5198c7359afa4d24c05fc060 Mon Sep 17 00:00:00 2001 From: lyon Date: Sat, 20 Jun 2026 12:32:44 +0800 Subject: [PATCH] fix: scope workbench performance attribution health --- internal/cloud/web-performance.test.ts | 45 ++++++++++++++++++++++++++ internal/cloud/web-performance.ts | 18 ++++++++--- web/hwlab-cloud-web/src/types/index.ts | 2 +- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/internal/cloud/web-performance.test.ts b/internal/cloud/web-performance.test.ts index 45c0da82..663b02a0 100644 --- a/internal/cloud/web-performance.test.ts +++ b/internal/cloud/web-performance.test.ts @@ -249,6 +249,51 @@ test("web performance summary exposes Workbench p75 and low-sample diagnostics w assert.doesNotMatch(json, /trc_secret|ses_secret|run_secret|cmd_secret|traceId|sessionId|runId|commandId|conversationId|prompt|api key/iu); }); +test("web performance collection health only requires backend attribution for backend-scoped Workbench series", () => { + const frontendOnly = createWebPerformanceStore({ env: { HWLAB_METRICS_NAMESPACE: "hwlab-v03", HWLAB_GITOPS_TARGET: "v03" } }); + frontendOnly.record({ + schemaVersion: "hwlab-web-performance-v2", + page: "/workbench", + events: Array.from({ length: 5 }, () => ({ + kind: "workbench_journey", + journey: "workbench_open_first_visible", + route: "/workbench", + cache: "warm", + authState: "warm", + visibility: "foreground", + outcome: "ok", + valueMs: 120 + })) + }); + const frontendSummary = frontendOnly.summary(); + assert.equal(frontendSummary.collectionHealth.status, "fresh"); + assert.equal(frontendSummary.collectionHealth.unknownAttribution.workbenchRows, 1); + assert.equal(frontendSummary.collectionHealth.unknownAttribution.attributableRows, 0); + assert.equal(frontendSummary.collectionHealth.unknownAttribution.rowsWithUnknown, 0); + assert.equal(frontendSummary.collectionHealth.diagnostics.some((item) => item.code === "unknown_attribution"), false); + + const backendScoped = createWebPerformanceStore({ env: { HWLAB_METRICS_NAMESPACE: "hwlab-v03", HWLAB_GITOPS_TARGET: "v03" } }); + backendScoped.record({ + schemaVersion: "hwlab-web-performance-v2", + page: "/workbench/sessions/ses_secret", + events: Array.from({ length: 5 }, () => ({ + kind: "workbench_journey", + journey: "submit_to_first_visible", + route: "/workbench/sessions/ses_secret", + entry: "existing", + visibility: "foreground", + outcome: "ok", + valueMs: 600 + })) + }); + const backendSummary = backendScoped.summary(); + assert.equal(backendSummary.collectionHealth.status, "degraded"); + assert.equal(backendSummary.collectionHealth.unknownAttribution.attributableRows, 1); + assert.equal(backendSummary.collectionHealth.unknownAttribution.rowsWithUnknown, 1); + assert.equal(backendSummary.collectionHealth.unknownAttribution.samplesWithUnknown, 5); + assert.equal(backendSummary.collectionHealth.diagnostics.some((item) => item.code === "unknown_attribution"), true); +}); + test("web performance store accepts Workbench UI lifecycle events without high-cardinality labels", () => { const store = createWebPerformanceStore({ env: { HWLAB_METRICS_NAMESPACE: "hwlab-v03", HWLAB_GITOPS_TARGET: "v03" } }); const result = store.record({ diff --git a/internal/cloud/web-performance.ts b/internal/cloud/web-performance.ts index 43cb5429..1ea62b6d 100644 --- a/internal/cloud/web-performance.ts +++ b/internal/cloud/web-performance.ts @@ -287,6 +287,7 @@ interface PerformanceCollectionHealth { sourceFamilyCounts: Array<{ sourceFamily: string; rowCount: number; sampleCount: number }>; unknownAttribution: { workbenchRows: number; + attributableRows: number; rowsWithUnknown: number; samplesWithUnknown: number; backendUnknownRows: number; @@ -1623,11 +1624,18 @@ function collectionSourceFamilyCounts(rows: PerformanceSummaryRow[]) { function collectionUnknownAttribution(rows: PerformanceSummaryRow[]) { const workbenchRows = rows.filter((row) => row.kind === "workbench_journey" || row.kind === "workbench_event_phase" || row.kind === "workbench_backend_event_visible"); - const backendUnknownRows = workbenchRows.filter((row) => unknownAttributionValue(row.backend)).length; - const transportUnknownRows = workbenchRows.filter((row) => unknownAttributionValue(row.transport)).length; - const rowsWithUnknown = workbenchRows.filter((row) => unknownAttributionValue(row.backend) || unknownAttributionValue(row.transport)).length; - const samplesWithUnknown = workbenchRows.reduce((sum, row) => sum + ((unknownAttributionValue(row.backend) || unknownAttributionValue(row.transport)) ? Math.max(0, Number(row.count) || 0) : 0), 0); - return { workbenchRows: workbenchRows.length, rowsWithUnknown, samplesWithUnknown, backendUnknownRows, transportUnknownRows }; + const attributionRows = workbenchRows.filter(rowNeedsWorkbenchAttribution); + const backendUnknownRows = attributionRows.filter((row) => unknownAttributionValue(row.backend)).length; + const transportUnknownRows = attributionRows.filter((row) => unknownAttributionValue(row.transport)).length; + const rowsWithUnknown = attributionRows.filter((row) => unknownAttributionValue(row.backend) || unknownAttributionValue(row.transport)).length; + const samplesWithUnknown = attributionRows.reduce((sum, row) => sum + ((unknownAttributionValue(row.backend) || unknownAttributionValue(row.transport)) ? Math.max(0, Number(row.count) || 0) : 0), 0); + return { workbenchRows: workbenchRows.length, attributableRows: attributionRows.length, rowsWithUnknown, samplesWithUnknown, backendUnknownRows, transportUnknownRows }; +} + +function rowNeedsWorkbenchAttribution(row: PerformanceSummaryRow) { + if (row.kind === "workbench_event_phase" || row.kind === "workbench_backend_event_visible") return true; + if (row.kind !== "workbench_journey") return false; + return row.metric === "submit_to_first_visible" || row.metric === "submit_to_failure" || row.metric === "backend_event_to_visible"; } function unknownAttributionValue(value: unknown) { diff --git a/web/hwlab-cloud-web/src/types/index.ts b/web/hwlab-cloud-web/src/types/index.ts index a5615305..e28993ca 100644 --- a/web/hwlab-cloud-web/src/types/index.ts +++ b/web/hwlab-cloud-web/src/types/index.ts @@ -468,7 +468,7 @@ export interface WebPerformanceCollectionHealth extends Record secondsSinceLastSample?: number | null; windowCoverageSeconds?: number | null; sourceFamilyCounts?: Array<{ sourceFamily?: string; rowCount?: number; sampleCount?: number; [key: string]: unknown }>; - unknownAttribution?: { workbenchRows?: number; rowsWithUnknown?: number; samplesWithUnknown?: number; backendUnknownRows?: number; transportUnknownRows?: number; [key: string]: unknown }; + unknownAttribution?: { workbenchRows?: number; attributableRows?: number; rowsWithUnknown?: number; samplesWithUnknown?: number; backendUnknownRows?: number; transportUnknownRows?: number; [key: string]: unknown }; diagnostics?: WebPerformanceCollectionHealthDiagnostic[]; }