Skip to content

Commit fb6a835

Browse files
committed
feat(cloudflare): Propagate traceparent to RPC calls
1 parent e090ccc commit fb6a835

29 files changed

+1440
-4
lines changed

dev-packages/cloudflare-integration-tests/runner.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,17 @@ export function createRunner(...paths: string[]) {
6868
// By default, we ignore session & sessions
6969
const ignored: Set<EnvelopeItemType> = new Set(['session', 'sessions', 'client_report']);
7070
let serverUrl: string | undefined;
71+
const extraWranglerArgs: string[] = [];
7172

7273
return {
7374
withServerUrl: function (url: string) {
7475
serverUrl = url;
7576
return this;
7677
},
78+
withWranglerArgs: function (...args: string[]) {
79+
extraWranglerArgs.push(...args);
80+
return this;
81+
},
7782
expect: function (expected: Expected) {
7883
expectedEnvelopes.push(expected);
7984
return this;
@@ -237,6 +242,7 @@ export function createRunner(...paths: string[]) {
237242
`SENTRY_DSN:http://public@localhost:${mockServerPort}/1337`,
238243
'--var',
239244
`SERVER_URL:${serverUrl}`,
245+
...extraWranglerArgs,
240246
],
241247
{ stdio, signal },
242248
);
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as Sentry from '@sentry/cloudflare';
2+
import { DurableObject } from 'cloudflare:workers';
3+
4+
interface Env {
5+
SENTRY_DSN: string;
6+
MY_DURABLE_OBJECT: DurableObjectNamespace;
7+
MY_QUEUE: Queue;
8+
}
9+
10+
class MyDurableObjectBase extends DurableObject<Env> {
11+
async fetch(request: Request) {
12+
return new Response('DO is fine');
13+
}
14+
}
15+
16+
export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
17+
(env: Env) => ({
18+
dsn: env.SENTRY_DSN,
19+
tracesSampleRate: 1.0,
20+
}),
21+
MyDurableObjectBase,
22+
);
23+
24+
export default Sentry.withSentry(
25+
(env: Env) => ({
26+
dsn: env.SENTRY_DSN,
27+
tracesSampleRate: 1.0,
28+
}),
29+
{
30+
async fetch(request, env) {
31+
const url = new URL(request.url);
32+
33+
if (url.pathname === '/queue/send') {
34+
await env.MY_QUEUE.send({ action: 'test' });
35+
return new Response('Queued');
36+
}
37+
38+
const id = env.MY_DURABLE_OBJECT.idFromName('test');
39+
const stub = env.MY_DURABLE_OBJECT.get(id);
40+
const response = await stub.fetch(new Request('http://fake-host/hello'));
41+
const text = await response.text();
42+
return new Response(text);
43+
},
44+
45+
async queue(batch, env, _ctx) {
46+
const id = env.MY_DURABLE_OBJECT.idFromName('test');
47+
const stub = env.MY_DURABLE_OBJECT.get(id);
48+
for (const message of batch.messages) {
49+
await stub.fetch(new Request('http://fake-host/hello'));
50+
message.ack();
51+
}
52+
},
53+
54+
async scheduled(controller, env, _ctx) {
55+
const id = env.MY_DURABLE_OBJECT.idFromName('test');
56+
const stub = env.MY_DURABLE_OBJECT.get(id);
57+
await stub.fetch(new Request('http://fake-host/hello'));
58+
},
59+
} satisfies ExportedHandler<Env>,
60+
);
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { expect, it } from 'vitest';
2+
import type { Event } from '@sentry/core';
3+
import { createRunner } from '../../../../runner';
4+
5+
it('propagates trace from worker to durable object', async ({ signal }) => {
6+
let workerTraceId: string | undefined;
7+
let workerSpanId: string | undefined;
8+
let doTraceId: string | undefined;
9+
let doParentSpanId: string | undefined;
10+
11+
const runner = createRunner(__dirname)
12+
.expect(envelope => {
13+
const transactionEvent = envelope[1]?.[0]?.[1] as Event;
14+
15+
expect(transactionEvent).toEqual(
16+
expect.objectContaining({
17+
contexts: expect.objectContaining({
18+
trace: expect.objectContaining({
19+
op: 'http.server',
20+
data: expect.objectContaining({
21+
'sentry.origin': 'auto.http.cloudflare',
22+
}),
23+
origin: 'auto.http.cloudflare',
24+
}),
25+
}),
26+
transaction: 'GET /hello',
27+
}),
28+
);
29+
doTraceId = transactionEvent.contexts?.trace?.trace_id as string;
30+
doParentSpanId = transactionEvent.contexts?.trace?.parent_span_id as string;
31+
})
32+
.expect(envelope => {
33+
const transactionEvent = envelope[1]?.[0]?.[1] as Event;
34+
35+
expect(transactionEvent).toEqual(
36+
expect.objectContaining({
37+
contexts: expect.objectContaining({
38+
trace: expect.objectContaining({
39+
op: 'http.server',
40+
data: expect.objectContaining({
41+
'sentry.origin': 'auto.http.cloudflare',
42+
}),
43+
origin: 'auto.http.cloudflare',
44+
}),
45+
}),
46+
transaction: 'GET /',
47+
}),
48+
);
49+
workerTraceId = transactionEvent.contexts?.trace?.trace_id as string;
50+
workerSpanId = transactionEvent.contexts?.trace?.span_id as string;
51+
})
52+
.unordered()
53+
.start(signal);
54+
await runner.makeRequest('get', '/');
55+
await runner.completed();
56+
57+
expect(workerTraceId).toBeDefined();
58+
expect(doTraceId).toBeDefined();
59+
expect(workerTraceId).toBe(doTraceId);
60+
61+
expect(workerSpanId).toBeDefined();
62+
expect(doParentSpanId).toBeDefined();
63+
expect(doParentSpanId).toBe(workerSpanId);
64+
});
65+
66+
it('propagates trace from queue handler to durable object', async ({ signal }) => {
67+
let queueTraceId: string | undefined;
68+
let queueSpanId: string | undefined;
69+
let doTraceId: string | undefined;
70+
let doParentSpanId: string | undefined;
71+
72+
const runner = createRunner(__dirname)
73+
.expect(envelope => {
74+
const transactionEvent = envelope[1]?.[0]?.[1] as Event;
75+
76+
expect(transactionEvent).toEqual(
77+
expect.objectContaining({
78+
contexts: expect.objectContaining({
79+
trace: expect.objectContaining({
80+
op: 'http.server',
81+
data: expect.objectContaining({
82+
'sentry.origin': 'auto.http.cloudflare',
83+
}),
84+
origin: 'auto.http.cloudflare',
85+
}),
86+
}),
87+
transaction: 'GET /hello',
88+
}),
89+
);
90+
doTraceId = transactionEvent.contexts?.trace?.trace_id as string;
91+
doParentSpanId = transactionEvent.contexts?.trace?.parent_span_id as string;
92+
})
93+
.expect(envelope => {
94+
const transactionEvent = envelope[1]?.[0]?.[1] as Event;
95+
96+
expect(transactionEvent).toEqual(
97+
expect.objectContaining({
98+
contexts: expect.objectContaining({
99+
trace: expect.objectContaining({
100+
op: 'queue.process',
101+
data: expect.objectContaining({
102+
'sentry.origin': 'auto.faas.cloudflare.queue',
103+
}),
104+
origin: 'auto.faas.cloudflare.queue',
105+
}),
106+
}),
107+
transaction: 'process my-queue',
108+
}),
109+
);
110+
queueTraceId = transactionEvent.contexts?.trace?.trace_id as string;
111+
queueSpanId = transactionEvent.contexts?.trace?.span_id as string;
112+
})
113+
// Also expect the fetch transaction from the /queue/send request
114+
.expect(envelope => {
115+
const transactionEvent = envelope[1]?.[0]?.[1] as Event;
116+
117+
expect(transactionEvent).toEqual(
118+
expect.objectContaining({
119+
contexts: expect.objectContaining({
120+
trace: expect.objectContaining({
121+
op: 'http.server',
122+
data: expect.objectContaining({
123+
'sentry.origin': 'auto.http.cloudflare',
124+
}),
125+
origin: 'auto.http.cloudflare',
126+
}),
127+
}),
128+
transaction: 'GET /queue/send',
129+
}),
130+
);
131+
})
132+
.unordered()
133+
.start(signal);
134+
// The fetch handler sends a message to the queue, which triggers the queue consumer
135+
await runner.makeRequest('get', '/queue/send');
136+
await runner.completed();
137+
138+
expect(queueTraceId).toBeDefined();
139+
expect(doTraceId).toBeDefined();
140+
expect(queueTraceId).toBe(doTraceId);
141+
142+
expect(queueSpanId).toBeDefined();
143+
expect(doParentSpanId).toBeDefined();
144+
expect(doParentSpanId).toBe(queueSpanId);
145+
});
146+
147+
it('propagates trace from scheduled handler to durable object', async ({ signal }) => {
148+
let scheduledTraceId: string | undefined;
149+
let scheduledSpanId: string | undefined;
150+
let doTraceId: string | undefined;
151+
let doParentSpanId: string | undefined;
152+
153+
const runner = createRunner(__dirname)
154+
.withWranglerArgs('--test-scheduled')
155+
.expect(envelope => {
156+
const transactionEvent = envelope[1]?.[0]?.[1] as Event;
157+
158+
expect(transactionEvent).toEqual(
159+
expect.objectContaining({
160+
contexts: expect.objectContaining({
161+
trace: expect.objectContaining({
162+
op: 'http.server',
163+
data: expect.objectContaining({
164+
'sentry.origin': 'auto.http.cloudflare',
165+
}),
166+
origin: 'auto.http.cloudflare',
167+
}),
168+
}),
169+
transaction: 'GET /hello',
170+
}),
171+
);
172+
doTraceId = transactionEvent.contexts?.trace?.trace_id as string;
173+
doParentSpanId = transactionEvent.contexts?.trace?.parent_span_id as string;
174+
})
175+
.expect(envelope => {
176+
const transactionEvent = envelope[1]?.[0]?.[1] as Event;
177+
178+
expect(transactionEvent).toEqual(
179+
expect.objectContaining({
180+
contexts: expect.objectContaining({
181+
trace: expect.objectContaining({
182+
op: 'faas.cron',
183+
data: expect.objectContaining({
184+
'sentry.origin': 'auto.faas.cloudflare.scheduled',
185+
}),
186+
origin: 'auto.faas.cloudflare.scheduled',
187+
}),
188+
}),
189+
}),
190+
);
191+
scheduledTraceId = transactionEvent.contexts?.trace?.trace_id as string;
192+
scheduledSpanId = transactionEvent.contexts?.trace?.span_id as string;
193+
})
194+
.unordered()
195+
.start(signal);
196+
await runner.makeRequest('get', '/__scheduled?cron=*+*+*+*+*');
197+
await runner.completed();
198+
199+
expect(scheduledTraceId).toBeDefined();
200+
expect(doTraceId).toBeDefined();
201+
expect(scheduledTraceId).toBe(doTraceId);
202+
203+
expect(scheduledSpanId).toBeDefined();
204+
expect(doParentSpanId).toBeDefined();
205+
expect(doParentSpanId).toBe(scheduledSpanId);
206+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "cloudflare-durable-objects",
3+
"main": "index.ts",
4+
"compatibility_date": "2025-06-17",
5+
"compatibility_flags": ["nodejs_als"],
6+
"migrations": [
7+
{
8+
"new_sqlite_classes": ["MyDurableObject"],
9+
"tag": "v1",
10+
},
11+
],
12+
"durable_objects": {
13+
"bindings": [
14+
{
15+
"class_name": "MyDurableObject",
16+
"name": "MY_DURABLE_OBJECT",
17+
},
18+
],
19+
},
20+
"queues": {
21+
"producers": [
22+
{
23+
"binding": "MY_QUEUE",
24+
"queue": "my-queue",
25+
},
26+
],
27+
"consumers": [
28+
{
29+
"queue": "my-queue",
30+
},
31+
],
32+
},
33+
"triggers": {
34+
"crons": ["* * * * *"],
35+
},
36+
"vars": {
37+
"SENTRY_DSN": "https://932e620ee3921c3b4a61c72558ad88ce@o447951.ingest.us.sentry.io/4509553159831552",
38+
},
39+
}

dev-packages/cloudflare-integration-tests/suites/tracing/worker-service-binding/index-sub-worker.ts renamed to dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/index-sub-worker.ts

File renamed without changes.

dev-packages/cloudflare-integration-tests/suites/tracing/worker-service-binding/index.ts renamed to dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/index.ts

File renamed without changes.

dev-packages/cloudflare-integration-tests/suites/tracing/worker-service-binding/test.ts renamed to dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/test.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import { expect, it } from 'vitest';
22
import type { Event } from '@sentry/core';
3-
import { createRunner } from '../../../runner';
3+
import { createRunner } from '../../../../runner';
4+
5+
it('propagates trace from worker to worker via service binding', async ({ signal }) => {
6+
let workerTraceId: string | undefined;
7+
let workerSpanId: string | undefined;
8+
let subWorkerTraceId: string | undefined;
9+
let subWorkerParentSpanId: string | undefined;
410

5-
it('adds a trace to a worker via service binding', async ({ signal }) => {
611
const runner = createRunner(__dirname)
712
.expect(envelope => {
813
const transactionEvent = envelope[1]?.[0]?.[1] as Event;
@@ -20,6 +25,8 @@ it('adds a trace to a worker via service binding', async ({ signal }) => {
2025
transaction: 'GET /',
2126
}),
2227
);
28+
workerTraceId = transactionEvent.contexts?.trace?.trace_id as string;
29+
workerSpanId = transactionEvent.contexts?.trace?.span_id as string;
2330
})
2431
.expect(envelope => {
2532
const transactionEvent = envelope[1]?.[0]?.[1] as Event;
@@ -37,9 +44,19 @@ it('adds a trace to a worker via service binding', async ({ signal }) => {
3744
transaction: 'GET /hello',
3845
}),
3946
);
47+
subWorkerTraceId = transactionEvent.contexts?.trace?.trace_id as string;
48+
subWorkerParentSpanId = transactionEvent.contexts?.trace?.parent_span_id as string;
4049
})
4150
.unordered()
4251
.start(signal);
4352
await runner.makeRequest('get', '/');
4453
await runner.completed();
54+
55+
expect(workerTraceId).toBeDefined();
56+
expect(subWorkerTraceId).toBeDefined();
57+
expect(workerTraceId).toBe(subWorkerTraceId);
58+
59+
expect(workerSpanId).toBeDefined();
60+
expect(subWorkerParentSpanId).toBeDefined();
61+
expect(subWorkerParentSpanId).toBe(workerSpanId);
4562
});

dev-packages/cloudflare-integration-tests/suites/tracing/worker-service-binding/wrangler-sub-worker.jsonc renamed to dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/wrangler-sub-worker.jsonc

File renamed without changes.

dev-packages/cloudflare-integration-tests/suites/tracing/worker-service-binding/wrangler.jsonc renamed to dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/wrangler.jsonc

File renamed without changes.

0 commit comments

Comments
 (0)