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 659b04a3f488..170b2fe5d499 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts @@ -50,23 +50,30 @@ export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry( TestDurableObjectBase, ); -export default { - 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; +export default Sentry.withSentry( + (env: Env) => ({ + dsn: env.SENTRY_DSN, + tracesSampleRate: 1.0, + enableRpcTracePropagation: true, + }), + { + 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; - if (request.url.includes('hello')) { - const greeting = await stub.sayHello('world'); - return new Response(greeting); - } + if (request.url.includes('hello')) { + const greeting = await stub.sayHello('world'); + return new Response(greeting); + } - // Test endpoint that modifies and reads a private field via RPC - if (request.url.includes('custom-greeting')) { - await stub.setGreeting('Howdy'); - const greeting = await stub.sayHello('partner'); - return new Response(greeting); - } + // Test endpoint that modifies and reads a private field via RPC + if (request.url.includes('custom-greeting')) { + await stub.setGreeting('Howdy'); + const greeting = await stub.sayHello('partner'); + return new Response(greeting); + } - return new Response('Usual response'); - }, -}; + return new Response('Usual response'); + }, + } satisfies ExportedHandler, +); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/index.html b/dev-packages/e2e-tests/test-applications/cloudflare-agent/index.html new file mode 100644 index 000000000000..6ce1b364b889 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/index.html @@ -0,0 +1,13 @@ + + + + + + + temp-cloudflare-react + + +
+ + + diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-agent/package.json new file mode 100644 index 000000000000..6c834c1d9f47 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/package.json @@ -0,0 +1,42 @@ +{ + "name": "cloudflare-agent", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "cf-typegen": "wrangler types --include-runtime=false", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test:dev && pnpm test:prod", + "test:prod": "TEST_ENV=production playwright test", + "test:dev": "TEST_ENV=development playwright test" + }, + "dependencies": { + "@cloudflare/ai-chat": "^0.7.1", + "@sentry/cloudflare": "^10.53.1", + "agents": "^0.13.1", + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@playwright/test": "~1.56.0", + "@cloudflare/vite-plugin": "^1.37.2", + "@cloudflare/workers-types": "^4.20260520.1", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@types/node": "^24.12.4", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.2", + "globals": "^17.6.0", + "typescript": "~6.0.3", + "vite": "^8.0.14", + "wrangler": "^4.93.0", + "ws": "^8.20.1" + }, + "volta": { + "node": "24.15.0", + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/playwright.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-agent/playwright.config.ts new file mode 100644 index 000000000000..cbd1dadb7d4f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/playwright.config.ts @@ -0,0 +1,10 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +export default getPlaywrightConfig({ + testDir: './tests', + port: 4173, + startCommand: 'pnpm preview', + use: { + baseURL: 'http://localhost:4173', + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/src/App.tsx b/dev-packages/e2e-tests/test-applications/cloudflare-agent/src/App.tsx new file mode 100644 index 000000000000..5a5a7ff9fe30 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/src/App.tsx @@ -0,0 +1,47 @@ +import { useState } from 'react'; +import { useAgent } from 'agents/react'; + +function App() { + const [greeting, setGreeting] = useState(''); + const [connected, setConnected] = useState(false); + + const agent = useAgent({ + agent: 'my-agent', + name: 'user-123', + onOpen: () => setConnected(true), + onClose: () => setConnected(false), + }); + + const handleGreet = async () => { + if (!connected) { + setGreeting('Not connected yet...'); + return; + } + try { + const result = await agent.call('greet', ['World']); + setGreeting(result as string); + } catch (err) { + setGreeting(`Error: ${err}`); + console.error('Agent call failed:', err); + } + }; + + return ( +
+ + {greeting &&

{greeting}

} +

{connected ? 'Connected' : 'Connecting...'}

+
+ ); +} + +export default App; diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/src/main.tsx b/dev-packages/e2e-tests/test-applications/cloudflare-agent/src/main.tsx new file mode 100644 index 000000000000..e17d50b10310 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-agent/start-event-proxy.mjs new file mode 100644 index 000000000000..882426136853 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/start-event-proxy.mjs @@ -0,0 +1,3 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ port: 3031, proxyServerName: 'cloudflare-agent' }); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/tests/callable.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-agent/tests/callable.test.ts new file mode 100644 index 000000000000..7bbb181ad192 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/tests/callable.test.ts @@ -0,0 +1,64 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('@callable() methods work correctly with Sentry instrumentDurableObjectWithSentry', async ({ page, baseURL }) => { + const transactionPromise = waitForTransaction('cloudflare-agent', transactionEvent => { + console.log(transactionEvent); + return ( + transactionEvent.transaction === 'GET /agents/my-agent/user-123' && + transactionEvent.contexts?.trace?.parent_span_id !== undefined + ); + }); + + await page.goto(baseURL!); + + await expect(page.getByText('Connected')).toBeVisible(); + await page.getByRole('button', { name: 'Call Agent' }).click(); + await expect(page.getByText('Hello, World!')).toBeVisible(); + + const transaction = await transactionPromise; + + expect(transaction).toEqual({ + contexts: { + trace: { + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: expect.any(Object), + op: 'http.server', + status: 'ok', + origin: 'auto.http.cloudflare', + }, + cloud_resource: { 'cloud.provider': 'cloudflare' }, + culture: { timezone: expect.any(String) }, + runtime: { name: 'cloudflare' }, + }, + spans: expect.arrayContaining([ + expect.objectContaining({ + op: 'db', + description: 'durable_object_storage_get', + }), + ]), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: 'GET /agents/my-agent/user-123', + type: 'transaction', + request: { + headers: expect.any(Object), + method: 'GET', + url: expect.stringContaining('/agents/my-agent/user-123'), + query_string: expect.any(String), + }, + transaction_info: { source: 'url' }, + platform: 'javascript', + event_id: expect.stringMatching(/[a-f0-9]{32}/), + environment: expect.any(String), + release: expect.any(String), + sdk: { + integrations: expect.any(Array), + name: 'sentry.javascript.cloudflare', + version: expect.any(String), + packages: expect.any(Array), + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/tsconfig.app.json b/dev-packages/e2e-tests/test-applications/cloudflare-agent/tsconfig.app.json new file mode 100644 index 000000000000..7f42e5f7cd2e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-agent/tsconfig.json new file mode 100644 index 000000000000..ec7f8daabfda --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/tsconfig.json @@ -0,0 +1,14 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.worker.json" + } + ] +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/tsconfig.node.json b/dev-packages/e2e-tests/test-applications/cloudflare-agent/tsconfig.node.json new file mode 100644 index 000000000000..d3c52ea64c6c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/tsconfig.worker.json b/dev-packages/e2e-tests/test-applications/cloudflare-agent/tsconfig.worker.json new file mode 100644 index 000000000000..7cff07bade2f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/tsconfig.worker.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.node.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.worker.tsbuildinfo", + "types": ["@cloudflare/workers-types", "vite/client"], + "erasableSyntaxOnly": false + }, + "include": ["./worker-configuration.d.ts", "worker"] +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/vite.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-agent/vite.config.ts new file mode 100644 index 000000000000..4bdcb8712920 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { cloudflare } from '@cloudflare/vite-plugin'; +import agents from 'agents/vite'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [agents(), react(), cloudflare()], +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/worker-configuration.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-agent/worker-configuration.d.ts new file mode 100644 index 000000000000..7e95ce232f43 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/worker-configuration.d.ts @@ -0,0 +1,21 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types --include-runtime=false` (hash: 603190983556d18d2fa32cebc898cb0b) +interface __BaseEnv_Env { + CF_VERSION_METADATA: WorkerVersionMetadata; + E2E_TEST_DSN: string; + MyAgent: DurableObjectNamespace; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import('./worker/index'); + durableNamespaces: 'MyAgent'; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} +type StringifyValues> = { + [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; +}; +declare namespace NodeJS { + interface ProcessEnv extends StringifyValues> {} +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/worker/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-agent/worker/index.ts new file mode 100644 index 000000000000..fa795b85d8f0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/worker/index.ts @@ -0,0 +1,39 @@ +import * as Sentry from '@sentry/cloudflare'; +import { routeAgentRequest, Agent, callable } from 'agents'; + +class MyBaseAgent extends Agent { + @callable() + async greet(name: string): Promise { + return `Hello, ${name}!`; + } +} + +export const MyAgent = Sentry.instrumentDurableObjectWithSentry( + (env: Env) => ({ + dsn: env.E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, + tracesSampleRate: 1, + enableRpcTracePropagation: true, + }), + MyBaseAgent, +); + +export default Sentry.withSentry( + (env: Env) => ({ + dsn: env.E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, + tracesSampleRate: 1, + enableRpcTracePropagation: true, + }), + { + async fetch(request: Request, env: Env): Promise { + const agentResponse = await routeAgentRequest(request, env); + + if (agentResponse) { + return agentResponse; + } + + return new Response(null, { status: 404 }); + }, + } satisfies ExportedHandler, +); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/cloudflare-agent/wrangler.jsonc new file mode 100644 index 000000000000..be99e5dee5c2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/wrangler.jsonc @@ -0,0 +1,34 @@ +/** + * For more details on how to configure Wrangler, refer to: + * https://developers.cloudflare.com/workers/wrangler/configuration/ + */ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "cloudflare-agent", + "main": "worker/index.ts", + "compatibility_date": "2026-05-20", + + "assets": { + "not_found_handling": "single-page-application", + }, + "secrets": { + "required": ["E2E_TEST_DSN"], + }, + + "observability": { + "enabled": true, + }, + + "upload_source_maps": true, + "compatibility_flags": ["nodejs_compat", "nodejs_als"], + + "durable_objects": { + "bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }], + }, + + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }], + + "version_metadata": { + "binding": "CF_VERSION_METADATA", + }, +} diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index 95068c7c9697..ba97fe31ee4c 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -8,18 +8,10 @@ import { instrumentEnv } from './instrumentations/worker/instrumentEnv'; import { getFinalOptions } from './options'; import { wrapRequestHandler } from './request'; import { instrumentContext } from './utils/instrumentContext'; +import { extractRpcMeta } from './utils/rpcMeta'; import { getEffectiveRpcPropagation } from './utils/rpcOptions'; import { type UncheckedMethod, wrapMethodWithSentry } from './wrapMethodWithSentry'; -const BUILT_IN_DO_METHODS = new Set([ - 'constructor', - 'fetch', - 'alarm', - 'webSocketError', - 'webSocketClose', - 'webSocketMessage', -]); - /** * Instruments a Durable Object class to capture errors and performance data. * @@ -139,51 +131,70 @@ export function instrumentDurableObjectWithSentry< : undefined; const allowSet = instrumentPrototypeMethods ? new Set(instrumentPrototypeMethods) : null; - // Return a Proxy that lazily wraps prototype methods on access. - // This avoids iterating the prototype chain at construction time — - // we only check if a property is an RPC method when it's accessed. - const rpcMethodCache = new Map(); + // When using the deprecated `instrumentPrototypeMethods` option, always create spans. + // When using the new `enableRpcTracePropagation`, only create spans when RPC metadata is present. + const alwaysTrace = options.instrumentPrototypeMethods !== undefined; + + // Return a Proxy that binds all methods to the original object and creates spans + // for RPC calls that have Sentry trace context propagated. + // Binding is required because frameworks may use private fields (babel WeakMap pattern), + // which fail if `this` is the Proxy instead of the original object. + const methodCache = new Map(); return new Proxy(obj, { get(proxyTarget, prop, receiver) { const value = Reflect.get(proxyTarget, prop, receiver); - if (typeof prop !== 'string' || BUILT_IN_DO_METHODS.has(prop)) { + if (typeof prop !== 'string' || typeof value !== 'function') { return value; } - const cached = rpcMethodCache.get(prop); + const cached = methodCache.get(prop); if (cached) { return cached; } + const boundMethod = (value as UncheckedMethod).bind(proxyTarget); + if ( - typeof value !== 'function' || + prop in Object.prototype || Object.prototype.hasOwnProperty.call(proxyTarget, prop) || - (allowSet && !allowSet.has(prop)) || - // Exclude inherited Object.prototype methods (toString, valueOf, etc.) - // These are not RPC methods and should not create spans - prop in Object.prototype + (allowSet && !allowSet.has(prop)) ) { - return value; - } + methodCache.set(prop, boundMethod); - // Bind the method to the original object to ensure private fields work correctly. - // When called via the Proxy, `this` would be the Proxy, but private fields require - // the original object. Bound functions ignore the thisArg passed via Reflect.apply. - const boundValue = (value as UncheckedMethod).bind(proxyTarget); + return boundMethod; + } - const wrapped = wrapMethodWithSentry( + // Pre-create the traced version + const tracedMethod = wrapMethodWithSentry( { options, context, spanName: prop, spanOp: 'rpc' }, - boundValue, + boundMethod, undefined, true, ); - rpcMethodCache.set(prop, wrapped); + // For deprecated `instrumentPrototypeMethods`, always trace. + // For new `enableRpcTracePropagation`, only trace when RPC metadata is present. + if (alwaysTrace) { + methodCache.set(prop, tracedMethod); + + return tracedMethod; + } + + // Wrapper that checks for Sentry RPC metadata at call time + const wrappedMethod = ((...args: unknown[]) => { + const { rpcMeta } = extractRpcMeta(args); + + // If Sentry RPC metadata is present, use the traced version (creates span) + // Otherwise, call the bound method directly (no span) + return rpcMeta ? tracedMethod(...args) : boundMethod(...args); + }) as UncheckedMethod; + + methodCache.set(prop, wrappedMethod); - return wrapped; + return wrappedMethod; }, }); }, diff --git a/packages/cloudflare/test/durableobject.test.ts b/packages/cloudflare/test/durableobject.test.ts index dc65cb44f6cc..3f6cb884a8c5 100644 --- a/packages/cloudflare/test/durableobject.test.ts +++ b/packages/cloudflare/test/durableobject.test.ts @@ -81,6 +81,27 @@ describe('instrumentDurableObjectWithSentry', () => { expect(initCore).nthCalledWith(2, expect.any(Function), expect.objectContaining({ orgId: 2 })); }); + it('Binds prototype methods to original object when enableRpcTracePropagation is true', () => { + const testClass = class { + method() { + return this; + } + }; + const instrumented = instrumentDurableObjectWithSentry( + vi.fn().mockReturnValue({ enableRpcTracePropagation: true }), + testClass as any, + ); + const obj = Reflect.construct(instrumented, []); + + // Method should be callable and return the original object (not the proxy) + const result = obj.method(); + expect(result).not.toBe(obj); // result is original object, obj is proxy + expect(typeof result.method).toBe('function'); // original object still has method + + // Methods should be cached (same reference on repeated access) + expect(obj.method).toBe(obj.method); + }); + it('Built-in durable object methods are always instrumented', () => { const testClass = class { fetch() {} @@ -102,6 +123,36 @@ describe('instrumentDurableObjectWithSentry', () => { } }); + it('Built-in durable object methods are own properties and not wrapped as RPC', () => { + const testClass = class { + fetch() { + return new Response('fetch'); + } + + alarm() {} + + rpcMethod() { + return 'rpc'; + } + }; + const instrumented = instrumentDurableObjectWithSentry( + vi.fn().mockReturnValue({ enableRpcTracePropagation: true }), + testClass as any, + ); + const obj = Reflect.construct(instrumented, []); + + // Built-in DO methods are set as own properties (not on prototype) + // This ensures they are not wrapped as RPC methods by the Proxy + expect(Object.prototype.hasOwnProperty.call(obj, 'fetch')).toBe(true); + expect(Object.prototype.hasOwnProperty.call(obj, 'alarm')).toBe(true); + + // RPC methods remain on the prototype + expect(Object.prototype.hasOwnProperty.call(obj, 'rpcMethod')).toBe(false); + + // All methods should still work correctly + expect(obj.rpcMethod()).toBe('rpc'); + }); + it('Does not instrument RPC methods when instrumentPrototypeMethods is not set', () => { const testClass = class { rpcMethod() { @@ -164,10 +215,7 @@ describe('instrumentDurableObjectWithSentry', () => { expect(obj.methodOne).not.toBe(testClass.prototype.methodOne); expect(obj.methodThree).not.toBe(testClass.prototype.methodThree); - // methodTwo is not in the allow-list and must remain the original - // prototype method (i.e. not wrapped). - expect(obj.methodTwo).toBe(testClass.prototype.methodTwo); - + // methodTwo is not in the allow-list — it's bound but not wrapped with Sentry tracing. // All methods should still be callable and behave correctly. expect(obj.methodOne()).toBe('one'); expect(obj.methodTwo()).toBe('two'); @@ -225,18 +273,12 @@ describe('instrumentDurableObjectWithSentry', () => { ); const obj = Reflect.construct(instrumented, []); - // Object.prototype methods should NOT be wrapped - they should be the original methods - expect(obj.toString).toBe(Object.prototype.toString); - expect(obj.valueOf).toBe(Object.prototype.valueOf); - expect(obj.hasOwnProperty).toBe(Object.prototype.hasOwnProperty); - expect(obj.propertyIsEnumerable).toBe(Object.prototype.propertyIsEnumerable); - expect(obj.isPrototypeOf).toBe(Object.prototype.isPrototypeOf); - expect(obj.toLocaleString).toBe(Object.prototype.toLocaleString); - - // They should still work correctly + // Object.prototype methods should NOT be wrapped with Sentry tracing. + // They are bound to the original object but still work correctly. expect(obj.toString()).toBe('[object Object]'); expect(obj.hasOwnProperty('rpcMethod')).toBe(false); // It's on prototype, not own - expect(obj.valueOf()).toBe(obj); + // valueOf returns the original object, not the proxy + expect(obj.valueOf()).not.toBe(obj); // Meanwhile, actual RPC methods SHOULD be wrapped (not equal to prototype method) expect(obj.rpcMethod).not.toBe(testClass.prototype.rpcMethod);