533 lines
29 KiB
TypeScript
533 lines
29 KiB
TypeScript
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-19-p0
|
|
// Verifies the Workbench RUM metrics contract and Prometheus label privacy boundary.
|
|
|
|
import assert from "node:assert/strict";
|
|
import { test } from "bun:test";
|
|
|
|
import { createCloudApiServer } from "./server.ts";
|
|
import { createWebPerformanceStore, emitWorkbenchUiOtelSpans, webPerformanceRouteTemplate } from "./web-performance.ts";
|
|
|
|
test("web performance store aggregates browser-perceived durations as Prometheus histograms", () => {
|
|
const store = createWebPerformanceStore({ env: { HWLAB_METRICS_NAMESPACE: "hwlab-v02", HWLAB_GITOPS_TARGET: "v02" } });
|
|
const result = store.record({
|
|
schemaVersion: "hwlab-web-performance-v1",
|
|
page: "/workspace",
|
|
events: [
|
|
{ kind: "web_vital", metric: "lcp", route: "/workspace", valueMs: 1234 },
|
|
{ kind: "web_vital", metric: "cls", route: "/workspace", value: 0.08 },
|
|
{ kind: "api", metric: "api_request", route: "/v1/agent/chat/result/trc_secret", method: "GET", status: 200, statusClass: "2xx", outcome: "ok", valueMs: 420 }
|
|
]
|
|
});
|
|
const text = store.metricsText();
|
|
|
|
assert.deepEqual(result, { accepted: 3, dropped: 0, received: 3 });
|
|
assert.match(text, /hwlab_webui_performance_sample_total\{service="hwlab-cloud-web",namespace="hwlab-v02",gitops_target="v02",kind="web_vital",metric="lcp",route="\/workspace"/u);
|
|
assert.match(text, /hwlab_webui_performance_duration_seconds_bucket\{[^}]*metric="api_request"[^}]*route="\/v1\/agent\/chat\/result\/:id"[^}]*le="0\.5"\} 1/u);
|
|
assert.match(text, /hwlab_webui_layout_shift_score_bucket\{[^}]*metric="cls"[^}]*le="0\.1"\} 1/u);
|
|
assert.doesNotMatch(text, /trc_secret|sessionId|conversationId|threadId|prompt|api key/iu);
|
|
});
|
|
|
|
test("web performance summary exposes user-perceived route latency without high-cardinality data", () => {
|
|
const store = createWebPerformanceStore({ env: { HWLAB_METRICS_NAMESPACE: "hwlab-v02", HWLAB_GITOPS_TARGET: "v02" } });
|
|
store.record({
|
|
schemaVersion: "hwlab-web-performance-v1",
|
|
page: "/workspace",
|
|
events: [
|
|
{ kind: "api", metric: "api_request", route: "/v1/agent/chat/result/trc_secret", method: "GET", status: 200, statusClass: "2xx", outcome: "ok", valueMs: 420 },
|
|
{ kind: "api", metric: "api_request", route: "/v1/agent/chat/result/trc_secret", method: "GET", status: 0, statusClass: "network", outcome: "timeout", valueMs: 2600 },
|
|
{ kind: "long_task", metric: "long_task", route: "/workspace", valueMs: 260 },
|
|
{ kind: "web_vital", metric: "lcp", route: "/workspace", valueMs: 3100 }
|
|
]
|
|
});
|
|
|
|
const summary = store.summary({ backendEvidence: {
|
|
schemaVersion: "hwlab-backend-performance-evidence-v1",
|
|
source: "test-backend-performance-store",
|
|
observedAt: "2026-06-18T00:00:05.000Z",
|
|
sampleCount: 2,
|
|
retainedSampleCount: 2,
|
|
routeCount: 1,
|
|
rows: [{
|
|
route: "/v1/agent/chat/result/:id",
|
|
method: "GET",
|
|
statusClass: "2xx",
|
|
outcome: "ok",
|
|
count: 2,
|
|
average: 0.11,
|
|
p50: 0.09,
|
|
p75: 0.12,
|
|
p95: 0.16,
|
|
firstObservedAt: "2026-06-18T00:00:00.000Z",
|
|
lastObservedAt: "2026-06-18T00:00:04.000Z",
|
|
responseBytes: { count: 2, average: 2048, p50: 1024, p75: 2048, p95: 4096 },
|
|
phases: [{ phase: "agent_result_read", outcome: "ok", count: 2, average: 0.05, p50: 0.04, p75: 0.05, p95: 0.08 }]
|
|
}],
|
|
cache: [{
|
|
route: "/v1/workbench/sessions",
|
|
cacheKeyClass: "sessions_summary",
|
|
count: 4,
|
|
hitCount: 3,
|
|
missCount: 1,
|
|
staleCount: 0,
|
|
unavailableCount: 0,
|
|
dbQueryAvoidedCount: 3,
|
|
hitRatio: 0.75,
|
|
dbQueryAvoidedRatio: 0.75,
|
|
unavailableRatio: 0,
|
|
operationDuration: { count: 4, average: 0.12, p50: 0.08, p75: 0.11, p95: 0.18, unit: "seconds" },
|
|
valuesRedacted: true
|
|
}],
|
|
valuesRedacted: true
|
|
} });
|
|
const json = JSON.stringify(summary);
|
|
assert.equal(summary.summary.sampleCount, 4);
|
|
assert.equal(summary.namespace, "hwlab-v02");
|
|
assert.equal(summary.collectionHealth.schemaVersion, "hwlab-web-performance-collection-health-v1");
|
|
assert.equal(summary.collectionHealth.status, "low-sample");
|
|
assert.equal(summary.collectionHealth.reason, "low_sample_window");
|
|
assert.ok(summary.collectionHealth.diagnostics.some((item) => item.code === "low_sample_window"));
|
|
assert.equal(summary.dashboard.schemaVersion, "hwlab-web-performance-dashboard-v1");
|
|
assert.equal(summary.dashboard.collectionHealth.status, "low-sample");
|
|
assert.ok(summary.dashboard.cards.some((card) => card.id === "samples" && card.value === 4));
|
|
assert.ok(summary.dashboard.trends.some((trend) => trend.id === "api-timing" && trend.points.length > 0));
|
|
assert.ok(summary.dashboard.topN.some((group) => group.id === "top-api" && group.rows.length > 0));
|
|
assert.ok(summary.workbenchCacheRows.some((row) => row.metric === "cache_hit_ratio" && row.cache === "sessions_summary" && row.p95 === 0.75));
|
|
assert.ok(summary.workbenchCacheRows.some((row) => row.metric === "db_query_avoided" && row.p95 === 0.75));
|
|
assert.ok(summary.workbenchCacheRows.some((row) => row.metric === "sessions_api_p95" && row.p95 === 0.18));
|
|
const matchedApi = summary.apiRoutes.find((row) => row.route === "/v1/agent/chat/result/:id" && row.statusClass === "2xx");
|
|
assert.equal(matchedApi?.backendEvidence?.status, "matched");
|
|
assert.equal(matchedApi?.backendEvidence?.diagnostic, "backend_low_sample");
|
|
assert.equal(matchedApi?.backendEvidence?.responseBytesP95, 4096);
|
|
assert.ok(summary.apiRoutes.some((row) => row.route === "/v1/agent/chat/result/:id" && row.metric === "api_request" && row.p95 >= 2.5));
|
|
assert.ok(summary.problems.some((row) => row.outcome === "timeout" && row.problem === "timeout" && row.sampleState === "low-sample" && row.sourceFamily === "rum_api_timing" && row.aggregationKind === "exact" && row.approximation === "exact"));
|
|
assert.equal(summary.problems.some((row) => row.outcome === "ok" && row.problem === "low-sample"), false);
|
|
assert.ok(summary.longTasks.some((row) => row.metric === "long_task" && row.problem === "ok" && row.sampleState === "low-sample" && row.tone === "source"));
|
|
assert.ok(summary.rows.every((row) => row.windowFrom && row.windowTo && row.generatedAt && row.aggregationKind === "exact" && row.approximation === "exact"));
|
|
assert.doesNotMatch(json, /trc_secret|sessionId|conversationId|threadId|prompt|api key/iu);
|
|
});
|
|
|
|
test("web performance summary explains empty recent windows when retained samples exist", () => {
|
|
let nowMs = 0;
|
|
const store = createWebPerformanceStore({
|
|
env: { HWLAB_METRICS_NAMESPACE: "hwlab-v03", HWLAB_GITOPS_TARGET: "v03" },
|
|
now: () => nowMs
|
|
});
|
|
|
|
nowMs = 1_000;
|
|
store.record({
|
|
schemaVersion: "hwlab-web-performance-v1",
|
|
page: "/workbench",
|
|
events: [{ kind: "api", metric: "api_request", route: "/v1/workbench/sessions", method: "GET", status: 200, statusClass: "2xx", outcome: "ok", valueMs: 450, traceId: "trc_old_secret" }]
|
|
} as Record<string, unknown>);
|
|
|
|
nowMs = 20 * 60 * 1000;
|
|
const recent = store.summary({ window: "5m" });
|
|
const json = JSON.stringify(recent);
|
|
|
|
assert.equal(recent.summary.sampleCount, 0);
|
|
assert.equal(recent.collectionHealth.status, "stale");
|
|
assert.equal(recent.collectionHealth.reason, "no_samples_in_window");
|
|
assert.equal(recent.collectionHealth.retainedSampleCount, 1);
|
|
assert.equal(recent.collectionHealth.lastSampleAt, null);
|
|
assert.ok(recent.collectionHealth.lastRetainedSampleAt);
|
|
assert.ok(recent.collectionHealth.diagnostics.some((item) => item.code === "no_samples_in_window"));
|
|
assert.equal(recent.dashboard.freshness.status, "stale");
|
|
assert.equal(recent.dashboard.collectionHealth.reason, "no_samples_in_window");
|
|
assert.doesNotMatch(json, /trc_old_secret|traceId|prompt|api key/iu);
|
|
});
|
|
|
|
test("web performance summary filters rows by requested recent window", () => {
|
|
let nowMs = 0;
|
|
const store = createWebPerformanceStore({
|
|
env: { HWLAB_METRICS_NAMESPACE: "hwlab-v03", HWLAB_GITOPS_TARGET: "v03" },
|
|
now: () => nowMs
|
|
});
|
|
|
|
nowMs = 1_000;
|
|
store.record({
|
|
schemaVersion: "hwlab-web-performance-v1",
|
|
page: "/workbench",
|
|
events: [{ kind: "api", metric: "api_request", route: "/v1/workbench/sessions", method: "GET", status: 200, statusClass: "2xx", outcome: "ok", valueMs: 2600, traceId: "trc_old_secret" }]
|
|
} as Record<string, unknown>);
|
|
|
|
nowMs = 20 * 60 * 1000;
|
|
store.record({
|
|
schemaVersion: "hwlab-web-performance-v1",
|
|
page: "/workbench",
|
|
events: [{ kind: "api", metric: "api_request", route: "/v1/workbench/sessions", method: "GET", status: 200, statusClass: "2xx", outcome: "ok", valueMs: 320, traceId: "trc_new_secret" }]
|
|
} as Record<string, unknown>);
|
|
|
|
const recent = store.summary({ window: "5m" });
|
|
const all = store.summary({ window: "all" });
|
|
const recentJson = JSON.stringify(recent);
|
|
const allJson = JSON.stringify(all);
|
|
|
|
assert.equal(recent.sampleWindow.id, "5m");
|
|
assert.equal(recent.summary.sampleCount, 1);
|
|
assert.equal(recent.apiRoutes[0].p95, 0.32);
|
|
assert.equal(recent.apiRoutes[0].aggregationKind, "exact");
|
|
assert.equal(recent.apiRoutes[0].approximation, "exact");
|
|
assert.equal(all.sampleWindow.id, "all");
|
|
assert.equal(all.summary.sampleCount, 2);
|
|
assert.equal(all.apiRoutes[0].p95, 2.6);
|
|
assert.equal(all.sampleWindow.windowFrom, null);
|
|
assert.doesNotMatch(recentJson, /trc_old_secret|trc_new_secret|traceId|prompt|api key/iu);
|
|
assert.doesNotMatch(allJson, /trc_old_secret|trc_new_secret|traceId|prompt|api key/iu);
|
|
});
|
|
|
|
test("web performance store accepts v2 Workbench journey and phase metrics without high-cardinality labels", () => {
|
|
const store = createWebPerformanceStore({ env: { HWLAB_METRICS_NAMESPACE: "hwlab-v03", HWLAB_GITOPS_TARGET: "v03" } });
|
|
const result = store.record({
|
|
schemaVersion: "hwlab-web-performance-v2",
|
|
page: "/workbench/sessions/ses_secret?traceId=trc_secret",
|
|
events: [
|
|
{
|
|
kind: "workbench_journey",
|
|
journey: "submit_to_first_visible",
|
|
route: "/workbench/sessions/ses_secret",
|
|
entry: "new",
|
|
backend: "agentrun-v01/codex",
|
|
transport: "sse",
|
|
visibility: "foreground",
|
|
outcome: "ok",
|
|
valueMs: 1234,
|
|
traceId: "trc_secret",
|
|
sessionId: "ses_secret",
|
|
runId: "run_secret"
|
|
},
|
|
{
|
|
kind: "workbench_event_phase",
|
|
phase: "created_to_append",
|
|
eventType: "assistant_message",
|
|
backend: "agentrun-v01/codex",
|
|
transport: "sse",
|
|
outcome: "ok",
|
|
valueMs: 42,
|
|
commandId: "cmd_secret"
|
|
},
|
|
{
|
|
kind: "workbench_journey",
|
|
journey: "submit_to_failure",
|
|
route: "/workbench/sessions/ses_secret",
|
|
entry: "new",
|
|
backend: "agentrun-v01/codex",
|
|
transport: "sse",
|
|
visibility: "foreground",
|
|
outcome: "network",
|
|
valueMs: 530,
|
|
traceId: "trc_secret"
|
|
},
|
|
{
|
|
kind: "workbench_backend_event_visible",
|
|
eventType: "tool",
|
|
backend: "agentrun-v01/codex",
|
|
transport: "sse",
|
|
outcome: "ok",
|
|
valueMs: 640,
|
|
conversationId: "cnv_secret"
|
|
}
|
|
]
|
|
} as Record<string, unknown>);
|
|
const text = store.metricsText();
|
|
|
|
assert.deepEqual(result, { accepted: 4, dropped: 0, received: 4 });
|
|
assert.match(text, /hwlab_workbench_journey_total\{[^}]*journey="submit_to_first_visible"[^}]*route="\/workbench\/sessions\/:id"[^}]*backend="agentrun-v01\/codex"/u);
|
|
assert.match(text, /hwlab_workbench_journey_total\{[^}]*journey="submit_to_failure"[^}]*outcome="network"/u);
|
|
assert.match(text, /hwlab_workbench_event_phase_duration_seconds_bucket\{[^}]*phase="created_to_append"[^}]*event_type="assistant"[^}]*le="0\.05"\} 1/u);
|
|
assert.match(text, /hwlab_workbench_backend_event_visible_latency_seconds_bucket\{[^}]*event_type="tool_call"[^}]*backend="agentrun-v01\/codex"[^}]*le="1"\} 1/u);
|
|
assert.doesNotMatch(text, /trc_secret|ses_secret|run_secret|cmd_secret|cnv_secret|traceId|sessionId|runId|commandId|conversationId|prompt|api key/iu);
|
|
});
|
|
|
|
test("web performance summary exposes Workbench p75 and low-sample diagnostics without high-cardinality labels", () => {
|
|
const store = createWebPerformanceStore({ env: { HWLAB_METRICS_NAMESPACE: "hwlab-v03", HWLAB_GITOPS_TARGET: "v03" } });
|
|
const result = store.record({
|
|
schemaVersion: "hwlab-web-performance-v2",
|
|
page: "/workbench/sessions/ses_secret?traceId=trc_secret",
|
|
events: [
|
|
{ kind: "workbench_journey", journey: "submit_to_first_visible", route: "/workbench/sessions/ses_secret", entry: "new", backend: "agentrun-v01/codex", transport: "sse", visibility: "foreground", outcome: "ok", valueMs: 1200, traceId: "trc_secret" },
|
|
{ kind: "workbench_journey", journey: "session_switch_first_visible", route: "/workbench/sessions/ses_secret", source: "rail", targetState: "running", cache: "warm", backend: "agentrun-v01/codex", transport: "rest_gap", visibility: "foreground", outcome: "ok", valueMs: 420, sessionId: "ses_secret" },
|
|
{ kind: "workbench_event_phase", phase: "created_to_append", eventType: "backend", backend: "agentrun-v01/codex", transport: "sse", outcome: "ok", valueMs: 28, commandId: "cmd_secret" },
|
|
{ kind: "workbench_backend_event_visible", eventType: "terminal", backend: "agentrun-v01/codex", transport: "sse", outcome: "ok", valueMs: 760, runId: "run_secret" }
|
|
]
|
|
} as Record<string, unknown>);
|
|
|
|
const summary = store.summary();
|
|
const json = JSON.stringify(summary);
|
|
assert.deepEqual(result, { accepted: 4, dropped: 0, received: 4 });
|
|
assert.equal(summary.summary.sampleCount, 4);
|
|
assert.equal(summary.summary.lowSampleThreshold, 5);
|
|
assert.equal(summary.collectionHealth.status, "low-sample");
|
|
assert.ok(summary.collectionHealth.sourceFamilyCounts.some((item) => item.sourceFamily === "workbench_journey"));
|
|
assert.equal(summary.dashboard.freshness.namespace, "hwlab-v03");
|
|
assert.ok(summary.dashboard.trends.some((trend) => trend.id === "workbench-experience" && trend.points.some((point) => point.label === "提交到首个可见结果")));
|
|
assert.ok(summary.dashboard.distributions.some((distribution) => distribution.id === "samples" && distribution.buckets.some((bucket) => bucket.label === "低样本")));
|
|
assert.ok(summary.workbenchJourneys.some((row) => row.metric === "submit_to_first_visible" && row.backend === "agentrun-v01/codex" && row.p75 >= row.p50 && row.lowSample === true && row.sampleState === "low-sample"));
|
|
assert.ok(summary.workbenchJourneys.some((row) => row.metric === "session_switch_first_visible" && row.transport === "detail_history"));
|
|
assert.ok(summary.workbenchEventPhases.some((row) => row.phase === "created_to_append" && row.eventType === "backend" && row.transport === "sse"));
|
|
assert.ok(summary.workbenchBackendEvents.some((row) => row.metric === "backend_event_to_visible" && row.eventType === "terminal" && row.backend === "agentrun-v01/codex"));
|
|
assert.equal(summary.summary.problemCount, 0);
|
|
assert.equal(summary.problems.some((row) => row.kind === "workbench_journey" && row.problem === "low-sample"), false);
|
|
assert.doesNotMatch(json, /trc_secret|ses_secret|run_secret|cmd_secret|traceId|sessionId|runId|commandId|conversationId|prompt|api key/iu);
|
|
});
|
|
|
|
test("web performance summary does not count low-sample Workbench errors as performance problems", () => {
|
|
const store = createWebPerformanceStore({ env: { HWLAB_METRICS_NAMESPACE: "hwlab-v03", HWLAB_GITOPS_TARGET: "v03" } });
|
|
store.record({
|
|
schemaVersion: "hwlab-web-performance-v2",
|
|
page: "/workbench/sessions/ses_secret",
|
|
events: [
|
|
{ kind: "workbench_journey", journey: "session_switch_first_visible", route: "/workbench/sessions/ses_secret", source: "deeplink", targetState: "unknown", cache: "cold", outcome: "ok", valueMs: 560, sessionId: "ses_secret" },
|
|
{ kind: "workbench_journey", journey: "session_switch_full_load", route: "/workbench/sessions/ses_secret", source: "deeplink", targetState: "unknown", cache: "cold", outcome: "error", valueMs: 620, sessionId: "ses_secret" }
|
|
]
|
|
} as Record<string, unknown>);
|
|
|
|
const summary = store.summary();
|
|
const fullLoad = summary.workbenchJourneys.find((row) => row.metric === "session_switch_full_load");
|
|
const problemCard = summary.dashboard.cards.find((card) => card.id === "problems");
|
|
|
|
assert.equal(fullLoad?.sampleState, "low-sample");
|
|
assert.equal(fullLoad?.tone, "source");
|
|
assert.equal(fullLoad?.problem, "low-sample");
|
|
assert.equal(summary.summary.problemCount, 0);
|
|
assert.equal(summary.problems.length, 0);
|
|
assert.equal(problemCard?.value, 0);
|
|
});
|
|
|
|
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({
|
|
schemaVersion: "hwlab-web-performance-v2",
|
|
page: "/workbench/sessions/ses_secret",
|
|
events: [
|
|
{ kind: "workbench_ui_event", eventType: "loading_state", loadingScope: "session_detail", state: "enter", reason: "hydrate", route: "/workbench/sessions/ses_secret", valueMs: 0, uiTraceId: "ui_secret", otelTraceId: "0123456789abcdef0123456789abcdef", sessionHash: "ses_hash", sessionId: "ses_secret" },
|
|
{ kind: "workbench_ui_event", eventType: "loading_state", loadingScope: "session_detail", state: "exit", reason: "hydrate", route: "/workbench/sessions/ses_secret", valueMs: 512, uiTraceId: "ui_secret", otelTraceId: "0123456789abcdef0123456789abcdef", sessionHash: "ses_hash", traceId: "trc_secret" },
|
|
{ kind: "workbench_ui_event", eventType: "api_request", loadingScope: "api", state: "request", reason: "api_request", route: "/v1/workbench/sessions/ses_secret/messages", method: "GET", status: 200, valueMs: 84, uiTraceId: "ui_secret" },
|
|
{ kind: "workbench_ui_event", eventType: "sse_lifecycle", loadingScope: "sse", state: "error", reason: "sse_error", route: "/v1/workbench/events?sessionId=ses_secret", outcome: "network", valueMs: 0, uiTraceId: "ui_secret" }
|
|
]
|
|
} as Record<string, unknown>);
|
|
const text = store.metricsText();
|
|
|
|
assert.deepEqual(result, { accepted: 4, dropped: 0, received: 4 });
|
|
assert.match(text, /hwlab_workbench_ui_event_total\{[^}]*event_type="loading_state"[^}]*scope="session_detail"[^}]*state="enter"[^}]*reason="hydrate"/u);
|
|
assert.match(text, /hwlab_workbench_ui_event_duration_seconds_bucket\{[^}]*event_type="api_request"[^}]*route="\/v1\/workbench\/sessions\/:id\/messages"[^}]*le="0\.1"\} 1/u);
|
|
assert.match(text, /hwlab_workbench_ui_event_total\{[^}]*event_type="sse_lifecycle"[^}]*state="error"[^}]*outcome="network"/u);
|
|
assert.doesNotMatch(text, /ses_secret|trc_secret|ui_secret|0123456789abcdef|sessionId|traceId|otelTraceId|prompt|api key/iu);
|
|
});
|
|
|
|
test("Workbench UI OTel spans include timeout activity diagnostics without raw trace ids", async () => {
|
|
const calls: OtelExportRequestForTest[] = [];
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (_url: string | URL | Request, init?: RequestInit) => {
|
|
calls.push(JSON.parse(String(init?.body ?? "{}")) as OtelExportRequestForTest);
|
|
return new Response(null, { status: 200 });
|
|
};
|
|
try {
|
|
const result = await emitWorkbenchUiOtelSpans({
|
|
schemaVersion: "hwlab-web-performance-v2",
|
|
page: "/workbench/sessions/ses_secret",
|
|
events: [{
|
|
kind: "workbench_ui_event",
|
|
eventType: "api_request",
|
|
loadingScope: "api",
|
|
state: "request",
|
|
reason: "api_request",
|
|
route: "/v1/workbench/turns/trc_secret",
|
|
method: "GET",
|
|
status: 0,
|
|
outcome: "timeout",
|
|
valueMs: 8001,
|
|
uiTraceId: "ui_secret",
|
|
otelTraceId: "0123456789abcdef0123456789abcdef",
|
|
timeoutName: "workbench turn",
|
|
activityWaitingFor: "code-agent",
|
|
activityLastEventLabel: "realtime:heartbeat",
|
|
activityIdleMs: 13042,
|
|
eventCount: 1,
|
|
contractVersion: "workbench-sync-v1",
|
|
realtimeAuthority: "workbench-realtime-authority-v2",
|
|
sinceOutboxSeq: 7,
|
|
syncCursorOutboxSeq: 12,
|
|
entityFamilyCount: 1,
|
|
entityFamilies: "turn",
|
|
maxEntityVersion: 3,
|
|
projectionRevision: "rev_sync",
|
|
terminalSeal: true,
|
|
detailProjection: false,
|
|
traceId: "trc_secret"
|
|
}]
|
|
} as Record<string, unknown>, { OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "http://otel.example/v1/traces" });
|
|
|
|
assert.equal(result.submitted, 1);
|
|
assert.equal(calls.length, 1);
|
|
const span = calls[0].resourceSpans[0].scopeSpans[0].spans[0];
|
|
const attrs = Object.fromEntries(span.attributes.map((item) => [item.key, item.value.stringValue ?? (item.value.intValue !== undefined ? Number(item.value.intValue) : item.value.boolValue)]));
|
|
assert.equal(attrs["http.route"], "/v1/workbench/turns/:id");
|
|
assert.equal(attrs["workbench.ui.timeout_name"], "workbench turn");
|
|
assert.equal(attrs["workbench.ui.activity_waiting_for"], "code-agent");
|
|
assert.equal(attrs["workbench.ui.activity_last_event_label"], "realtime:heartbeat");
|
|
assert.equal(attrs["workbench.ui.activity_idle_ms"], 13042);
|
|
assert.equal(attrs["workbench.sync.event_count"], 1);
|
|
assert.equal(attrs["workbench.sync.contract_version"], "workbench-sync-v1");
|
|
assert.equal(attrs["workbench.realtime.authority"], "workbench-realtime-authority-v2");
|
|
assert.equal(attrs["workbench.sync.since_outbox_seq"], 7);
|
|
assert.equal(attrs["workbench.sync.cursor_outbox_seq"], 12);
|
|
assert.equal(attrs["workbench.sync.entity_family_count"], 1);
|
|
assert.equal(attrs["workbench.sync.entity_families"], "turn");
|
|
assert.equal(attrs["workbench.sync.max_entity_version"], 3);
|
|
assert.equal(attrs["workbench.projection.revision"], "rev_sync");
|
|
assert.equal(attrs["workbench.terminal.seal"], true);
|
|
assert.equal(attrs["workbench.detail.projection"], false);
|
|
assert.doesNotMatch(JSON.stringify(calls[0]), /trc_secret|ses_secret|prompt|api key/iu);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
interface OtelExportRequestForTest {
|
|
resourceSpans: Array<{
|
|
scopeSpans: Array<{
|
|
spans: Array<{
|
|
attributes: Array<{
|
|
key: string;
|
|
value: { stringValue?: string; intValue?: string | number; boolValue?: boolean };
|
|
}>;
|
|
}>;
|
|
}>;
|
|
}>;
|
|
}
|
|
|
|
test("web performance store drops unsupported Workbench labels and counts series limit drops", () => {
|
|
const store = createWebPerformanceStore({ env: { HWLAB_METRICS_NAMESPACE: "hwlab-v03", HWLAB_GITOPS_TARGET: "v03" }, maxSeries: 1 });
|
|
const first = store.record({
|
|
schemaVersion: "hwlab-web-performance-v2",
|
|
page: "/workbench",
|
|
events: [{ kind: "workbench_journey", journey: "session_switch_first_visible", route: "/workbench/sessions/ses_first", valueMs: 250, backend: "agentrun-v01/codex", transport: "sse" }]
|
|
});
|
|
const second = store.record({
|
|
schemaVersion: "hwlab-web-performance-v2",
|
|
page: "/workbench",
|
|
events: [
|
|
{ kind: "workbench_journey", journey: "session_switch_full_load", route: "/workbench/sessions/ses_second", valueMs: 900, backend: "run_abcdefghijklmnopqrstuvwxyz", transport: "sse" },
|
|
{ kind: "workbench_journey", journey: "trace_trc_secret", route: "/workbench/sessions/ses_third", valueMs: 100, backend: "agentrun-v01/codex", transport: "sse" }
|
|
]
|
|
});
|
|
const text = store.metricsText();
|
|
|
|
assert.deepEqual(first, { accepted: 1, dropped: 0, received: 1 });
|
|
assert.deepEqual(second, { accepted: 0, dropped: 2, received: 2 });
|
|
assert.match(text, /hwlab_webui_performance_dropped_total\{[^}]*reason="series_limit"\} 1/u);
|
|
assert.match(text, /hwlab_webui_performance_dropped_total\{[^}]*reason="invalid_event"\} 1/u);
|
|
assert.doesNotMatch(text, /ses_second|ses_third|trace_trc_secret|run_abcdefghijklmnopqrstuvwxyz/u);
|
|
});
|
|
|
|
test("web performance store filters observability self-noise", () => {
|
|
const store = createWebPerformanceStore({ env: { HWLAB_METRICS_NAMESPACE: "hwlab-v02", HWLAB_GITOPS_TARGET: "v02" } });
|
|
const result = store.record({
|
|
schemaVersion: "hwlab-web-performance-v1",
|
|
page: "/performance",
|
|
events: [
|
|
{ kind: "web_vital", metric: "lcp", route: "/performance", valueMs: 5900 },
|
|
{ kind: "long_task", metric: "long_task", route: "/performance", valueMs: 120 },
|
|
{ kind: "api", metric: "api_request", route: "/v1/web-performance/summary", method: "GET", status: 200, statusClass: "2xx", outcome: "ok", valueMs: 620 },
|
|
{ kind: "api", metric: "api_request", route: "/v1/skills", method: "GET", status: 200, statusClass: "2xx", outcome: "ok", valueMs: 780 }
|
|
]
|
|
});
|
|
const summary = store.summary();
|
|
const rowsJson = JSON.stringify({ apiRoutes: summary.apiRoutes, webVitals: summary.webVitals, longTasks: summary.longTasks, problems: summary.problems, rows: summary.rows });
|
|
assert.deepEqual(result, { accepted: 0, dropped: 4, received: 4 });
|
|
assert.equal(summary.summary.sampleCount, 0);
|
|
assert.doesNotMatch(rowsJson, /\/performance|\/v1\/web-performance/u);
|
|
});
|
|
|
|
test("web performance route templates keep metrics low-cardinality", () => {
|
|
assert.equal(webPerformanceRouteTemplate("/v1/workbench/sessions/ses_abcdef?includeMessages=false"), "/v1/workbench/sessions/:id?includeMessages=false");
|
|
assert.equal(webPerformanceRouteTemplate("/v1/workbench/sessions/ses_abcdef/messages?traceId=trc_secret"), "/v1/workbench/sessions/:id/messages");
|
|
assert.equal(webPerformanceRouteTemplate("#/skills"), "/skills");
|
|
});
|
|
|
|
test("cloud api ingests WebUI performance samples and exposes metrics only on loopback host", async () => {
|
|
const server = createCloudApiServer({
|
|
accessController: {
|
|
required: false,
|
|
async authenticate() { return { ok: true, actor: { id: "usr_web_performance", role: "admin" }, session: { id: "uss_web_performance" } }; }
|
|
},
|
|
env: { HWLAB_METRICS_NAMESPACE: "hwlab-v02", HWLAB_GITOPS_TARGET: "v02" }
|
|
});
|
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
try {
|
|
const address = server.address();
|
|
assert.ok(address && typeof address === "object");
|
|
const baseUrl = `http://127.0.0.1:${address.port}`;
|
|
const ingest = await fetch(`${baseUrl}/v1/web-performance`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({
|
|
schemaVersion: "hwlab-web-performance-v1",
|
|
page: "/workspace",
|
|
events: [{ kind: "web_vital", metric: "lcp", route: "/workspace", valueMs: 900 }]
|
|
})
|
|
});
|
|
assert.equal(ingest.status, 202);
|
|
const metrics = await fetch(`${baseUrl}/v1/web-performance/metrics`);
|
|
const text = await metrics.text();
|
|
assert.equal(metrics.status, 200);
|
|
assert.match(text, /hwlab_webui_performance_duration_seconds_bucket\{[^}]*metric="lcp"/u);
|
|
|
|
const publicHost = await fetch(`${baseUrl}/v1/web-performance/metrics`, { headers: { host: "74.48.78.17:19667" } });
|
|
assert.equal(publicHost.status, 404);
|
|
|
|
const summary = await fetch(`${baseUrl}/v1/web-performance/summary`);
|
|
const body = await summary.json();
|
|
assert.equal(summary.status, 200);
|
|
assert.equal(body.route, "/v1/web-performance/summary");
|
|
assert.equal(body.summary.sampleCount, 1);
|
|
} finally {
|
|
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
}
|
|
});
|