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