Skip to content

Commit a7acc8f

Browse files
authored
feat(cloudflare): Add instrumentPrototypeMethods option to instrument RPC methods for DurableObjects (#17424)
Fixes `The RPC receiver does not implement the method "METHOD_NAME error` errors by wrapping methods and putting them back on the prototype. Added an option `instrumentPrototypeMethods` to opt into wrapping RPC methods. These are potentially expensive to wrap because each invocation wraps so we provide an option to either enable this as a whole or pass method names that should be wrapped. Also removes the flag to not record spans because no spans were collected ever. I think this is related to connected traces not working correctly for the SDK? cc @AbhiPrasad. Closes: #17127
1 parent 018db43 commit a7acc8f

File tree

8 files changed

+387
-76
lines changed

8 files changed

+387
-76
lines changed

CHANGELOG.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,37 @@
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
66

7+
### Important Changes
8+
9+
- feat(cloudflare): Add `instrumentPrototypeMethods` option to instrument RPC methods for DurableObjects ([#17424](https://github.com/getsentry/sentry-javascript/pull/17424))
10+
11+
By default, `Sentry.instrumentDurableObjectWithSentry` will not wrap any RPC methods on the prototype. To enable wrapping for RPC methods, set `instrumentPrototypeMethods` to `true` or, if performance is a concern, a list of only the methods you want to instrument:
12+
13+
```js
14+
class MyDurableObjectBase extends DurableObject<Env> {
15+
method1() {
16+
// ...
17+
}
18+
19+
method2() {
20+
// ...
21+
}
22+
23+
method3() {
24+
// ...
25+
}
26+
}
27+
// Export your named class as defined in your wrangler config
28+
export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
29+
(env: Env) => ({
30+
dsn: "https://ac49b7af3017c458bd12dab9b3328bfc@o4508482761982032.ingest.de.sentry.io/4508482780987481",
31+
tracesSampleRate: 1.0,
32+
instrumentPrototypeMethods: ['method1', 'method3'],
33+
}),
34+
MyDurableObjectBase,
35+
);
36+
```
37+
738
## 10.6.0
839

940
### Important Changes
@@ -41,8 +72,6 @@ The Sentry Nuxt SDK is now considered stable and no longer in beta!
4172

4273
</details>
4374

44-
Work in this release was contributed by @Karibash. Thank you for your contribution!
45-
4675
## 10.5.0
4776

4877
- feat(core): better cause data extraction ([#17375](https://github.com/getsentry/sentry-javascript/pull/17375))
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as Sentry from '@sentry/cloudflare';
2+
import { DurableObject } from 'cloudflare:workers';
3+
4+
interface Env {
5+
SENTRY_DSN: string;
6+
TEST_DURABLE_OBJECT: DurableObjectNamespace;
7+
}
8+
9+
class TestDurableObjectBase extends DurableObject<Env> {
10+
public constructor(ctx: DurableObjectState, env: Env) {
11+
super(ctx, env);
12+
}
13+
14+
// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility
15+
async sayHello(name: string): Promise<string> {
16+
return `Hello, ${name}`;
17+
}
18+
}
19+
20+
export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry(
21+
(env: Env) => ({
22+
dsn: env.SENTRY_DSN,
23+
tracesSampleRate: 1.0,
24+
instrumentPrototypeMethods: true,
25+
}),
26+
TestDurableObjectBase,
27+
);
28+
29+
export default {
30+
async fetch(request: Request, env: Env): Promise<Response> {
31+
const id: DurableObjectId = env.TEST_DURABLE_OBJECT.idFromName('test');
32+
const stub = env.TEST_DURABLE_OBJECT.get(id) as unknown as TestDurableObjectBase;
33+
34+
if (request.url.includes('hello')) {
35+
const greeting = await stub.sayHello('world');
36+
return new Response(greeting);
37+
}
38+
39+
return new Response('Usual response');
40+
},
41+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { expect, it } from 'vitest';
2+
import { createRunner } from '../../../runner';
3+
4+
it('traces a durable object method', async () => {
5+
const runner = createRunner(__dirname)
6+
.expect(envelope => {
7+
const transactionEvent = envelope[1]?.[0]?.[1];
8+
expect(transactionEvent).toEqual(
9+
expect.objectContaining({
10+
contexts: expect.objectContaining({
11+
trace: expect.objectContaining({
12+
op: 'rpc',
13+
data: expect.objectContaining({
14+
'sentry.op': 'rpc',
15+
'sentry.origin': 'auto.faas.cloudflare_durableobjects',
16+
}),
17+
origin: 'auto.faas.cloudflare_durableobjects',
18+
}),
19+
}),
20+
transaction: 'sayHello',
21+
}),
22+
);
23+
})
24+
.start();
25+
await runner.makeRequest('get', '/hello');
26+
await runner.completed();
27+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "worker-name",
3+
"main": "index.ts",
4+
"compatibility_date": "2025-06-17",
5+
"migrations": [
6+
{
7+
"new_sqlite_classes": ["TestDurableObject"],
8+
"tag": "v1"
9+
}
10+
],
11+
"durable_objects": {
12+
"bindings": [
13+
{
14+
"class_name": "TestDurableObject",
15+
"name": "TEST_DURABLE_OBJECT"
16+
}
17+
]
18+
},
19+
"compatibility_flags": ["nodejs_als"],
20+
"vars": {
21+
"SENTRY_DSN": "https://[email protected]/4509553159831552"
22+
}
23+
}

dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* Learn more at https://developers.cloudflare.com/workers/
1212
*/
1313
import * as Sentry from '@sentry/cloudflare';
14-
import { DurableObject } from "cloudflare:workers";
14+
import { DurableObject } from 'cloudflare:workers';
1515

1616
class MyDurableObjectBase extends DurableObject<Env> {
1717
private throwOnExit = new WeakMap<WebSocket, Error>();
@@ -44,7 +44,7 @@ class MyDurableObjectBase extends DurableObject<Env> {
4444
}
4545

4646
webSocketClose(ws: WebSocket): void | Promise<void> {
47-
if (this.throwOnExit.has(ws)) {
47+
if (this.throwOnExit.has(ws)) {
4848
const error = this.throwOnExit.get(ws)!;
4949
this.throwOnExit.delete(ws);
5050
throw error;
@@ -53,36 +53,37 @@ class MyDurableObjectBase extends DurableObject<Env> {
5353
}
5454

5555
export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
56-
(env: Env) => ({
57-
dsn: env.E2E_TEST_DSN,
58-
environment: 'qa', // dynamic sampling bias to keep transactions
59-
tunnel: `http://localhost:3031/`, // proxy server
60-
tracesSampleRate: 1.0,
61-
sendDefaultPii: true,
62-
transportOptions: {
63-
// We are doing a lot of events at once in this test
64-
bufferSize: 1000,
65-
},
66-
}),
67-
MyDurableObjectBase,
56+
(env: Env) => ({
57+
dsn: env.E2E_TEST_DSN,
58+
environment: 'qa', // dynamic sampling bias to keep transactions
59+
tunnel: `http://localhost:3031/`, // proxy server
60+
tracesSampleRate: 1.0,
61+
sendDefaultPii: true,
62+
transportOptions: {
63+
// We are doing a lot of events at once in this test
64+
bufferSize: 1000,
65+
},
66+
instrumentPrototypeMethods: true,
67+
}),
68+
MyDurableObjectBase,
6869
);
6970

7071
export default Sentry.withSentry(
71-
(env: Env) => ({
72-
dsn: env.E2E_TEST_DSN,
73-
environment: 'qa', // dynamic sampling bias to keep transactions
74-
tunnel: `http://localhost:3031/`, // proxy server
75-
tracesSampleRate: 1.0,
76-
sendDefaultPii: true,
77-
transportOptions: {
78-
// We are doing a lot of events at once in this test
79-
bufferSize: 1000,
80-
},
81-
}),
82-
{
83-
async fetch(request, env) {
84-
const url = new URL(request.url);
85-
switch (url.pathname) {
72+
(env: Env) => ({
73+
dsn: env.E2E_TEST_DSN,
74+
environment: 'qa', // dynamic sampling bias to keep transactions
75+
tunnel: `http://localhost:3031/`, // proxy server
76+
tracesSampleRate: 1.0,
77+
sendDefaultPii: true,
78+
transportOptions: {
79+
// We are doing a lot of events at once in this test
80+
bufferSize: 1000,
81+
},
82+
}),
83+
{
84+
async fetch(request, env) {
85+
const url = new URL(request.url);
86+
switch (url.pathname) {
8687
case '/rpc/throwException':
8788
{
8889
const id = env.MY_DURABLE_OBJECT.idFromName('foo');
@@ -105,7 +106,7 @@ export default Sentry.withSentry(
105106
return stub.fetch(new Request(url, request));
106107
}
107108
}
108-
return new Response('Hello World!');
109-
},
110-
} satisfies ExportedHandler<Env>,
109+
return new Response('Hello World!');
110+
},
111+
} satisfies ExportedHandler<Env>,
111112
);

packages/cloudflare/src/client.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,29 @@ interface BaseCloudflareOptions {
6868
* @default false
6969
*/
7070
skipOpenTelemetrySetup?: boolean;
71+
72+
/**
73+
* Enable instrumentation of prototype methods for DurableObjects.
74+
*
75+
* When `true`, the SDK will wrap all methods on the DurableObject prototype chain
76+
* to automatically create spans and capture errors for RPC method calls.
77+
*
78+
* When an array of strings is provided, only the specified method names will be instrumented.
79+
*
80+
* This feature adds runtime overhead as it wraps methods at the prototype level.
81+
* Only enable this if you need automatic instrumentation of prototype methods.
82+
*
83+
* @default false
84+
* @example
85+
* ```ts
86+
* // Instrument all prototype methods
87+
* instrumentPrototypeMethods: true
88+
*
89+
* // Instrument only specific methods
90+
* instrumentPrototypeMethods: ['myMethod', 'anotherMethod']
91+
* ```
92+
*/
93+
instrumentPrototypeMethods?: boolean | string[];
7194
}
7295

7396
/**

0 commit comments

Comments
 (0)