Skip to content
33 changes: 31 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,37 @@

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

### Important Changes

- feat(cloudflare): Add `instrumentPrototypeMethods` option to instrument RPC methods for DurableObjects ([#17424](https://github.com/getsentry/sentry-javascript/pull/17424))

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:

```js
class MyDurableObjectBase extends DurableObject<Env> {
method1() {
// ...
}

method2() {
// ...
}

method3() {
// ...
}
}
// Export your named class as defined in your wrangler config
export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: "https://ac49b7af3017c458bd12dab9b3328bfc@o4508482761982032.ingest.de.sentry.io/4508482780987481",
tracesSampleRate: 1.0,
instrumentPrototypeMethods: ['method1', 'method3'],
}),
MyDurableObjectBase,
);
```

## 10.6.0

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

</details>

Work in this release was contributed by @Karibash. Thank you for your contribution!

## 10.5.0

- feat(core): better cause data extraction ([#17375](https://github.com/getsentry/sentry-javascript/pull/17375))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
TEST_DURABLE_OBJECT: DurableObjectNamespace;
}

class TestDurableObjectBase extends DurableObject<Env> {
public constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
}

// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility
async sayHello(name: string): Promise<string> {
return `Hello, ${name}`;
}
}

export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
instrumentPrototypeMethods: true,
}),
TestDurableObjectBase,
);

export default {
async fetch(request: Request, env: Env): Promise<Response> {
const id: DurableObjectId = env.TEST_DURABLE_OBJECT.idFromName('test');
const stub = env.TEST_DURABLE_OBJECT.get(id) as unknown as TestDurableObjectBase;

if (request.url.includes('hello')) {
const greeting = await stub.sayHello('world');
return new Response(greeting);
}

return new Response('Usual response');
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { expect, it } from 'vitest';
import { createRunner } from '../../../runner';

it('traces a durable object method', async () => {
const runner = createRunner(__dirname)
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1];
expect(transactionEvent).toEqual(
expect.objectContaining({
contexts: expect.objectContaining({
trace: expect.objectContaining({
op: 'rpc',
data: expect.objectContaining({
'sentry.op': 'rpc',
'sentry.origin': 'auto.faas.cloudflare_durableobjects',
}),
origin: 'auto.faas.cloudflare_durableobjects',
}),
}),
transaction: 'sayHello',
}),
);
})
.start();
await runner.makeRequest('get', '/hello');
await runner.completed();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "worker-name",
"main": "index.ts",
"compatibility_date": "2025-06-17",
"migrations": [
{
"new_sqlite_classes": ["TestDurableObject"],
"tag": "v1"
}
],
"durable_objects": {
"bindings": [
{
"class_name": "TestDurableObject",
"name": "TEST_DURABLE_OBJECT"
}
]
},
"compatibility_flags": ["nodejs_als"],
"vars": {
"SENTRY_DSN": "https://[email protected]/4509553159831552"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* Learn more at https://developers.cloudflare.com/workers/
*/
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from "cloudflare:workers";
import { DurableObject } from 'cloudflare:workers';

class MyDurableObjectBase extends DurableObject<Env> {
private throwOnExit = new WeakMap<WebSocket, Error>();
Expand Down Expand Up @@ -44,7 +44,7 @@ class MyDurableObjectBase extends DurableObject<Env> {
}

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

export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.E2E_TEST_DSN,
environment: 'qa', // dynamic sampling bias to keep transactions
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
sendDefaultPii: true,
transportOptions: {
// We are doing a lot of events at once in this test
bufferSize: 1000,
},
}),
MyDurableObjectBase,
(env: Env) => ({
dsn: env.E2E_TEST_DSN,
environment: 'qa', // dynamic sampling bias to keep transactions
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
sendDefaultPii: true,
transportOptions: {
// We are doing a lot of events at once in this test
bufferSize: 1000,
},
instrumentPrototypeMethods: true,
}),
MyDurableObjectBase,
);

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.E2E_TEST_DSN,
environment: 'qa', // dynamic sampling bias to keep transactions
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
sendDefaultPii: true,
transportOptions: {
// We are doing a lot of events at once in this test
bufferSize: 1000,
},
}),
{
async fetch(request, env) {
const url = new URL(request.url);
switch (url.pathname) {
(env: Env) => ({
dsn: env.E2E_TEST_DSN,
environment: 'qa', // dynamic sampling bias to keep transactions
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
sendDefaultPii: true,
transportOptions: {
// We are doing a lot of events at once in this test
bufferSize: 1000,
},
}),
{
async fetch(request, env) {
const url = new URL(request.url);
switch (url.pathname) {
case '/rpc/throwException':
{
const id = env.MY_DURABLE_OBJECT.idFromName('foo');
Expand All @@ -105,7 +106,7 @@ export default Sentry.withSentry(
return stub.fetch(new Request(url, request));
}
}
return new Response('Hello World!');
},
} satisfies ExportedHandler<Env>,
return new Response('Hello World!');
},
} satisfies ExportedHandler<Env>,
);
23 changes: 23 additions & 0 deletions packages/cloudflare/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,29 @@ interface BaseCloudflareOptions {
* @default false
*/
skipOpenTelemetrySetup?: boolean;

/**
* Enable instrumentation of prototype methods for DurableObjects.
*
* When `true`, the SDK will wrap all methods on the DurableObject prototype chain
* to automatically create spans and capture errors for RPC method calls.
*
* When an array of strings is provided, only the specified method names will be instrumented.
*
* This feature adds runtime overhead as it wraps methods at the prototype level.
* Only enable this if you need automatic instrumentation of prototype methods.
*
* @default false
* @example
* ```ts
* // Instrument all prototype methods
* instrumentPrototypeMethods: true
*
* // Instrument only specific methods
* instrumentPrototypeMethods: ['myMethod', 'anotherMethod']
* ```
*/
instrumentPrototypeMethods?: boolean | string[];
}

/**
Expand Down
Loading
Loading