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
This commit is contained in:
程序员阿江(Relakkes) 2026-06-23 15:36:21 +08:00
parent be64e16621
commit 6e9c13f032
2 changed files with 69 additions and 14 deletions

View File

@ -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

View File

@ -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<string, string>
| 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<string, string> | 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<string, string> | 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<string, string> | 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