From 348b74b2dcbd9682a7e6f3b6e9743ee4b8194b4b Mon Sep 17 00:00:00 2001 From: Martijn Walraven Date: Wed, 8 Jul 2026 17:44:19 +0200 Subject: [PATCH] fix(cloudflare): Honor newTarget in instrumentDurableObjectWithSentry construct trap The construct trap created instances with `new target(...)`, ignoring newTarget. Subclasses of the instrumented class - such as miniflare's local-explorer wrapper, which extends every user Durable Object class to add introspection methods - lost their own prototype: subclass methods disappeared and instanceof checks against the subclass failed. Construct through Reflect.construct with newTarget so the subclass prototype is preserved. Co-Authored-By: Claude Fable 5 --- packages/cloudflare/src/durableobject.ts | 7 +++-- .../cloudflare/test/durableobject.test.ts | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index e91f2fbd4b55..e312c31b59ad 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -51,13 +51,16 @@ export function instrumentDurableObjectWithSentry< C extends new (state: DurableObjectState, env: E) => T, >(optionsCallback: (env: E) => CloudflareOptions, DurableObjectClass: C): C { return new Proxy(DurableObjectClass, { - construct(target, [ctx, env]) { + construct(target, [ctx, env], newTarget) { setAsyncLocalStorageAsyncContextStrategy(); const context = instrumentContext(ctx); const options = getFinalOptions(optionsCallback(env), env); const instrumentedEnv = instrumentEnv(env, options); - const obj = new target(context, instrumentedEnv); + // Pass `newTarget` so that subclasses of the instrumented class (e.g. the wrapper classes + // created by wrangler's local dev tooling or `@cloudflare/vitest-pool-workers`) keep their + // own prototype — otherwise subclass methods disappear and `instanceof` checks break. + const obj = Reflect.construct(target, [context, instrumentedEnv], newTarget) as T; // These are the methods that are available on a Durable Object // ref: https://developers.cloudflare.com/durable-objects/api/base/ diff --git a/packages/cloudflare/test/durableobject.test.ts b/packages/cloudflare/test/durableobject.test.ts index 6bcf5c94675f..83b473fabe4d 100644 --- a/packages/cloudflare/test/durableobject.test.ts +++ b/packages/cloudflare/test/durableobject.test.ts @@ -232,6 +232,35 @@ describe('instrumentDurableObjectWithSentry', () => { expect(obj.constructor).toBe(testClass); }); + it('honors newTarget so that subclasses of the instrumented class keep their prototype', () => { + const testClass = class { + fetch() { + return new Response('fetch'); + } + + alarm() {} + }; + const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any); + + // Dev tooling such as wrangler or `@cloudflare/vitest-pool-workers` subclasses the exported + // (instrumented) class, so the construct trap is invoked with the subclass as `newTarget`. + class Subclass extends instrumented { + subclassMethod() { + return 'subclass-result'; + } + } + + const obj = Reflect.construct(Subclass, []); + + // The subclass prototype must be preserved + expect(obj).toBeInstanceOf(Subclass); + expect(obj.subclassMethod()).toBe('subclass-result'); + + // Built-in DO methods are still instrumented + expect(getInstrumented(obj.fetch)).toBeTruthy(); + expect(getInstrumented(obj.alarm)).toBeTruthy(); + }); + it('Does not instrument RPC methods when instrumentPrototypeMethods is not set', () => { const testClass = class { rpcMethod() {