Skip to content

Commit 77c7260

Browse files
committed
feat(cloudflare): Use getTracingHeadersForFetchRequest to get trace headers
1 parent fb6a835 commit 77c7260

File tree

9 files changed

+390
-103
lines changed

9 files changed

+390
-103
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { instrumentDurableObjectWithSentry, withSentry } from '@sentry/cloudflare';
2+
import { DurableObject } from 'cloudflare:workers';
3+
4+
interface Env {
5+
SENTRY_DSN: string;
6+
ECHO_HEADERS_DO: DurableObjectNamespace;
7+
}
8+
9+
class EchoHeadersDurableObjectBase extends DurableObject<Env> {
10+
async fetch(incoming: Request): Promise<Response> {
11+
return Response.json({
12+
sentryTrace: incoming.headers.get('sentry-trace'),
13+
baggage: incoming.headers.get('baggage'),
14+
authorization: incoming.headers.get('authorization'),
15+
xFromInit: incoming.headers.get('x-from-init'),
16+
xExtra: incoming.headers.get('x-extra'),
17+
xMergeProbe: incoming.headers.get('x-merge-probe'),
18+
});
19+
}
20+
}
21+
22+
export const EchoHeadersDurableObject = instrumentDurableObjectWithSentry(
23+
(env: Env) => ({
24+
dsn: env.SENTRY_DSN,
25+
tracesSampleRate: 1.0,
26+
}),
27+
EchoHeadersDurableObjectBase,
28+
);
29+
30+
export default withSentry(
31+
(env: Env) => ({
32+
dsn: env.SENTRY_DSN,
33+
tracesSampleRate: 1.0,
34+
}),
35+
{
36+
async fetch(request, env) {
37+
const url = new URL(request.url);
38+
const id = env.ECHO_HEADERS_DO.idFromName('instrument-fetcher-echo');
39+
const stub = env.ECHO_HEADERS_DO.get(id);
40+
const doUrl = new URL(request.url);
41+
42+
let subResponse: Response;
43+
44+
if (url.pathname === '/via-init') {
45+
subResponse = await stub.fetch(doUrl, {
46+
headers: {
47+
Authorization: 'Bearer from-init',
48+
'X-Extra': 'init-extra',
49+
'X-Merge-Probe': 'via-init-probe',
50+
},
51+
});
52+
} else if (url.pathname === '/via-request') {
53+
subResponse = await stub.fetch(
54+
new Request(doUrl, {
55+
headers: {
56+
Authorization: 'Bearer from-request',
57+
'X-Extra': 'request-extra',
58+
'X-Merge-Probe': 'via-request-probe',
59+
},
60+
}),
61+
);
62+
} else if (url.pathname === '/via-request-and-init') {
63+
subResponse = await stub.fetch(
64+
new Request(doUrl, {
65+
headers: {
66+
Authorization: 'Bearer from-request',
67+
'X-Extra': 'request-extra',
68+
'X-Merge-Probe': 'dropped-from-request',
69+
},
70+
}),
71+
{
72+
headers: {
73+
'X-From-Init': '1',
74+
'X-Merge-Probe': 'via-init-wins',
75+
},
76+
},
77+
);
78+
} else if (url.pathname === '/with-preset-sentry-baggage') {
79+
subResponse = await stub.fetch(
80+
new Request(doUrl, {
81+
headers: {
82+
baggage: 'sentry-environment=preset,acme=vendor',
83+
},
84+
}),
85+
);
86+
} else {
87+
return new Response('not found', { status: 404 });
88+
}
89+
90+
const payload: unknown = await subResponse.json();
91+
return Response.json(payload);
92+
},
93+
} satisfies ExportedHandler<Env>,
94+
);
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { expect, it } from 'vitest';
2+
import type { Event } from '@sentry/core';
3+
import { createRunner } from '../../../runner';
4+
5+
type EchoedHeaders = {
6+
sentryTrace: string | null;
7+
baggage: string | null;
8+
authorization: string | null;
9+
xFromInit: string | null;
10+
xExtra: string | null;
11+
xMergeProbe: string | null;
12+
};
13+
14+
const SENTRY_TRACE_HEADER_RE = /^[0-9a-f]{32}-[0-9a-f]{16}-[01]$/;
15+
16+
type ScenarioPath =
17+
| '/via-init'
18+
| '/via-request'
19+
| '/via-request-and-init'
20+
| '/with-preset-sentry-baggage';
21+
22+
function startStubFetchScenario(path: ScenarioPath, signal: AbortSignal) {
23+
let mainTraceId: string | undefined;
24+
let mainSpanId: string | undefined;
25+
let doTraceId: string | undefined;
26+
let doParentSpanId: string | undefined;
27+
28+
const traceBase = {
29+
op: 'http.server',
30+
data: expect.objectContaining({
31+
'sentry.origin': 'auto.http.cloudflare',
32+
}),
33+
origin: 'auto.http.cloudflare',
34+
};
35+
36+
const { makeRequest, completed } = createRunner(__dirname)
37+
.expect(envelope => {
38+
const transactionEvent = envelope[1]?.[0]?.[1] as Event;
39+
const parentSpanId = transactionEvent.contexts?.trace?.parent_span_id;
40+
41+
expect(transactionEvent).toEqual(
42+
expect.objectContaining({
43+
contexts: expect.objectContaining({
44+
trace: expect.objectContaining(traceBase),
45+
}),
46+
transaction: `GET ${path}`,
47+
}),
48+
);
49+
expect(parentSpanId).toBeUndefined();
50+
51+
mainTraceId = transactionEvent.contexts?.trace?.trace_id as string;
52+
mainSpanId = transactionEvent.contexts?.trace?.span_id as string;
53+
})
54+
.expect(envelope => {
55+
const transactionEvent = envelope[1]?.[0]?.[1] as Event;
56+
const parentSpanId = transactionEvent.contexts?.trace?.parent_span_id;
57+
58+
expect(transactionEvent).toEqual(
59+
expect.objectContaining({
60+
contexts: expect.objectContaining({
61+
trace: expect.objectContaining(traceBase),
62+
}),
63+
transaction: `GET ${path}`,
64+
}),
65+
);
66+
expect(parentSpanId).toBeDefined();
67+
68+
doTraceId = transactionEvent.contexts?.trace?.trace_id as string;
69+
doParentSpanId = parentSpanId as string;
70+
})
71+
.unordered()
72+
.start(signal);
73+
74+
return {
75+
makeRequest,
76+
async completedWithTraceCheck(): Promise<void> {
77+
await completed();
78+
expect(mainTraceId).toBeDefined();
79+
expect(doTraceId).toBeDefined();
80+
expect(mainTraceId).toBe(doTraceId);
81+
expect(mainSpanId).toBeDefined();
82+
expect(doParentSpanId).toBeDefined();
83+
expect(doParentSpanId).toBe(mainSpanId);
84+
},
85+
};
86+
}
87+
88+
it('stub.fetch: headers in init (URL string + init)', async ({ signal }) => {
89+
const { makeRequest, completedWithTraceCheck } = startStubFetchScenario('/via-init', signal);
90+
const body = await makeRequest<EchoedHeaders>('get', '/via-init');
91+
await completedWithTraceCheck();
92+
93+
expect(body?.sentryTrace).toEqual(expect.stringMatching(SENTRY_TRACE_HEADER_RE));
94+
expect(body?.baggage).toContain('sentry-environment=production,sentry-public_key=public,sentry-trace_id=');
95+
expect(body?.authorization).toBe('Bearer from-init');
96+
expect(body?.xExtra).toBe('init-extra');
97+
expect(body?.xMergeProbe).toBe('via-init-probe');
98+
expect(body?.xFromInit).toBeNull();
99+
});
100+
101+
it('stub.fetch: headers on Request (URL from incoming request)', async ({ signal }) => {
102+
const { makeRequest, completedWithTraceCheck } = startStubFetchScenario('/via-request', signal);
103+
const body = await makeRequest<EchoedHeaders>('get', '/via-request');
104+
await completedWithTraceCheck();
105+
106+
expect(body?.sentryTrace).toEqual(expect.stringMatching(SENTRY_TRACE_HEADER_RE));
107+
expect(body?.baggage).toContain('sentry-environment=production,sentry-public_key=public,sentry-trace_id=');
108+
expect(body?.authorization).toBe('Bearer from-request');
109+
expect(body?.xExtra).toBe('request-extra');
110+
expect(body?.xMergeProbe).toBe('via-request-probe');
111+
expect(body?.xFromInit).toBeNull();
112+
});
113+
114+
it('stub.fetch: Request + init — only init headers are sent', async ({ signal }) => {
115+
const { makeRequest, completedWithTraceCheck } = startStubFetchScenario('/via-request-and-init', signal);
116+
const body = await makeRequest<EchoedHeaders>('get', '/via-request-and-init');
117+
await completedWithTraceCheck();
118+
119+
expect(body?.sentryTrace).toEqual(expect.stringMatching(SENTRY_TRACE_HEADER_RE));
120+
expect(body?.baggage).toContain('sentry-environment=production,sentry-public_key=public,sentry-trace_id=');
121+
expect(body?.authorization).toBeNull();
122+
expect(body?.xExtra).toBeNull();
123+
expect(body?.xMergeProbe).toBe('via-init-wins');
124+
expect(body?.xFromInit).toBe('1');
125+
});
126+
127+
it('stub.fetch: does not append SDK baggage when the Request already includes Sentry baggage', async ({ signal }) => {
128+
const { makeRequest, completedWithTraceCheck } = startStubFetchScenario('/with-preset-sentry-baggage', signal);
129+
const body = await makeRequest<EchoedHeaders>('get', '/with-preset-sentry-baggage');
130+
await completedWithTraceCheck();
131+
132+
expect(body?.sentryTrace).toEqual(expect.stringMatching(SENTRY_TRACE_HEADER_RE));
133+
// Dynamic SDK baggage includes `sentry-trace_id=…`; appending it again would change this string.
134+
expect(body?.baggage).toBe('sentry-environment=preset,acme=vendor');
135+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "cloudflare-instrument-fetcher",
3+
"main": "index.ts",
4+
"compatibility_date": "2025-06-17",
5+
"compatibility_flags": ["nodejs_als"],
6+
"migrations": [
7+
{
8+
"new_sqlite_classes": ["EchoHeadersDurableObject"],
9+
"tag": "v1",
10+
},
11+
],
12+
"durable_objects": {
13+
"bindings": [
14+
{
15+
"class_name": "EchoHeadersDurableObject",
16+
"name": "ECHO_HEADERS_DO",
17+
},
18+
],
19+
},
20+
}
Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Fetcher } from '@cloudflare/workers-types';
2-
import { addTraceHeaders } from '../../utils/addTraceHeaders';
2+
import { getTracingHeadersForFetchRequest } from '@sentry/core';
33

44
/**
55
* Wraps a fetch-like function to create a span and propagate trace headers
@@ -10,8 +10,25 @@ import { addTraceHeaders } from '../../utils/addTraceHeaders';
1010
*/
1111
export function instrumentFetcher(fetchFn: Fetcher['fetch']): Fetcher['fetch'] {
1212
return function (input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
13-
const newInit = addTraceHeaders(input, init);
13+
const headers = getTracingHeadersForFetchRequest(input, { headers: init?.headers });
1414

15-
return fetchFn(input, newInit);
15+
if (input instanceof Request && init === undefined) {
16+
if (!headers) {
17+
return fetchFn(input);
18+
}
19+
20+
// Newly created headers already include the previous headers from the original request
21+
// so we can clone the request and pass in all headers.
22+
const requestWithTracing = new Request(input, { headers: headers as HeadersInit });
23+
24+
return fetchFn(requestWithTracing);
25+
}
26+
27+
const mergedInit = {
28+
...init,
29+
...(headers ? { headers } : {}),
30+
} as NonNullable<Parameters<Fetcher['fetch']>[1]>;
31+
32+
return fetchFn(input, mergedInit);
1633
};
1734
}

packages/cloudflare/src/utils/addTraceHeaders.ts

Lines changed: 0 additions & 35 deletions
This file was deleted.

0 commit comments

Comments
 (0)