Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ export function instrumentFetcher(fetchFn: Fetcher['fetch']): Fetcher['fetch'] {
return fetchFn(requestWithTracing);
}

if (init instanceof Request) {
// `init` is itself a Request (the `stub.fetch(url, request)` Workers idiom). A Request's
// `method`, `headers`, `body` and the rest are prototype getters, not own properties, so the
// object spread below would drop them and the call would reach the fetcher as a bare GET with
// no headers or body. Rebuild through the Request constructor, which reads those members by
// property access, then layer the tracing headers (which already include the request's own
// headers) on top.
const request = new Request(input, init);

if (!headers) {
return fetchFn(request);
}

return fetchFn(new Request(request, { headers: headers as HeadersInit }));
}

const mergedInit = {
...init,
...(headers ? { headers } : {}),
Expand Down
46 changes: 46 additions & 0 deletions packages/cloudflare/test/utils/instrumentFetcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,52 @@ describe('instrumentFetcher', () => {
expect(headers.get('baggage')).toBe('sentry-environment=production');
});

it('forwards method, headers and body when init is a Request', async () => {
getTraceDataMock.mockReturnValue({
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
baggage: 'sentry-environment=production',
});

const mockFetch = vi.fn().mockResolvedValue(new Response('ok'));
const wrapped = instrumentFetcher(mockFetch);

// The `stub.fetch(url, request)` Workers idiom: `init` is itself a Request.
const incoming = new Request('https://example.com/room/ABC/websocket', {
method: 'POST',
headers: { Upgrade: 'websocket', 'Content-Type': 'application/json' },
body: '{"a":1}',
});

await wrapped('https://internal/websocket', incoming as unknown as RequestInit);

expect(mockFetch).toHaveBeenCalledTimes(1);
const forwarded = mockFetch.mock.calls[0]![0] as Request;
expect(forwarded.method).toBe('POST');
expect(forwarded.headers.get('upgrade')).toBe('websocket');
expect(forwarded.headers.get('content-type')).toBe('application/json');
expect(forwarded.headers.get('sentry-trace')).toBe('12345678901234567890123456789012-1234567890123456-1');
expect(await forwarded.text()).toBe('{"a":1}');
});

it('forwards method and body when init is a Request and tracing is disabled', async () => {
getTraceDataMock.mockReturnValue({});

const mockFetch = vi.fn().mockResolvedValue(new Response('ok'));
const wrapped = instrumentFetcher(mockFetch);

const incoming = new Request('https://example.com/upload', {
method: 'POST',
body: 'payload',
});

await wrapped('https://internal/upload', incoming as unknown as RequestInit);

expect(mockFetch).toHaveBeenCalledTimes(1);
const forwarded = mockFetch.mock.calls[0]![0] as Request;
expect(forwarded.method).toBe('POST');
expect(await forwarded.text()).toBe('payload');
});

it('appends baggage to existing non-sentry baggage', async () => {
getTraceDataMock.mockReturnValue({
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
Expand Down
Loading