Skip to content

Commit feb3ccd

Browse files
committed
fix(cloudflare): Instrument custom WorkerEntrypoint RPC methods
1 parent 2cbacb5 commit feb3ccd

9 files changed

Lines changed: 469 additions & 52 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
Work in this release was contributed by @dobladov and @PeterWadie. Thank you for your contributions!
88

9+
- feat(cloudflare): Instrument custom WorkerEntrypoint RPC receiver methods
10+
911
## 10.65.0
1012

1113
- feat(angular): Set `url` attributes on pageload and navigation spans ([#21985](https://github.com/getsentry/sentry-javascript/pull/21985))

dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/index-sub-worker.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@ interface Env {
55
SENTRY_DSN: string;
66
}
77

8-
class MySubWorkerEntrypointBase extends WorkerEntrypoint {
8+
interface Props {
9+
accountId: string;
10+
}
11+
12+
class BaseEntrypoint extends WorkerEntrypoint<Env, Props> {
13+
inherited(value: string): string {
14+
return value;
15+
}
16+
}
17+
18+
class MySubWorkerEntrypointBase extends BaseEntrypoint {
919
async fetch(request: Request): Promise<Response> {
1020
const url = new URL(request.url);
1121

@@ -20,13 +30,30 @@ class MySubWorkerEntrypointBase extends WorkerEntrypoint {
2030

2131
return new Response('Not found', { status: 404 });
2232
}
33+
34+
get(key: string): { argumentCount: number; key: string } {
35+
Sentry.setTag('key', key);
36+
return { argumentCount: arguments.length, key };
37+
}
38+
39+
throwError(): never {
40+
throw new Error('custom RPC receiver failed');
41+
}
2342
}
2443

25-
export default Sentry.withSentry(
44+
export const BindingEntrypoint = Sentry.withSentry(
2645
(env: Env) => ({
2746
dsn: env.SENTRY_DSN,
2847
tracesSampleRate: 1.0,
2948
enableRpcTracePropagation: true,
49+
initialScope: { tags: { initial_scope: 'applied' } },
50+
beforeSend(event) {
51+
event.tags = { ...event.tags, before_send: 'applied' };
52+
return event;
53+
},
54+
transportOptions: { fetch: fetch.bind(globalThis) },
3055
}),
3156
MySubWorkerEntrypointBase,
3257
);
58+
59+
export default BindingEntrypoint;

dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/index.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,26 @@
11
import * as Sentry from '@sentry/cloudflare';
2+
import { WorkerEntrypoint, exports as workerExports } from 'cloudflare:workers';
23

34
interface Env {
45
SENTRY_DSN: string;
5-
SUB_WORKER: Fetcher;
6+
SUB_WORKER: Fetcher & {
7+
get(key: string): Promise<{ argumentCount: number; key: string }>;
8+
inherited(value: string): Promise<string>;
9+
throwError(): Promise<never>;
10+
};
611
}
712

13+
class LoopbackEntrypointBase extends WorkerEntrypoint<Env> {
14+
throwError(): never {
15+
throw new Error('loopback RPC receiver failed');
16+
}
17+
}
18+
19+
export const LoopbackEntrypoint = Sentry.withSentry(
20+
(env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 0 }),
21+
LoopbackEntrypointBase,
22+
);
23+
824
export default Sentry.withSentry(
925
(env: Env) => ({
1026
dsn: env.SENTRY_DSN,
@@ -27,6 +43,30 @@ export default Sentry.withSentry(
2743
return new Response(text);
2844
}
2945

46+
if (url.pathname === '/call-entrypoint-rpc') {
47+
const result = await env.SUB_WORKER.get('feature-key');
48+
const inherited = await env.SUB_WORKER.inherited('base-value');
49+
return Response.json({ inherited, ...result });
50+
}
51+
52+
if (url.pathname === '/call-entrypoint-rpc-error') {
53+
try {
54+
await env.SUB_WORKER.throwError();
55+
} catch {
56+
return new Response('fallback');
57+
}
58+
}
59+
60+
if (url.pathname === '/call-loopback-rpc-error') {
61+
try {
62+
await (
63+
workerExports as unknown as { LoopbackEntrypoint: { throwError(): Promise<never> } }
64+
).LoopbackEntrypoint.throwError();
65+
} catch {
66+
return new Response('fallback');
67+
}
68+
}
69+
3070
return new Response('Not found', { status: 404 });
3171
},
3272
} satisfies ExportedHandler<Env>,

dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,89 @@ it('propagates trace for request with query params from Worker to WorkerEntrypoi
127127
expect(entrypointParentSpanId).toBeDefined();
128128
expect(entrypointParentSpanId).toBe(workerSpanId);
129129
});
130+
131+
it('instruments inherited custom WorkerEntrypoint RPC methods and strips metadata', async ({ signal }) => {
132+
let callerTraceId: string | undefined;
133+
let receiverTraceId: string | undefined;
134+
135+
const runner = createRunner(__dirname)
136+
.expect(envelope => {
137+
const event = envelope[1]?.[0]?.[1] as Event;
138+
expect(event.transaction).toBe('GET /call-entrypoint-rpc');
139+
callerTraceId = event.contexts?.trace?.trace_id;
140+
})
141+
.expect(envelope => {
142+
const event = envelope[1]?.[0]?.[1] as Event;
143+
expect(event.transaction).toBe('get');
144+
expect(event.contexts?.trace?.op).toBe('rpc');
145+
receiverTraceId = event.contexts?.trace?.trace_id;
146+
})
147+
.expect(envelope => {
148+
const event = envelope[1]?.[0]?.[1] as Event;
149+
expect(event.transaction).toBe('inherited');
150+
expect(event.contexts?.trace?.op).toBe('rpc');
151+
})
152+
.unordered()
153+
.start(signal);
154+
155+
const response = await runner.makeRequest<{ argumentCount: number; inherited: string; key: string }>(
156+
'get',
157+
'/call-entrypoint-rpc',
158+
);
159+
expect(response).toEqual({ argumentCount: 1, inherited: 'base-value', key: 'feature-key' });
160+
161+
await runner.completed();
162+
expect(receiverTraceId).toBe(callerTraceId);
163+
});
164+
165+
it('captures errors thrown by custom WorkerEntrypoint RPC methods', async ({ signal }) => {
166+
const runner = createRunner(__dirname)
167+
.expect(envelope => {
168+
const event = envelope[1]?.[0]?.[1] as Event;
169+
expect(event.exception?.values?.[0]?.value).toBe('custom RPC receiver failed');
170+
expect(event.exception?.values?.[0]?.mechanism).toEqual({
171+
handled: false,
172+
type: 'auto.faas.cloudflare.worker_entrypoint',
173+
});
174+
expect(event.tags?.initial_scope).toBe('applied');
175+
expect(event.tags?.before_send).toBe('applied');
176+
})
177+
.expect(envelope => {
178+
const event = envelope[1]?.[0]?.[1] as Event;
179+
expect(event.transaction).toBe('throwError');
180+
})
181+
.expect(envelope => {
182+
const event = envelope[1]?.[0]?.[1] as Event;
183+
expect(event.transaction).toBe('GET /call-entrypoint-rpc-error');
184+
})
185+
.unordered()
186+
.start(signal);
187+
188+
const response = await runner.makeRequest<string>('get', '/call-entrypoint-rpc-error');
189+
expect(response).toBe('fallback');
190+
191+
await runner.completed();
192+
});
193+
194+
it('captures errors from loopback WorkerEntrypoint RPC without trace propagation', async ({ signal }) => {
195+
const runner = createRunner(__dirname)
196+
.expect(envelope => {
197+
const event = envelope[1]?.[0]?.[1] as Event;
198+
expect(event.exception?.values?.[0]?.value).toBe('loopback RPC receiver failed');
199+
expect(event.exception?.values?.[0]?.mechanism).toEqual({
200+
handled: false,
201+
type: 'auto.faas.cloudflare.worker_entrypoint',
202+
});
203+
})
204+
.expect(envelope => {
205+
const event = envelope[1]?.[0]?.[1] as Event;
206+
expect(event.transaction).toBe('GET /call-loopback-rpc-error');
207+
})
208+
.unordered()
209+
.start(signal);
210+
211+
const response = await runner.makeRequest<string>('get', '/call-loopback-rpc-error');
212+
expect(response).toBe('fallback');
213+
214+
await runner.completed();
215+
});

dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/wrangler.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
{
88
"binding": "SUB_WORKER",
99
"service": "cloudflare-worker-workerentrypoint-rpc-sub",
10+
"entrypoint": "BindingEntrypoint",
1011
},
1112
],
1213
}

packages/cloudflare/src/client.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,9 @@ interface BaseCloudflareOptions {
177177
* When enabled, trace context (sentry-trace + baggage) is propagated across:
178178
* - `stub.fetch()` calls to Durable Objects (via HTTP headers)
179179
* - Service binding `fetch()` calls (via HTTP headers)
180-
* - RPC method calls to Durable Objects (via trailing argument)
180+
* - RPC method calls to Durable Objects and WorkerEntrypoints (via trailing argument)
181181
*
182-
* When enabled on the **receiver side** (DurableObject), the SDK will also:
182+
* When enabled on the **receiver side** (DurableObject or WorkerEntrypoint), the SDK will also:
183183
* - Extract and continue traces from incoming RPC calls
184184
* - Create spans for each RPC method invocation
185185
* - Capture errors thrown by RPC methods
@@ -206,6 +206,12 @@ interface BaseCloudflareOptions {
206206
* }),
207207
* MyDOBase,
208208
* );
209+
*
210+
* // WorkerEntrypoint side (receiver)
211+
* export const MyEntrypoint = Sentry.withSentry(
212+
* env => ({ dsn: env.SENTRY_DSN, enableRpcTracePropagation: true }),
213+
* MyEntrypointBase,
214+
* );
209215
* ```
210216
*/
211217
enableRpcTracePropagation?: boolean;

0 commit comments

Comments
 (0)