From 6e9c13f032ef7408d1147f266c34fc95e4258f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Tue, 23 Jun 2026 15:36:21 +0800 Subject: [PATCH] fix: capture direct provider trace headers Trace direct provider calls from the effective Request metadata so headers carried by Request input are sanitized and persisted instead of being dropped when init.headers is empty. Tested: bun test src/server/__tests__/trace-capture.test.ts src/server/__tests__/proxy-network-settings.test.ts Tested: bun run check:server Confidence: high Scope-risk: narrow --- src/server/__tests__/trace-capture.test.ts | 46 ++++++++++++++++++++++ src/services/api/dumpPrompts.ts | 37 ++++++++++------- 2 files changed, 69 insertions(+), 14 deletions(-) diff --git a/src/server/__tests__/trace-capture.test.ts b/src/server/__tests__/trace-capture.test.ts index 290523b2..5fe9e134 100644 --- a/src/server/__tests__/trace-capture.test.ts +++ b/src/server/__tests__/trace-capture.test.ts @@ -565,6 +565,52 @@ describe('trace capture service', () => { } }) + test('captures direct provider headers when fetch input is a Request', async () => { + const originalFetch = globalThis.fetch + const originalTraceEnv = process.env.CC_HAHA_TRACE_API_CALLS + process.env.CC_HAHA_TRACE_API_CALLS = '1' + try { + globalThis.fetch = (async () => new Response( + JSON.stringify({ id: 'msg-request-input', content: [{ type: 'text', text: 'ok' }] }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }, + )) as typeof fetch + + const traceFetch = createDumpPromptsFetch('agent-direct-request-input', { + traceSessionId: 'session-direct-request-input', + querySource: 'test_query', + }) + const requestInput = new Request('https://sub2api.example.test/v1/messages', { + method: 'POST', + headers: { + Authorization: 'Bearer sk-direct-header-secret', + 'Content-Type': 'application/json', + }, + }) + + await traceFetch(requestInput, { + method: 'POST', + body: JSON.stringify({ model: 'gpt-5.5', messages: [{ role: 'user', content: 'request input' }] }), + }) + + const trace = await waitForTrace( + 'session-direct-request-input', + (snapshot) => Boolean(snapshot.calls[0]?.response) && snapshot.events.length >= 2, + ) + expect(trace.summary.apiCalls).toBe(1) + expect(trace.calls[0].request.headers.authorization).toBe('[redacted]') + expect(trace.calls[0].request.headers['content-type']).toBe('application/json') + expect(trace.calls[0].request.body.preview).toContain('request input') + expect(trace.calls[0].response.body.preview).toContain('msg-request-input') + } finally { + globalThis.fetch = originalFetch + if (originalTraceEnv === undefined) delete process.env.CC_HAHA_TRACE_API_CALLS + else process.env.CC_HAHA_TRACE_API_CALLS = originalTraceEnv + } + }) + test('captures direct provider fetch failures without changing thrown behavior', async () => { const originalFetch = globalThis.fetch const originalTraceEnv = process.env.CC_HAHA_TRACE_API_CALLS diff --git a/src/services/api/dumpPrompts.ts b/src/services/api/dumpPrompts.ts index be0bfb94..8f44fa7e 100644 --- a/src/services/api/dumpPrompts.ts +++ b/src/services/api/dumpPrompts.ts @@ -266,6 +266,15 @@ export function createDumpPromptsFetch( })(), } : init + const traceRequest = timestamp + ? new Request(input, requestInit) + : undefined + const traceRequestUrl = traceRequest?.url ?? requestUrl + const traceRequestMethod = traceRequest?.method ?? init?.method ?? 'POST' + const traceRequestHeaders = (traceRequest?.headers ?? requestInit?.headers) as + | Headers + | Record + | undefined const traceModel = extractModelFromRequestBody(traceRequestBody) const traceCallId = timestamp && traceEnabled && !isProviderProxyTrace @@ -283,9 +292,9 @@ export function createDumpPromptsFetch( status: 'pending', startedAt: timestamp, request: { - method: init?.method ?? 'POST', - url: requestUrl, - headers: requestInit?.headers as Headers | Record | undefined, + method: traceRequestMethod, + url: traceRequestUrl, + headers: traceRequestHeaders, bodySnapshot: createRequestPendingSnapshot(traceRequestBody), }, metadata: { @@ -303,7 +312,7 @@ export function createDumpPromptsFetch( severity: 'info', title: 'API call started', metadata: { - url: requestUrl, + url: traceRequestUrl, }, }) } @@ -328,9 +337,9 @@ export function createDumpPromptsFetch( completedAt, durationMs: Date.now() - traceStartedAtMs, request: { - method: init?.method ?? 'POST', - url: requestUrl, - headers: requestInit?.headers as Headers | Record | undefined, + method: traceRequestMethod, + url: traceRequestUrl, + headers: traceRequestHeaders, bodySnapshot: createRequestPendingSnapshot(traceRequestBody), }, error: err, @@ -351,7 +360,7 @@ export function createDumpPromptsFetch( title: 'API call failed', message: err instanceof Error ? err.message : String(err), metadata: { - url: requestUrl, + url: traceRequestUrl, ...(aborted ? { aborted: true } : {}), }, }) @@ -372,9 +381,9 @@ export function createDumpPromptsFetch( model: traceModel, startedAt: timestamp, request: { - method: init?.method ?? 'POST', - url: requestUrl, - headers: requestInit?.headers as Headers | Record | undefined, + method: traceRequestMethod, + url: traceRequestUrl, + headers: traceRequestHeaders, body: traceRequestBody, }, } @@ -420,7 +429,7 @@ export function createDumpPromptsFetch( title: 'API call completed', metadata: { status: response.status, - url: requestUrl, + url: traceRequestUrl, }, }) return @@ -455,7 +464,7 @@ export function createDumpPromptsFetch( message: abortError.message, metadata: { status: response.status, - url: requestUrl, + url: traceRequestUrl, durationMs, aborted: true, }, @@ -472,7 +481,7 @@ export function createDumpPromptsFetch( message: captureFailure instanceof Error ? captureFailure.message : String(captureFailure), metadata: { status: response.status, - url: requestUrl, + url: traceRequestUrl, }, }) // The clone shares the upstream source with the SDK's branch, so a