From 3ea90d75967125b84cbbf12bb6e403c464d10f45 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 2 Jul 2026 16:17:29 +0000 Subject: [PATCH] fix(workbench): surface sse flush diagnostics --- .../scripts/workbench-performance.test.ts | 78 ++++++++++++++++++- ...rkbench-message-projection-runtime.test.ts | 6 ++ web/hwlab-cloud-web/src/stores/workbench.ts | 29 +++++-- .../src/utils/workbench-performance.ts | 17 ++++ 4 files changed, 120 insertions(+), 10 deletions(-) diff --git a/web/hwlab-cloud-web/scripts/workbench-performance.test.ts b/web/hwlab-cloud-web/scripts/workbench-performance.test.ts index 453e8c18..bf5e03a9 100644 --- a/web/hwlab-cloud-web/scripts/workbench-performance.test.ts +++ b/web/hwlab-cloud-web/scripts/workbench-performance.test.ts @@ -142,6 +142,56 @@ test("Workbench SSE flush diagnostics expose bounded queue cost fields", () => { } }); +test("Workbench SSE flush diagnostics request prompt web-performance delivery", async () => { + resetWorkbenchPerformanceForTest(); + const timers: { delay: number; handler: () => void }[] = []; + const posts: { url: string; body: string }[] = []; + const restoreBrowserRuntime = installBrowserProbeRuntime({ + setTimeout: (handler, timeout) => { + timers.push({ delay: Number(timeout ?? 0), handler: () => invokeTimerHandler(handler) }); + return timers.length; + }, + clearTimeout: () => undefined, + fetch: (input, init) => { + posts.push({ url: String(input), body: String(init?.body ?? "") }); + return Promise.resolve(new Response(null, { status: 204 })); + } + }); + try { + recordWorkbenchRuntimeDiagnostic({ + module: "workbench-events", + sessionId: "ses_secret", + traceId: "trc_secret", + outcome: "ok", + diagnostic: { + code: "workbench_sse_flush", + eventCount: 3, + deliveredCount: 2, + dropped: 1, + chunkCount: 2, + flushDurationMs: 9, + maxItemsPerChunk: 4, + maxChunkMs: 8 + } + }); + + const priorityTimer = timers.find((timer) => timer.delay === 250); + assert.ok(priorityTimer); + priorityTimer.handler(); + await Promise.resolve(); + + assert.equal(posts.length, 1); + assert.equal(posts[0]?.url, "/v1/web-performance"); + assert.match(posts[0]?.body ?? "", /workbench_sse_flush/u); + assert.match(posts[0]?.body ?? "", /"eventCount":3/u); + assert.match(posts[0]?.body ?? "", /"maxChunkMs":8/u); + assert.equal(drainWorkbenchPerformanceEventsForTest().length, 0); + } finally { + resetWorkbenchPerformanceForTest(); + restoreBrowserRuntime(); + } +}); + test("Workbench API timing enriches from ResourceTiming and clears the browser buffer", () => { resetWorkbenchPerformanceForTest(); let clearCount = 0; @@ -250,18 +300,19 @@ function traceOnlyAgentMessage(sessionId: string, traceId: string, events: Trace }; } -function installBrowserProbeRuntime(options: { performance?: FakePerformance } = {}): () => void { +function installBrowserProbeRuntime(options: { performance?: FakePerformance; setTimeout?: BrowserSetTimeout; clearTimeout?: BrowserClearTimeout; fetch?: BrowserFetch } = {}): () => void { const previousWindow = Object.getOwnPropertyDescriptor(globalThis, "window"); const previousDocument = Object.getOwnPropertyDescriptor(globalThis, "document"); const previousPerformance = Object.getOwnPropertyDescriptor(globalThis, "performance"); + const previousFetch = Object.getOwnPropertyDescriptor(globalThis, "fetch"); Object.defineProperty(globalThis, "window", { configurable: true, value: { location: { href: "https://hwlab.example.test/workbench", pathname: "/workbench", hash: "" }, crypto: globalThis.crypto, addEventListener: () => undefined, - setTimeout: globalThis.setTimeout, - clearTimeout: globalThis.clearTimeout + setTimeout: options.setTimeout ?? globalThis.setTimeout, + clearTimeout: options.clearTimeout ?? globalThis.clearTimeout } }); Object.defineProperty(globalThis, "document", { @@ -277,13 +328,24 @@ function installBrowserProbeRuntime(options: { performance?: FakePerformance } = value: options.performance }); } + if (options.fetch) { + Object.defineProperty(globalThis, "fetch", { + configurable: true, + value: options.fetch + }); + } return () => { restoreGlobalProperty("window", previousWindow); restoreGlobalProperty("document", previousDocument); restoreGlobalProperty("performance", previousPerformance); + restoreGlobalProperty("fetch", previousFetch); }; } +type BrowserSetTimeout = (handler: TimerHandler, timeout?: number, ...arguments_: unknown[]) => number; +type BrowserClearTimeout = (handle?: number) => void; +type BrowserFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise; + interface FakePerformance { timeOrigin: number; now: () => number; @@ -292,6 +354,14 @@ interface FakePerformance { clearResourceTimings?: () => void; } +function invokeTimerHandler(handler: TimerHandler): void { + if (typeof handler === "function") { + handler(); + return; + } + throw new TypeError("string timer handlers are not supported by this test runtime"); +} + function resourceTimingEntry(input: { name: string; startTime: number; duration: number; responseStart: number; responseEnd: number }): PerformanceResourceTiming { return { name: input.name, @@ -310,7 +380,7 @@ function resourceTimingEntry(input: { name: string; startTime: number; duration: } as unknown as PerformanceResourceTiming; } -function restoreGlobalProperty(key: "window" | "document" | "performance", descriptor: PropertyDescriptor | undefined): void { +function restoreGlobalProperty(key: "window" | "document" | "performance" | "fetch", descriptor: PropertyDescriptor | undefined): void { if (descriptor) { Object.defineProperty(globalThis, key, descriptor); return; diff --git a/web/hwlab-cloud-web/src/stores/workbench-message-projection-runtime.test.ts b/web/hwlab-cloud-web/src/stores/workbench-message-projection-runtime.test.ts index dd4c785b..d1e3616b 100644 --- a/web/hwlab-cloud-web/src/stores/workbench-message-projection-runtime.test.ts +++ b/web/hwlab-cloud-web/src/stores/workbench-message-projection-runtime.test.ts @@ -138,6 +138,7 @@ test("workbench active terminal paths seal final response from turn authority", const realtimeTurnBlock = source.slice(source.indexOf("function applyRealtimeTurnSnapshot"), source.indexOf("async function refreshTerminalTraceFromRest")); const terminalRestBlock = source.slice(source.indexOf("async function refreshTerminalTraceFromRest"), source.indexOf("function installRealtimeVisibilityHandler")); const completeBlock = source.slice(source.indexOf("function completeTrace"), source.indexOf("async function hydrateTerminalMessageDiagnostics")); + const realtimeSessionDetailBlock = source.slice(source.indexOf("async function refreshRealtimeSessionFromRest"), source.indexOf("function completeTrace")); const loadBlock = source.slice(source.indexOf("async function loadWorkbenchSession"), source.indexOf("async function sealRestoredActiveTurnMessages")); const restoreSealBlock = source.slice(source.indexOf("async function sealRestoredActiveTurnMessages"), source.indexOf("function reattachRestoredActiveTrace")); @@ -150,7 +151,12 @@ test("workbench active terminal paths seal final response from turn authority", assert.match(terminalRestBlock, /traceProjectionIsTerminalSealed\(id, ownerMessagesBeforeProjection\)[\s\S]*return/u); assert.match(completeBlock, /projectTurnAuthorityToMessages\(traceId, result, "complete-trace"\)/u); assert.match(completeBlock, /traceProjectionIsTerminalSealed\(traceId, serverState\.value\.messagesBySessionId\[ownerSessionId\] \?\? \[\]\)[\s\S]*scheduleSessionListRefresh/u); + assert.match(realtimeSessionDetailBlock, /traceIdFromRealtimeRefreshReason\(reason\)/u); + assert.match(realtimeSessionDetailBlock, /traceTerminalBodyIsVisible\(traceId, id\)[\s\S]*terminal_low_priority_session_detail_skip[\s\S]*return/u); + assert.match(realtimeSessionDetailBlock, /traceTerminalBodyIsVisible\(traceId, id\)[\s\S]*terminal_low_priority_session_detail_apply_skip[\s\S]*return/u); assert.match(loadBlock, /const messageLimit = sessionMessageProjectionWindowLimit\(\);/u); + assert.match(loadBlock, /sessionFromWorkbenchSession\(detail\.data\?\.session, \{ includeMessages: false \}\)/u); + assert.match(loadBlock, /const fallbackMessages = seed\?\.sessionId === id \? seed\.messages \?\? \[\] : \[\];/u); assert.doesNotMatch(loadBlock, /limit:\s*100/u); assert.doesNotMatch(restoreSealBlock, /Promise\.all/u); assert.doesNotMatch(restoreSealBlock, /force:\s*true/u); diff --git a/web/hwlab-cloud-web/src/stores/workbench.ts b/web/hwlab-cloud-web/src/stores/workbench.ts index 0f3f6cb1..17dc17b4 100644 --- a/web/hwlab-cloud-web/src/stores/workbench.ts +++ b/web/hwlab-cloud-web/src/stores/workbench.ts @@ -1353,10 +1353,21 @@ export const useWorkbenchStore = defineStore("workbench", () => { async function refreshRealtimeSessionFromRest(sessionId: string, reason: string): Promise { const id = normalizeWorkbenchSessionId(sessionId); if (!id || id !== activeSessionId.value) return; + const traceId = traceIdFromRealtimeRefreshReason(reason); + if (traceTerminalBodyIsVisible(traceId, id)) { + recordWorkbenchRuntimeDiagnostic({ module: "workbench-terminal-priority", sessionId: id, traceId, outcome: "ok", diagnostic: { code: "terminal_low_priority_session_detail_skip", reason, source: "realtime-session-detail", valuesRedacted: true } }); + return; + } recordActivity(reason); const existing = sessions.value.find((item) => item.sessionId === id) ?? null; const selected = await loadWorkbenchSession(id, existing); - if (selected && activeSessionId.value === id) applySelectedSessionDetail(selected, "system"); + if (selected && activeSessionId.value === id) { + if (traceTerminalBodyIsVisible(traceId, id)) { + recordWorkbenchRuntimeDiagnostic({ module: "workbench-terminal-priority", sessionId: id, traceId, outcome: "ok", diagnostic: { code: "terminal_low_priority_session_detail_apply_skip", reason, source: "realtime-session-detail", valuesRedacted: true } }); + return; + } + applySelectedSessionDetail(selected, "system"); + } } function completeTrace(traceId: string, result: AgentChatResultResponse, options: { forceRead?: boolean } = {}): void { @@ -1695,14 +1706,15 @@ export const useWorkbenchStore = defineStore("workbench", () => { const eagerMessages = normalizedRequestId ? workbenchColadaQueries.fetchSessionMessages(normalizedRequestId, { limit: messageLimit, force: true, minIntervalMs: runtimePolicy.workbenchSessionMessagesMinRefreshMs }) : null; const detail = await workbenchColadaQueries.fetchSession(requestId, { force: true, minIntervalMs: runtimePolicy.workbenchSessionDetailMinRefreshMs }); if (!detail.ok) return null; - const detailSession = sessionFromWorkbenchSession(detail.data?.session); + const detailSession = sessionFromWorkbenchSession(detail.data?.session, { includeMessages: false }); const id = detailSession?.sessionId ?? normalizedRequestId ?? seed?.sessionId; if (!id) return null; const messages = eagerMessages && id === normalizedRequestId ? await eagerMessages : await workbenchColadaQueries.fetchSessionMessages(id, { limit: messageLimit, force: true, minIntervalMs: runtimePolicy.workbenchSessionMessagesMinRefreshMs }); - const base = detailSession ?? seed; + const fallbackMessages = seed?.sessionId === id ? seed.messages ?? [] : []; + const base = detailSession ? { ...detailSession, messages: fallbackMessages } : seed; if (!base) return null; const page = messages.ok ? messages.data : null; - const pageMessages = Array.isArray(page?.messages) ? await sealRestoredActiveTurnMessages(page.messages.map((message) => normalizeChatMessage(message as ChatMessage))) : base.messages; + const pageMessages = Array.isArray(page?.messages) ? await sealRestoredActiveTurnMessages(page.messages.map((message) => normalizeChatMessage(message as ChatMessage))) : fallbackMessages; return { ...base, sessionId: id, messages: pageMessages, messageCount: page?.total ?? pageMessages?.length ?? base.messageCount }; } @@ -1744,7 +1756,7 @@ function workbenchSessionsFromPayload(payload: unknown): WorkbenchSessionRecord[ return sessions.map((item) => sessionFromWorkbenchSession(item)).filter((item): item is WorkbenchSessionRecord => Boolean(item)); } -function sessionFromWorkbenchSession(value: unknown): WorkbenchSessionRecord | null { +function sessionFromWorkbenchSession(value: unknown, options: { includeMessages?: boolean } = {}): WorkbenchSessionRecord | null { const record = recordValue(value); const sessionId = normalizeWorkbenchSessionId(record?.sessionId ?? record?.id); if (!sessionId) return null; @@ -1758,10 +1770,15 @@ function sessionFromWorkbenchSession(value: unknown): WorkbenchSessionRecord | n lastTraceId: firstNonEmptyString(record?.lastTraceId), messageCount: firstFiniteNumber(record?.messageCount) ?? undefined, firstUserMessagePreview: firstNonEmptyString(record?.firstUserMessagePreview), - messages: Array.isArray(record?.messages) ? record.messages.map((message) => normalizeChatMessage(message as ChatMessage)) : undefined + messages: options.includeMessages === false ? undefined : Array.isArray(record?.messages) ? record.messages.map((message) => normalizeChatMessage(message as ChatMessage)) : undefined }; } +function traceIdFromRealtimeRefreshReason(reason: string): string | null { + const match = /(?:^|[:\s])((?:trc|trace)[_-][A-Za-z0-9_-]+)/u.exec(reason); + return firstNonEmptyString(match?.[1]); +} + function routeSelectedSessionRecord(sessionId: string | null | undefined, loadingSessionId: string | null | undefined, errorText: string | null | undefined, projected: WorkbenchSessionRecord | null): WorkbenchSessionRecord | null { if (projected) return null; const id = normalizeWorkbenchSessionId(sessionId); diff --git a/web/hwlab-cloud-web/src/utils/workbench-performance.ts b/web/hwlab-cloud-web/src/utils/workbench-performance.ts index e980df8f..15b933a6 100644 --- a/web/hwlab-cloud-web/src/utils/workbench-performance.ts +++ b/web/hwlab-cloud-web/src/utils/workbench-performance.ts @@ -146,6 +146,7 @@ interface LoadingStateRecord { const SCHEMA_VERSION = "hwlab-web-performance-v2"; const FLUSH_INTERVAL_MS = 10_000; +const PRIORITY_FLUSH_DELAY_MS = 250; const FLUSH_BATCH_THRESHOLD = 120; const MAX_QUEUE = 300; const submitJourneys = new Map(); @@ -157,6 +158,7 @@ let queue: WorkbenchPerformanceEvent[] = []; let installed = false; let resourceTimingBufferConfigured = false; let flushTimer: number | null = null; +let priorityFlushTimer: number | null = null; let uiTrace: WorkbenchUiTrace | null = null; export function currentWorkbenchUiTrace(): WorkbenchUiTrace | null { @@ -388,6 +390,7 @@ export function recordWorkbenchRuntimeDiagnostic(input: { module?: string | null maxItemsPerChunk: diagnosticNumber(diagnostic, "maxItemsPerChunk"), maxChunkMs: diagnosticNumber(diagnostic, "maxChunkMs") }); + if (isSseFlush) schedulePriorityFlush(); } export function startWorkbenchOpenJourney(input: { route?: string; cache?: "warm" | "cold" | "unknown"; authState?: "warm" | "login_redirect" | "unknown" } = {}): void { @@ -578,6 +581,8 @@ export function resetWorkbenchPerformanceForTest(): void { uiTrace = null; if (flushTimer !== null && hasBrowserTimerRuntime()) window.clearTimeout(flushTimer); flushTimer = null; + if (priorityFlushTimer !== null && hasBrowserTimerRuntime()) window.clearTimeout(priorityFlushTimer); + priorityFlushTimer = null; installed = false; resourceTimingBufferConfigured = false; } @@ -617,11 +622,23 @@ function scheduleFlush(): void { flushTimer = window.setTimeout(() => flushWorkbenchPerformance(false), FLUSH_INTERVAL_MS); } +function schedulePriorityFlush(): void { + if (!hasBrowserTimerRuntime() || priorityFlushTimer !== null) return; + priorityFlushTimer = window.setTimeout(() => { + priorityFlushTimer = null; + flushWorkbenchPerformance(false); + }, PRIORITY_FLUSH_DELAY_MS); +} + function flushWorkbenchPerformance(useBeacon: boolean): void { if (flushTimer !== null && hasBrowserTimerRuntime()) { window.clearTimeout(flushTimer); flushTimer = null; } + if (priorityFlushTimer !== null && hasBrowserTimerRuntime()) { + window.clearTimeout(priorityFlushTimer); + priorityFlushTimer = null; + } if (!queue.length || typeof window === "undefined") return; const events = queue.splice(0, queue.length).map(enrichWorkbenchEventForFlush); const body = JSON.stringify({ schemaVersion: SCHEMA_VERSION, serviceId: "hwlab-cloud-web", page: pageRoute(), events });