From ac55737c106cc4733240a400846b5e9833291413 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Thu, 14 Aug 2025 17:30:13 +0200 Subject: [PATCH 1/8] fix(cloudflare): Avoid breaking rpc calls when wrapping DurableObjects From a97426204c52523c184ec96ca423d8766f8f110a Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Thu, 14 Aug 2025 17:30:24 +0200 Subject: [PATCH 2/8] Add integration tests for DurableObjects --- .../suites/tracing/durableobject/index.ts | 44 +++++++++++++++++++ .../suites/tracing/durableobject/test.ts | 27 ++++++++++++ .../tracing/durableobject/wrangler.jsonc | 23 ++++++++++ 3 files changed, 94 insertions(+) create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/test.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/wrangler.jsonc diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts new file mode 100644 index 000000000000..2ddb0da4815c --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts @@ -0,0 +1,44 @@ +import * as Sentry from '@sentry/cloudflare'; +import { DurableObject } from 'cloudflare:workers'; + +interface Env { + SENTRY_DSN: string; + TEST_DURABLE_OBJECT: DurableObjectNamespace; +} + +class TestDurableObjectBase extends DurableObject { + public constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + } + + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + async sayHello(name: string): Promise { + return `Hello, ${name}`; + } +} + +export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry( + (env: Env) => ({ + dsn: env.SENTRY_DSN, + tracesSampleRate: 1.0, + beforeSendTransaction: transaction => { + console.log('beforeSendTransaction', transaction); + return transaction; + }, + }), + TestDurableObjectBase, +); + +export default { + async fetch(request, env): Promise { + 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'); + }, +}; diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/test.ts new file mode 100644 index 000000000000..cfb6841004a9 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/test.ts @@ -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(); +}); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/wrangler.jsonc new file mode 100644 index 000000000000..8f27c3af7a22 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/wrangler.jsonc @@ -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://932e620ee3921c3b4a61c72558ad88ce@o447951.ingest.us.sentry.io/4509553159831552" + } +} From aa04f4b9b8ba40a6e788dc7a78a8a34bfc2d36da Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Thu, 14 Aug 2025 21:20:06 +0200 Subject: [PATCH 3/8] Instrument methods at prototype level not instance level --- packages/cloudflare/src/durableobject.ts | 108 ++++++++++++------ .../cloudflare/test/durableobject.test.ts | 15 ++- 2 files changed, 85 insertions(+), 38 deletions(-) diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index 4efaf33c9b1c..53a5a881f034 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -110,8 +110,7 @@ function wrapMethodWithSentry( } : {}; - // Only create these spans if they have a parent span. - return startSpan({ name: wrapperOptions.spanName, attributes, onlyIfParent: true }, () => { + return startSpan({ name: wrapperOptions.spanName, attributes }, () => { try { const result = Reflect.apply(target, thisArg, args); @@ -273,46 +272,87 @@ export function instrumentDurableObjectWithSentry< ); } } - const instrumentedPrototype = instrumentPrototype(target, options, context); - Object.setPrototypeOf(obj, instrumentedPrototype); + + // Store context and options on the instance for prototype methods to access + Object.defineProperty(obj, '__SENTRY_CONTEXT__', { + value: context, + enumerable: false, + writable: false, + configurable: false, + }); + + Object.defineProperty(obj, '__SENTRY_OPTIONS__', { + value: options, + enumerable: false, + writable: false, + configurable: false, + }); + + instrumentPrototype(target); return obj; }, }); } -function instrumentPrototype( - target: T, - options: CloudflareOptions, - context: MethodWrapperOptions['context'], -): T { - return new Proxy(target.prototype, { - get(target, prop, receiver) { - const value = Reflect.get(target, prop, receiver); - if (prop === 'constructor' || typeof value !== 'function') { - return value; +function instrumentPrototype(target: T): void { + const proto = target.prototype; + + // Get all methods from the prototype chain + const methodNames = new Set(); + let current = proto; + + while (current && current !== Object.prototype) { + Object.getOwnPropertyNames(current).forEach(name => { + if (name !== 'constructor' && typeof current[name] === 'function') { + methodNames.add(name); + } + }); + current = Object.getPrototypeOf(current); + } + + // Instrument each method on the prototype + methodNames.forEach(methodName => { + const originalMethod = proto[methodName]; + + if (!originalMethod || isInstrumented(originalMethod)) { + return; + } + + // Create a wrapper that gets context/options from the instance at runtime + const wrappedMethod = function (this: any, ...args: any[]) { + const instanceContext = this.__SENTRY_CONTEXT__; + const instanceOptions = this.__SENTRY_OPTIONS__; + + if (!instanceOptions) { + // Fallback to original method if no Sentry data found + return originalMethod.apply(this, args); } - const wrapped = wrapMethodWithSentry( - { options, context, spanName: prop.toString(), spanOp: 'rpc' }, - value, + + // Use the existing wrapper but with instance-specific context/options + const wrapper = wrapMethodWithSentry( + { + options: instanceOptions, + context: instanceContext, + spanName: methodName, + spanOp: 'rpc', + }, + originalMethod, undefined, - true, + true, // noMark = true since we'll mark the prototype method ); - const instrumented = new Proxy(wrapped, { - get(target, p, receiver) { - if ('__SENTRY_INSTRUMENTED__' === p) { - return true; - } - return Reflect.get(target, p, receiver); - }, - }); - Object.defineProperty(receiver, prop, { - value: instrumented, - enumerable: true, - writable: true, - configurable: true, - }); - return instrumented; - }, + + return wrapper.apply(this, args); + }; + + markAsInstrumented(wrappedMethod); + + // Replace the prototype method + Object.defineProperty(proto, methodName, { + value: wrappedMethod, + enumerable: false, + writable: true, + configurable: true, + }); }); } diff --git a/packages/cloudflare/test/durableobject.test.ts b/packages/cloudflare/test/durableobject.test.ts index 2add5dde9343..7768adb6d08b 100644 --- a/packages/cloudflare/test/durableobject.test.ts +++ b/packages/cloudflare/test/durableobject.test.ts @@ -46,8 +46,12 @@ describe('instrumentDurableObjectWithSentry', () => { }); it('Instruments prototype methods without "sticking" to the options', () => { + const mockContext = { + waitUntil: vi.fn(), + } as any; + const mockEnv = {} as any; // Environment mock const initCore = vi.spyOn(SentryCore, 'initAndBind'); - vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined); + const getClientSpy = vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined); const options = vi .fn() .mockReturnValueOnce({ @@ -59,8 +63,12 @@ describe('instrumentDurableObjectWithSentry', () => { const testClass = class { method() {} }; - (Reflect.construct(instrumentDurableObjectWithSentry(options, testClass as any), []) as any).method(); - (Reflect.construct(instrumentDurableObjectWithSentry(options, testClass as any), []) as any).method(); + const instance1 = Reflect.construct(instrumentDurableObjectWithSentry(options, testClass as any), [mockContext, mockEnv]) as any; + instance1.method(); + + const instance2 = Reflect.construct(instrumentDurableObjectWithSentry(options, testClass as any), [mockContext, mockEnv]) as any; + instance2.method(); + expect(initCore).nthCalledWith(1, expect.any(Function), expect.objectContaining({ orgId: 1 })); expect(initCore).nthCalledWith(2, expect.any(Function), expect.objectContaining({ orgId: 2 })); }); @@ -83,7 +91,6 @@ describe('instrumentDurableObjectWithSentry', () => { }; const instrumented = instrumentDurableObjectWithSentry(vi.fn(), testClass as any); const obj = Reflect.construct(instrumented, []); - expect(Object.getPrototypeOf(obj), 'Prototype is instrumented').not.toBe(testClass.prototype); for (const method_name of [ 'propertyFunction', 'fetch', From 996b2a96248405a54f725cc85c607507c63a701d Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 25 Aug 2025 14:40:22 +0200 Subject: [PATCH 4/8] Fix type issues --- .../suites/tracing/durableobject/index.ts | 6 +----- packages/cloudflare/src/durableobject.ts | 20 +++++++++++-------- .../cloudflare/test/durableobject.test.ts | 16 ++++++++++----- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts index 2ddb0da4815c..9edf58536541 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts @@ -21,16 +21,12 @@ export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - beforeSendTransaction: transaction => { - console.log('beforeSendTransaction', transaction); - return transaction; - }, }), TestDurableObjectBase, ); export default { - async fetch(request, env): Promise { + async fetch(request: Request, env: Env): Promise { const id: DurableObjectId = env.TEST_DURABLE_OBJECT.idFromName('test'); const stub = env.TEST_DURABLE_OBJECT.get(id) as unknown as TestDurableObjectBase; diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index 53a5a881f034..b2cd76f764e3 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -304,7 +304,7 @@ function instrumentPrototype(target: T): void { while (current && current !== Object.prototype) { Object.getOwnPropertyNames(current).forEach(name => { - if (name !== 'constructor' && typeof current[name] === 'function') { + if (name !== 'constructor' && typeof (current as Record)[name] === 'function') { methodNames.add(name); } }); @@ -313,20 +313,24 @@ function instrumentPrototype(target: T): void { // Instrument each method on the prototype methodNames.forEach(methodName => { - const originalMethod = proto[methodName]; + const originalMethod = (proto as Record)[methodName]; if (!originalMethod || isInstrumented(originalMethod)) { return; } // Create a wrapper that gets context/options from the instance at runtime - const wrappedMethod = function (this: any, ...args: any[]) { - const instanceContext = this.__SENTRY_CONTEXT__; - const instanceOptions = this.__SENTRY_OPTIONS__; + const wrappedMethod = function (this: any, ...args: any[]): unknown { + const thisWithSentry = this as { + __SENTRY_CONTEXT__: DurableObjectState; + __SENTRY_OPTIONS__: CloudflareOptions; + }; + const instanceContext = thisWithSentry.__SENTRY_CONTEXT__; + const instanceOptions = thisWithSentry.__SENTRY_OPTIONS__; if (!instanceOptions) { // Fallback to original method if no Sentry data found - return originalMethod.apply(this, args); + return (originalMethod as (...args: any[]) => any).apply(this, args); } // Use the existing wrapper but with instance-specific context/options @@ -337,12 +341,12 @@ function instrumentPrototype(target: T): void { spanName: methodName, spanOp: 'rpc', }, - originalMethod, + originalMethod as (...args: any[]) => any, undefined, true, // noMark = true since we'll mark the prototype method ); - return wrapper.apply(this, args); + return (wrapper as (...args: any[]) => any).apply(this, args); }; markAsInstrumented(wrappedMethod); diff --git a/packages/cloudflare/test/durableobject.test.ts b/packages/cloudflare/test/durableobject.test.ts index 7768adb6d08b..ca0026ee025d 100644 --- a/packages/cloudflare/test/durableobject.test.ts +++ b/packages/cloudflare/test/durableobject.test.ts @@ -51,7 +51,7 @@ describe('instrumentDurableObjectWithSentry', () => { } as any; const mockEnv = {} as any; // Environment mock const initCore = vi.spyOn(SentryCore, 'initAndBind'); - const getClientSpy = vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined); + vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined); const options = vi .fn() .mockReturnValueOnce({ @@ -63,12 +63,18 @@ describe('instrumentDurableObjectWithSentry', () => { const testClass = class { method() {} }; - const instance1 = Reflect.construct(instrumentDurableObjectWithSentry(options, testClass as any), [mockContext, mockEnv]) as any; + const instance1 = Reflect.construct(instrumentDurableObjectWithSentry(options, testClass as any), [ + mockContext, + mockEnv, + ]) as any; instance1.method(); - - const instance2 = Reflect.construct(instrumentDurableObjectWithSentry(options, testClass as any), [mockContext, mockEnv]) as any; + + const instance2 = Reflect.construct(instrumentDurableObjectWithSentry(options, testClass as any), [ + mockContext, + mockEnv, + ]) as any; instance2.method(); - + expect(initCore).nthCalledWith(1, expect.any(Function), expect.objectContaining({ orgId: 1 })); expect(initCore).nthCalledWith(2, expect.any(Function), expect.objectContaining({ orgId: 2 })); }); From 2ea28b1c9d99b2583eae16e5def7d5f7a016dbde Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 25 Aug 2025 17:07:36 +0200 Subject: [PATCH 5/8] Add option to instrument rpc methods --- packages/cloudflare/src/client.ts | 23 ++++ packages/cloudflare/src/durableobject.ts | 14 ++- .../cloudflare/test/durableobject.test.ts | 101 +++++++++++++++++- 3 files changed, 131 insertions(+), 7 deletions(-) diff --git a/packages/cloudflare/src/client.ts b/packages/cloudflare/src/client.ts index b6b4695835ba..2de5147d3d5a 100644 --- a/packages/cloudflare/src/client.ts +++ b/packages/cloudflare/src/client.ts @@ -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[]; } /** diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index b2cd76f764e3..bda7a9aa3538 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -288,14 +288,16 @@ export function instrumentDurableObjectWithSentry< configurable: false, }); - instrumentPrototype(target); + if (options?.instrumentPrototypeMethods) { + instrumentPrototype(target, options.instrumentPrototypeMethods); + } return obj; }, }); } -function instrumentPrototype(target: T): void { +function instrumentPrototype(target: T, methodsToInstrument: boolean | string[]): void { const proto = target.prototype; // Get all methods from the prototype chain @@ -311,6 +313,9 @@ function instrumentPrototype(target: T): void { current = Object.getPrototypeOf(current); } + // Create a set for efficient lookups when methodsToInstrument is an array + const methodsToInstrumentSet = Array.isArray(methodsToInstrument) ? new Set(methodsToInstrument) : null; + // Instrument each method on the prototype methodNames.forEach(methodName => { const originalMethod = (proto as Record)[methodName]; @@ -319,6 +324,11 @@ function instrumentPrototype(target: T): void { return; } + // If methodsToInstrument is an array, only instrument methods in that set + if (methodsToInstrumentSet && !methodsToInstrumentSet.has(methodName)) { + return; + } + // Create a wrapper that gets context/options from the instance at runtime const wrappedMethod = function (this: any, ...args: any[]): unknown { const thisWithSentry = this as { diff --git a/packages/cloudflare/test/durableobject.test.ts b/packages/cloudflare/test/durableobject.test.ts index ca0026ee025d..a845a58cf5ef 100644 --- a/packages/cloudflare/test/durableobject.test.ts +++ b/packages/cloudflare/test/durableobject.test.ts @@ -10,7 +10,7 @@ describe('instrumentDurableObjectWithSentry', () => { }); it('Generic functionality', () => { - const options = vi.fn(); + const options = vi.fn().mockReturnValue({}); const instrumented = instrumentDurableObjectWithSentry(options, vi.fn()); expect(instrumented).toBeTypeOf('function'); expect(() => Reflect.construct(instrumented, [])).not.toThrow(); @@ -23,7 +23,7 @@ describe('instrumentDurableObjectWithSentry', () => { return 'sync-result'; } }; - const obj = Reflect.construct(instrumentDurableObjectWithSentry(vi.fn(), testClass as any), []) as any; + const obj = Reflect.construct(instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any), []) as any; expect(obj.method).toBe(obj.method); const result = obj.method(); @@ -37,7 +37,7 @@ describe('instrumentDurableObjectWithSentry', () => { return 'async-result'; } }; - const obj = Reflect.construct(instrumentDurableObjectWithSentry(vi.fn(), testClass as any), []) as any; + const obj = Reflect.construct(instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any), []) as any; expect(obj.asyncMethod).toBe(obj.asyncMethod); const result = obj.asyncMethod(); @@ -56,9 +56,11 @@ describe('instrumentDurableObjectWithSentry', () => { .fn() .mockReturnValueOnce({ orgId: 1, + instrumentPrototypeMethods: true, }) .mockReturnValueOnce({ orgId: 2, + instrumentPrototypeMethods: true, }); const testClass = class { method() {} @@ -79,7 +81,7 @@ describe('instrumentDurableObjectWithSentry', () => { expect(initCore).nthCalledWith(2, expect.any(Function), expect.objectContaining({ orgId: 2 })); }); - it('All available durable object methods are instrumented', () => { + it('All available durable object methods are instrumented when instrumentPrototypeMethods is enabled', () => { const testClass = class { propertyFunction = vi.fn(); @@ -95,7 +97,7 @@ describe('instrumentDurableObjectWithSentry', () => { webSocketError() {} }; - const instrumented = instrumentDurableObjectWithSentry(vi.fn(), testClass as any); + const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({ instrumentPrototypeMethods: true }), testClass as any); const obj = Reflect.construct(instrumented, []); for (const method_name of [ 'propertyFunction', @@ -135,4 +137,93 @@ describe('instrumentDurableObjectWithSentry', () => { await Promise.all(waitUntil.mock.calls.map(([p]) => p)); expect(flush).toBeCalled(); }); + + describe('instrumentPrototypeMethods option', () => { + it('does not instrument prototype methods when option is not set', () => { + const testClass = class { + prototypeMethod() { + return 'prototype-result'; + } + }; + const options = vi.fn().mockReturnValue({}); + const instrumented = instrumentDurableObjectWithSentry(options, testClass as any); + const obj = Reflect.construct(instrumented, []) as any; + + expect(isInstrumented(obj.prototypeMethod)).toBeFalsy(); + }); + + it('does not instrument prototype methods when option is false', () => { + const testClass = class { + prototypeMethod() { + return 'prototype-result'; + } + }; + const options = vi.fn().mockReturnValue({ instrumentPrototypeMethods: false }); + const instrumented = instrumentDurableObjectWithSentry(options, testClass as any); + const obj = Reflect.construct(instrumented, []) as any; + + expect(isInstrumented(obj.prototypeMethod)).toBeFalsy(); + }); + + it('instruments all prototype methods when option is true', () => { + const testClass = class { + methodOne() { + return 'one'; + } + methodTwo() { + return 'two'; + } + }; + const options = vi.fn().mockReturnValue({ instrumentPrototypeMethods: true }); + const instrumented = instrumentDurableObjectWithSentry(options, testClass as any); + const obj = Reflect.construct(instrumented, []) as any; + + expect(isInstrumented(obj.methodOne)).toBeTruthy(); + expect(isInstrumented(obj.methodTwo)).toBeTruthy(); + }); + + it('instruments only specified methods when option is array', () => { + const testClass = class { + methodOne() { + return 'one'; + } + methodTwo() { + return 'two'; + } + methodThree() { + return 'three'; + } + }; + const options = vi.fn().mockReturnValue({ instrumentPrototypeMethods: ['methodOne', 'methodThree'] }); + const instrumented = instrumentDurableObjectWithSentry(options, testClass as any); + const obj = Reflect.construct(instrumented, []) as any; + + expect(isInstrumented(obj.methodOne)).toBeTruthy(); + expect(isInstrumented(obj.methodTwo)).toBeFalsy(); + expect(isInstrumented(obj.methodThree)).toBeTruthy(); + }); + + it('still instruments instance methods regardless of prototype option', () => { + const testClass = class { + propertyFunction = vi.fn(); + + fetch() {} + alarm() {} + webSocketMessage() {} + webSocketClose() {} + webSocketError() {} + }; + const options = vi.fn().mockReturnValue({ instrumentPrototypeMethods: false }); + const instrumented = instrumentDurableObjectWithSentry(options, testClass as any); + const obj = Reflect.construct(instrumented, []) as any; + + // Instance methods should still be instrumented + expect(isInstrumented(obj.propertyFunction)).toBeTruthy(); + expect(isInstrumented(obj.fetch)).toBeTruthy(); + expect(isInstrumented(obj.alarm)).toBeTruthy(); + expect(isInstrumented(obj.webSocketMessage)).toBeTruthy(); + expect(isInstrumented(obj.webSocketClose)).toBeTruthy(); + expect(isInstrumented(obj.webSocketError)).toBeTruthy(); + }); + }); }); From 878052c51ae70e7de69d3c29960cb0cb457f0703 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 25 Aug 2025 17:13:39 +0200 Subject: [PATCH 6/8] Fix integration tests --- .../suites/tracing/durableobject/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts index 9edf58536541..74ce2cbbdac4 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts @@ -21,6 +21,7 @@ export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, + instrumentPrototypeMethods: true, }), TestDurableObjectBase, ); From adcd67dbc4818d3b3f6455b877fd6c2e1bc8b378 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 25 Aug 2025 17:48:29 +0200 Subject: [PATCH 7/8] Add changelog entry --- CHANGELOG.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c90e60a23b06..23c1d8555909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,38 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott -Work in this release was contributed by @Karibash. Thank you for your contribution! +### 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 a list of methods you want to instrument: + +```js +class MyDurableObjectBase extends DurableObject { + 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, +); +``` + + Work in this release was contributed by @Karibash. Thank you for your contribution! ## 10.5.0 From 0b821adb9bc9a4aac68412d3d227345b1cb4dd61 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 25 Aug 2025 18:12:22 +0200 Subject: [PATCH 8/8] Formatting fix --- CHANGELOG.md | 2 +- packages/cloudflare/test/durableobject.test.ts | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23c1d8555909..0670b5d012b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( ); ``` - Work in this release was contributed by @Karibash. Thank you for your contribution! +Work in this release was contributed by @Karibash. Thank you for your contribution! ## 10.5.0 diff --git a/packages/cloudflare/test/durableobject.test.ts b/packages/cloudflare/test/durableobject.test.ts index a845a58cf5ef..ce794dc7fb69 100644 --- a/packages/cloudflare/test/durableobject.test.ts +++ b/packages/cloudflare/test/durableobject.test.ts @@ -23,7 +23,10 @@ describe('instrumentDurableObjectWithSentry', () => { return 'sync-result'; } }; - const obj = Reflect.construct(instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any), []) as any; + const obj = Reflect.construct( + instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any), + [], + ) as any; expect(obj.method).toBe(obj.method); const result = obj.method(); @@ -37,7 +40,10 @@ describe('instrumentDurableObjectWithSentry', () => { return 'async-result'; } }; - const obj = Reflect.construct(instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any), []) as any; + const obj = Reflect.construct( + instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any), + [], + ) as any; expect(obj.asyncMethod).toBe(obj.asyncMethod); const result = obj.asyncMethod(); @@ -97,7 +103,10 @@ describe('instrumentDurableObjectWithSentry', () => { webSocketError() {} }; - const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({ instrumentPrototypeMethods: true }), testClass as any); + const instrumented = instrumentDurableObjectWithSentry( + vi.fn().mockReturnValue({ instrumentPrototypeMethods: true }), + testClass as any, + ); const obj = Reflect.construct(instrumented, []); for (const method_name of [ 'propertyFunction',