From bb72eaecb8640622b21568e6c310adf3021e45ee Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 12 Aug 2026 15:21:29 +0200 Subject: [PATCH 1/2] feat(cloudflare)!: Replace enableRpcTracePropagation with rpcTracePropagationTargets `enableRpcTracePropagation: true` propagated trace context to every Durable Object namespace and service binding on `env`. RPC carries that context as a trailing argument, and only a Sentry-instrumented receiver strips it again, so any receiver the user does not own saw an extra argument. `rpcTracePropagationTargets` names the bindings to propagate to instead, mirroring `tracePropagationTargets`. Strings match a binding name exactly, regular expressions match by pattern. The option is now caller-only. Receivers no longer take a switch: a Durable Object instruments its RPC methods unconditionally, matching what a WorkerEntrypoint already did. `extractRpcMeta` only strips a trailing argument that actually carries `__sentry_rpc_meta__`, so a call arriving without metadata is untouched. BREAKING CHANGE: `enableRpcTracePropagation` is removed. Replace it on the caller with `rpcTracePropagationTargets` listing the bindings you call, and drop it from receivers. Co-Authored-By: Claude Opus 5 --- .../suites/durableobject-scope/index.ts | 3 +- .../durableobject-alarm-links-sync/index.ts | 3 +- .../durableobject-alarm-links/index.ts | 3 +- .../durableobject-rpc-private-fields/index.ts | 3 +- .../durableobject-rpc-private-fields/test.ts | 2 +- .../tracing/durableobject-spans/index.ts | 3 +- .../suites/tracing/durableobject/index.ts | 3 +- .../suites/tracing/durableobject/test.ts | 2 +- .../tracing/instrument-fetcher/index.ts | 2 +- .../no-propagation-worker-do/test.ts | 6 +- .../worker-do-rpc-disabled/test.ts | 2 +- .../propagation/worker-do-rpc/index.ts | 3 +- .../tracing/propagation/worker-do/index.ts | 3 +- .../worker-service-binding/index.ts | 2 +- .../worker-worker-do-rpc/index-sub-worker.ts | 3 +- .../propagation/worker-worker-do-rpc/index.ts | 2 +- .../index-sub-worker.ts | 11 +- .../worker-workerentrypoint-rpc/index.ts | 12 ++- .../worker-workerentrypoint-rpc/test.ts | 19 +++- .../wrangler.jsonc | 5 + .../workerentrypoint-do-rpc-disabled/test.ts | 2 +- .../workerentrypoint-do-rpc/index.ts | 3 +- .../index-sub-worker.ts | 3 +- .../index.ts | 2 +- .../tracing/propagation/workflow-do/index.ts | 5 +- .../cloudflare-agent/worker/index.ts | 2 +- .../src/instrument.server.ts | 1 - .../cloudflare-workers-streaming/src/index.ts | 3 +- .../cloudflare-workers/src/index.ts | 3 +- .../cloudflare-workersentrypoint/src/index.ts | 3 +- packages/cloudflare/src/client.ts | 48 ++++----- packages/cloudflare/src/durableobject.ts | 11 +- .../instrumentWorkerEntrypoint.ts | 4 - .../instrumentations/worker/instrumentEnv.ts | 5 +- .../cloudflare/src/utils/rpcPropagation.ts | 23 ++++ packages/cloudflare/test/agents.test.ts | 5 +- .../cloudflare/test/durableobject.test.ts | 75 ++++--------- .../instrumentations/instrumentEnv.test.ts | 101 +++++++++++++++--- .../instrumentWorkerEntrypoint.test.ts | 42 +++----- .../test/utils/rpcPropagation.test.ts | 48 +++++++++ 40 files changed, 286 insertions(+), 195 deletions(-) create mode 100644 packages/cloudflare/src/utils/rpcPropagation.ts create mode 100644 packages/cloudflare/test/utils/rpcPropagation.test.ts diff --git a/dev-packages/cloudflare-integration-tests/suites/durableobject-scope/index.ts b/dev-packages/cloudflare-integration-tests/suites/durableobject-scope/index.ts index 86eec9128c5b..a7fd083993bb 100644 --- a/dev-packages/cloudflare-integration-tests/suites/durableobject-scope/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/durableobject-scope/index.ts @@ -75,7 +75,6 @@ export const ScopeDurableObject = Sentry.instrumentDurableObjectWithSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1, - enableRpcTracePropagation: true, }), ScopeDurableObjectBase, ); @@ -84,7 +83,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['SCOPE_DO'], }), { async fetch(request, env) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm-links-sync/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm-links-sync/index.ts index 2f595fd137fa..6e7fe5fdbed4 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm-links-sync/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm-links-sync/index.ts @@ -27,7 +27,6 @@ export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, }), SyncAlarmDurableObjectBase, ); @@ -37,7 +36,7 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['TEST_DURABLE_OBJECT'], }), { async fetch(request: Request, env: Env): Promise { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm-links/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm-links/index.ts index 4de067e5c2a4..2823c411fe2a 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm-links/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm-links/index.ts @@ -25,7 +25,6 @@ export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, }), AlarmDurableObjectBase, ); @@ -35,7 +34,7 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['TEST_DURABLE_OBJECT'], }), { async fetch(request: Request, env: Env): Promise { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-rpc-private-fields/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-rpc-private-fields/index.ts index 0e687e8bda16..8a378fb4c5b6 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-rpc-private-fields/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-rpc-private-fields/index.ts @@ -37,7 +37,6 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, }), MyDurableObjectBase, ); @@ -47,7 +46,7 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['MY_DURABLE_OBJECT'], }), { async fetch(request, env) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-rpc-private-fields/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-rpc-private-fields/test.ts index 0d7609f1c772..576d14127e13 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-rpc-private-fields/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-rpc-private-fields/test.ts @@ -3,7 +3,7 @@ import type { Event } from '@sentry/core'; import { createRunner } from '../../../runner'; // Regression for #23040 — a Durable Object using native private fields must stay functional when -// instrumented with `enableRpcTracePropagation: true`. Native RPC dispatch (Durable Object facets, +// instrumented with Sentry. Native RPC dispatch (Durable Object facets, // the Agents SDK bootstrap) invokes prototype methods with the stored instance as the receiver, // so the instrumented instance must not be a Proxy: a Proxy does not carry the private-field // brand and `this.#field` throws "Cannot read private member". diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-spans/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-spans/index.ts index 40e9f463a2e2..b4fda9fd27e9 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-spans/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-spans/index.ts @@ -32,7 +32,6 @@ export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, }), TestDurableObjectBase, ); @@ -42,7 +41,7 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['TEST_DURABLE_OBJECT'], }), { async fetch(_request: Request, env: Env): Promise { 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 df44042c0f5f..45325345f2c8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts @@ -46,7 +46,6 @@ export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, }), TestDurableObjectBase, ); @@ -56,7 +55,7 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['TEST_DURABLE_OBJECT'], }), { async fetch(request: Request, env: Env): Promise { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/test.ts index 4e9e65f22118..c1ebc4e3318f 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/test.ts @@ -65,7 +65,7 @@ it('handles consecutive RPC calls without throwing "RPC receiver does not implem }); // Regression test: RPC methods that access private fields should work correctly. -// When enableRpcTracePropagation wraps the DO in a Proxy, calling methods through +// When rpcTracePropagationTargets wraps the DO in a Proxy, calling methods through // the Proxy must ensure `this` refers to the original object (not the Proxy), // otherwise private field access throws: "Cannot read private member from an object // whose class did not declare it" diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/instrument-fetcher/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/instrument-fetcher/index.ts index 85fc93e1f477..ba1178039813 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/instrument-fetcher/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/instrument-fetcher/index.ts @@ -33,7 +33,7 @@ export default withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['ECHO_HEADERS_DO'], }), { async fetch(request, env) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/no-propagation-worker-do/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/no-propagation-worker-do/test.ts index 6431cc07b13a..667a07283d93 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/no-propagation-worker-do/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/no-propagation-worker-do/test.ts @@ -2,7 +2,7 @@ import { expect, it } from 'vitest'; import type { Event } from '@sentry/core'; import { createRunner } from '../../../../runner'; -it('does not propagate trace from worker to durable object when enableRpcTracePropagation is disabled', async ({ +it('does not propagate trace from worker to durable object when rpcTracePropagationTargets is empty', async ({ signal, }) => { let workerTraceId: string | undefined; @@ -64,7 +64,7 @@ it('does not propagate trace from worker to durable object when enableRpcTracePr expect(doParentSpanId).toBeUndefined(); }); -it('does not propagate trace from queue handler to durable object when enableRpcTracePropagation is disabled', async ({ +it('does not propagate trace from queue handler to durable object when rpcTracePropagationTargets is empty', async ({ signal, }) => { let queueTraceId: string | undefined; @@ -146,7 +146,7 @@ it('does not propagate trace from queue handler to durable object when enableRpc expect(doParentSpanId).toBeUndefined(); }); -it('does not propagate trace from scheduled handler to durable object when enableRpcTracePropagation is disabled', async ({ +it('does not propagate trace from scheduled handler to durable object when rpcTracePropagationTargets is empty', async ({ signal, }) => { let scheduledTraceId: string | undefined; diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc-disabled/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc-disabled/test.ts index 4fe2b98956d5..8426dc708b70 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc-disabled/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc-disabled/test.ts @@ -2,7 +2,7 @@ import { expect, it } from 'vitest'; import type { Event } from '@sentry/core'; import { createRunner } from '../../../../runner'; -it('does not propagate trace when enableRpcTracePropagation is disabled', async ({ signal }) => { +it('does not propagate trace when rpcTracePropagationTargets is empty', async ({ signal }) => { let workerTraceId: string | undefined; let doTraceId: string | undefined; diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc/index.ts index 13aa8b6214b9..2f2cb58dd548 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc/index.ts @@ -21,7 +21,6 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, }), MyDurableObjectBase, ); @@ -31,7 +30,7 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['MY_DURABLE_OBJECT'], }), { async fetch(request, env) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do/index.ts index a6c32b5a5d3e..4d4366494e80 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do/index.ts @@ -18,7 +18,6 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, }), MyDurableObjectBase, ); @@ -28,7 +27,7 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['MY_DURABLE_OBJECT'], }), { async fetch(request, env) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/index.ts index 158d32889959..32fc6dd78662 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/index.ts @@ -10,7 +10,7 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['ANOTHER_WORKER'], }), { async fetch(request, env) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-worker-do-rpc/index-sub-worker.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-worker-do-rpc/index-sub-worker.ts index fd548a39ac0e..b15a93ef8b59 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-worker-do-rpc/index-sub-worker.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-worker-do-rpc/index-sub-worker.ts @@ -17,7 +17,6 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, }), MyDurableObjectBase, ); @@ -27,7 +26,7 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['MY_DURABLE_OBJECT'], }), { async fetch(request, env) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-worker-do-rpc/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-worker-do-rpc/index.ts index 8f5d330ff71f..7ba0ce0d4c67 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-worker-do-rpc/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-worker-do-rpc/index.ts @@ -10,7 +10,7 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['SUB_WORKER'], }), { async fetch(request, env) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/index-sub-worker.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/index-sub-worker.ts index 6c8638be5ad1..46508f2791be 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/index-sub-worker.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/index-sub-worker.ts @@ -46,7 +46,6 @@ export const BindingEntrypoint = Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, initialScope: { tags: { initial_scope: 'applied' } }, beforeSend(event) { event.tags = { ...event.tags, before_send: 'applied' }; @@ -57,6 +56,8 @@ export const BindingEntrypoint = Sentry.withSentry( MySubWorkerEntrypointBase, ); +// Instrumented like any other receiver. It is the caller that leaves this binding out of its +// targets, which is now the only way to opt a binding out of trace propagation. export const NoPropagationEntrypoint = Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, @@ -67,4 +68,12 @@ export const NoPropagationEntrypoint = Sentry.withSentry( MySubWorkerEntrypointBase, ); +// Deliberately not wrapped with Sentry: nothing strips a trailing RPC metadata argument here, so +// this is what a caller corrupts if it propagates to a receiver it has no guarantees about. +export class UninstrumentedEntrypoint extends WorkerEntrypoint { + get(key: string): { argumentCount: number; key: string } { + return { argumentCount: arguments.length, key }; + } +} + export default BindingEntrypoint; diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/index.ts index d366e7d71afc..1d7177c92dbd 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/index.ts @@ -11,6 +11,9 @@ interface Env { SUB_WORKER_NO_PROPAGATION: Fetcher & { get(key: string): Promise<{ argumentCount: number; key: string }>; }; + SUB_WORKER_UNINSTRUMENTED: Fetcher & { + get(key: string): Promise<{ argumentCount: number; key: string }>; + }; } class LoopbackEntrypointBase extends WorkerEntrypoint { @@ -29,7 +32,10 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + // Targeted by binding name. Two bindings are deliberately left out: + // `SUB_WORKER_UNINSTRUMENTED`, whose receiver has no Sentry to strip a trailing metadata + // argument, and `SUB_WORKER_NO_PROPAGATION`, which covers the untargeted-binding path. + rpcTracePropagationTargets: ['SUB_WORKER'], }), { async fetch(request, env, ctx) { @@ -61,6 +67,10 @@ export default Sentry.withSentry( } } + if (url.pathname === '/call-uninstrumented-rpc') { + return Response.json(await env.SUB_WORKER_UNINSTRUMENTED.get('uninstrumented-key')); + } + if (url.pathname === '/call-entrypoint-rpc-no-propagation') { const result = await env.SUB_WORKER_NO_PROPAGATION.get('no-prop-key'); return Response.json(result); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/test.ts index 9309d0f4f97b..0c6664a0d56a 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/test.ts @@ -244,7 +244,24 @@ it('captures errors thrown by custom WorkerEntrypoint RPC methods', async ({ sig await runner.completed(); }); -it('does not inject RPC trace metadata into receiver calls when enableRpcTracePropagation is disabled', async ({ +// Regression test for https://github.com/getsentry/sentry-javascript/issues/23233: a receiver that +// is not instrumented never strips Sentry's trailing metadata argument, so a caller must only +// propagate to bindings it was explicitly told about. +it('does not change RPC method arguments for a binding left off the allowlist', async ({ signal }) => { + const runner = createRunner(__dirname) + .expect(envelope => { + const transactionEvent = envelope[1]?.[0]?.[1] as Event; + expect(transactionEvent.transaction).toBe('GET /call-uninstrumented-rpc'); + }) + .start(signal); + + const response = await runner.makeRequest<{ argumentCount: number; key: string }>('get', '/call-uninstrumented-rpc'); + expect(response).toEqual({ argumentCount: 1, key: 'uninstrumented-key' }); + + await runner.completed(); +}); + +it('does not inject RPC trace metadata into receiver calls when rpcTracePropagationTargets is empty', async ({ signal, }) => { const runner = createRunner(__dirname) diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/wrangler.jsonc index badfcd962843..6327e35d9fff 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/wrangler.jsonc @@ -14,5 +14,10 @@ "service": "cloudflare-worker-workerentrypoint-rpc-sub", "entrypoint": "NoPropagationEntrypoint", }, + { + "binding": "SUB_WORKER_UNINSTRUMENTED", + "service": "cloudflare-worker-workerentrypoint-rpc-sub", + "entrypoint": "UninstrumentedEntrypoint", + }, ], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc-disabled/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc-disabled/test.ts index 4882f09ccaaa..0cec81b7984b 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc-disabled/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc-disabled/test.ts @@ -2,7 +2,7 @@ import { expect, it } from 'vitest'; import type { Event } from '@sentry/core'; import { createRunner } from '../../../../runner'; -it('does not propagate trace when enableRpcTracePropagation is disabled (WorkerEntrypoint)', async ({ signal }) => { +it('does not propagate trace when rpcTracePropagationTargets is empty (WorkerEntrypoint)', async ({ signal }) => { let workerTraceId: string | undefined; let doTraceId: string | undefined; diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc/index.ts index d742bd1b120d..68001696838a 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc/index.ts @@ -21,7 +21,6 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, }), MyDurableObjectBase, ); @@ -51,7 +50,7 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['MY_DURABLE_OBJECT'], }), MyWorkerEntrypointBase, ); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-workerentrypoint-do-rpc/index-sub-worker.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-workerentrypoint-do-rpc/index-sub-worker.ts index 7007d35e91b3..f17ef0f2f5c3 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-workerentrypoint-do-rpc/index-sub-worker.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-workerentrypoint-do-rpc/index-sub-worker.ts @@ -17,7 +17,6 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, }), MyDurableObjectBase, ); @@ -42,7 +41,7 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['MY_DURABLE_OBJECT'], }), MySubWorkerEntrypointBase, ); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-workerentrypoint-do-rpc/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-workerentrypoint-do-rpc/index.ts index 26e89570aaf3..29fd1956b5c4 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-workerentrypoint-do-rpc/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-workerentrypoint-do-rpc/index.ts @@ -25,7 +25,7 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['SUB_WORKER'], }), MyWorkerEntrypointBase, ); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workflow-do/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workflow-do/index.ts index d2c4e6a03ac5..b4f6c7219884 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workflow-do/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workflow-do/index.ts @@ -39,7 +39,8 @@ export const MyWorkflow = Sentry.instrumentWorkflowWithSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + // The workflow is itself a caller: `run` reaches the Durable Object through `this.env`. + rpcTracePropagationTargets: ['MY_DURABLE_OBJECT'], }), MyWorkflowBase, ); @@ -49,7 +50,7 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['MY_WORKFLOW'], }), { async fetch(request, env) { 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 index 53de97a457c6..dd3dfba727c8 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-agent/worker/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/worker/index.ts @@ -12,7 +12,7 @@ const sentryOptions = (env: Env) => ({ dsn: env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, tracesSampleRate: 1, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['MyAgent', 'MyChatAgent', 'MyManualChatAgent'], durableObjectStorageSpanAllowlist: ['cf_user_key'], }); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/src/instrument.server.ts b/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/src/instrument.server.ts index 73432eb22c8b..2461cc5fd5aa 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/src/instrument.server.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/src/instrument.server.ts @@ -7,7 +7,6 @@ export default (env: Env) => ({ environment: 'qa', tunnel: 'http://localhost:3031/', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, transportOptions: { bufferSize: 1000, }, diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/src/index.ts index efd4b805647d..f763a90c21ef 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/src/index.ts @@ -85,7 +85,6 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( // We are doing a lot of events at once in this test bufferSize: 1000, }, - enableRpcTracePropagation: true, }), MyDurableObjectBase, ); @@ -101,7 +100,7 @@ export default Sentry.withSentry( // We are doing a lot of events at once in this test bufferSize: 1000, }, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['MY_DURABLE_OBJECT'], }), { async fetch(request, env) { diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts index bc5eec66c8b6..e0ef918b6c4b 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts @@ -85,7 +85,6 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( // We are doing a lot of events at once in this test bufferSize: 1000, }, - enableRpcTracePropagation: true, }), MyDurableObjectBase, ); @@ -101,7 +100,7 @@ export default Sentry.withSentry( // We are doing a lot of events at once in this test bufferSize: 1000, }, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['MY_DURABLE_OBJECT'], }), { async fetch(request, env) { diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/src/index.ts index a5acdfdd7fee..76729d6899c1 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/src/index.ts @@ -72,7 +72,6 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( // We are doing a lot of events at once in this test bufferSize: 1000, }, - enableRpcTracePropagation: true, }), MyDurableObjectBase, ); @@ -118,7 +117,7 @@ export default Sentry.withSentry( // We are doing a lot of events at once in this test bufferSize: 1000, }, - enableRpcTracePropagation: true, + rpcTracePropagationTargets: ['MY_DURABLE_OBJECT'], }), MyWorker, ); diff --git a/packages/cloudflare/src/client.ts b/packages/cloudflare/src/client.ts index 85e47fbee3d7..780db6d937f8 100644 --- a/packages/cloudflare/src/client.ts +++ b/packages/cloudflare/src/client.ts @@ -188,49 +188,37 @@ interface BaseCloudflareOptions { enableOpenTelemetrySetup?: boolean; /** - * Enable trace propagation for RPC calls between Workers, Durable Objects, and Service Bindings. + * The bindings on `env` that outgoing RPC calls propagate trace context to. * - * When enabled, trace context (sentry-trace + baggage) is propagated across: - * - `stub.fetch()` calls to Durable Objects (via HTTP headers) - * - Service binding `fetch()` calls (via HTTP headers) - * - RPC method calls to Durable Objects and WorkerEntrypoints (via trailing argument) + * Strings match a binding name exactly, regular expressions match by pattern. An empty array + * (the default) propagates to nothing. * - * When enabled on the **receiver side** (DurableObject or WorkerEntrypoint), the SDK will also: - * - Extract and continue traces from incoming RPC calls - * - Create spans for each RPC method invocation - * - Capture errors thrown by RPC methods + * RPC has no headers to carry trace context, so the SDK appends it as a trailing argument to + * every RPC method call on a matching binding. Only a Sentry-instrumented receiver strips that + * argument again. Anywhere else it arrives as a real argument and changes what the method was + * called with, so list only the bindings whose receiver you know runs Sentry. * - * **Important:** This option should be enabled on **both sides** for full trace propagation. + * Propagation over `stub.fetch()` and service binding `fetch()` uses HTTP headers and is not + * affected by this option. * - * @default false + * When you build with the Sentry Cloudflare Vite plugin, bindings that resolve to *this* worker + * (its own Durable Objects, its self service bindings) are added for you, because the plugin + * instruments those receivers itself. Whatever you list here is added on top of them. + * + * @default [] * @example * ```ts - * // Worker side (caller) + * // Propagate to `env.ORDERS` and every `env.SVC_*` binding * export default Sentry.withSentry( - * (env) => ({ + * env => ({ * dsn: env.SENTRY_DSN, - * enableRpcTracePropagation: true, + * rpcTracePropagationTargets: ['ORDERS', /^SVC_/], * }), * handler, * ); - * - * // Durable Object side (receiver) - * export const MyDO = Sentry.instrumentDurableObjectWithSentry( - * (env) => ({ - * dsn: env.SENTRY_DSN, - * enableRpcTracePropagation: true, - * }), - * MyDOBase, - * ); - * - * // WorkerEntrypoint side (receiver) - * export const MyEntrypoint = Sentry.withSentry( - * env => ({ dsn: env.SENTRY_DSN, enableRpcTracePropagation: true }), - * MyEntrypointBase, - * ); * ``` */ - enableRpcTracePropagation?: boolean; + rpcTracePropagationTargets?: Array; /** * Table names that should stay instrumented even though they match the reserved `cf_` prefix used diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index e1dea406287a..4c9b008c3fd4 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -258,8 +258,7 @@ const rpcInstanceStates = new WeakMap(); * visible to Cloudflare's RPC dispatcher. Built-in handlers, Agent handlers, and methods managed by * another framework are left untouched. * - * Call this after all per-instance instrumentation has been applied. If RPC trace propagation is - * disabled, the object is returned unchanged. + * Call this after all per-instance instrumentation has been applied. * * @param obj The constructed Durable Object instance. * @param options The resolved SDK options for this instance. @@ -274,11 +273,6 @@ export function finalizeWithRpcInstrumentation( context: InstrumentedDurableObjectContext, excludedMethods?: ReadonlySet, ): T { - // Skip RPC instrumentation if not enabled - if (!options.enableRpcTracePropagation) { - return obj; - } - rpcInstanceStates.set(obj, { options, context }); instrumentPrototypeRpcMethods(obj, excludedMethods); @@ -403,7 +397,7 @@ function createRpcPrototypeWrapper(methodName: string, originalMethod: Unchecked * - webSocketClose * - webSocketError * - * To instrument RPC methods (prototype methods), enable the `enableRpcTracePropagation` option. + * RPC methods (prototype methods) are instrumented too, so an incoming trace continues into them. * * @param optionsCallback Function that returns the options for the SDK initialization. * @param DurableObjectClass The Durable Object class to instrument. @@ -483,7 +477,6 @@ export function instrumentDurableObjectWithSentry< * env => ({ * dsn: env.SENTRY_DSN, * tracesSampleRate: 1.0, - * enableRpcTracePropagation: true, * }), * MyAgentBase, * ); diff --git a/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts b/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts index be82607463db..f3f393b4bcfd 100644 --- a/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts +++ b/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts @@ -94,10 +94,6 @@ function instrumentMethod( true, ); - if (!options.enableRpcTracePropagation) { - return captureMethod; - } - const tracedMethod = wrapMethodWithSentry( { options, context, spanName: prop, spanOp: 'rpc', origin: WORKER_ENTRYPOINT_ORIGIN }, boundMethod, diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts b/packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts index 5a440503a4ee..eb660e4126e9 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts @@ -12,6 +12,7 @@ import { } from '../../utils/isBinding'; import { instrumentD1 } from './instrumentD1'; import { appendRpcMeta } from '../../utils/rpcMeta'; +import { createRpcPropagationResolver } from '../../utils/rpcPropagation'; import { instrumentDurableObjectNamespace, STUB_NON_RPC_METHODS } from '../instrumentDurableObjectNamespace'; import { instrumentFetcher } from './instrumentFetcher'; import { instrumentQueueProducer } from './instrumentQueueProducer'; @@ -44,6 +45,8 @@ export function instrumentEnv>(env: Env, opt return env; } + const shouldPropagateRpcTrace = createRpcPropagationResolver(options); + return new Proxy(env, { get(target, prop, receiver) { const item = Reflect.get(target, prop, receiver); @@ -91,7 +94,7 @@ export function instrumentEnv>(env: Env, opt return instrumented; } - if (!options?.enableRpcTracePropagation) { + if (!shouldPropagateRpcTrace(String(prop))) { return item; } diff --git a/packages/cloudflare/src/utils/rpcPropagation.ts b/packages/cloudflare/src/utils/rpcPropagation.ts new file mode 100644 index 000000000000..7250ec0270f0 --- /dev/null +++ b/packages/cloudflare/src/utils/rpcPropagation.ts @@ -0,0 +1,23 @@ +import { stringMatchesSomePattern } from '@sentry/core'; +import type { CloudflareOptions } from '../client'; + +const PROPAGATE_TO_NONE = () => false; + +/** + * Builds the per-binding predicate that decides whether a binding takes part in RPC trace + * propagation. + * + * Callers only. Receivers continue an incoming trace whenever one arrives, so they have nothing to + * match against. + */ +export function createRpcPropagationResolver(options: CloudflareOptions | undefined): (bindingName: string) => boolean { + const targets = options?.rpcTracePropagationTargets; + + if (!targets?.length) { + return PROPAGATE_TO_NONE; + } + + // Strings must match a binding name exactly, without this, an entry of `DB` would also enable + // propagation for a binding named `MY_DB`. Regular expressions still give pattern matching. + return (bindingName: string) => stringMatchesSomePattern(bindingName, targets, true); +} diff --git a/packages/cloudflare/test/agents.test.ts b/packages/cloudflare/test/agents.test.ts index 93c6abda81f0..c34826374147 100644 --- a/packages/cloudflare/test/agents.test.ts +++ b/packages/cloudflare/test/agents.test.ts @@ -52,10 +52,7 @@ describe('instrumentAgentWithSentry', () => { } }; - const instrumented = instrumentAgentWithSentry( - vi.fn().mockReturnValue({ enableRpcTracePropagation: true }), - testClass as any, - ); + const instrumented = instrumentAgentWithSentry(vi.fn().mockReturnValue({}), testClass as any); const obj = Reflect.construct(instrumented, []); // Agent-specific handlers become own properties, so they are excluded from RPC method tracing. diff --git a/packages/cloudflare/test/durableobject.test.ts b/packages/cloudflare/test/durableobject.test.ts index 44d4bf25d12b..ccd87ada3725 100644 --- a/packages/cloudflare/test/durableobject.test.ts +++ b/packages/cloudflare/test/durableobject.test.ts @@ -59,11 +59,9 @@ describe('instrumentDurableObjectWithSentry', () => { .fn() .mockReturnValueOnce({ orgId: 1, - enableRpcTracePropagation: true, }) .mockReturnValueOnce({ orgId: 2, - enableRpcTracePropagation: true, }); const testClass = class { method() {} @@ -118,7 +116,7 @@ describe('instrumentDurableObjectWithSentry', () => { expect(initCore).nthCalledWith(2, expect.any(Function), expect.objectContaining({ orgId: 2 })); }); - it('does not create RPC spans without metadata when enableRpcTracePropagation is true', () => { + it('does not create RPC spans without metadata', () => { const startSpanSpy = vi.spyOn(SentryCore, 'startSpan'); vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined); @@ -127,28 +125,20 @@ describe('instrumentDurableObjectWithSentry', () => { return 'result'; } }; - const instrumented = instrumentDurableObjectWithSentry( - vi.fn().mockReturnValue({ - enableRpcTracePropagation: true, - }), - testClass as any, - ); + const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any); const obj = Reflect.construct(instrumented, []); expect(obj.rpcMethod()).toBe('result'); expect(startSpanSpy).not.toHaveBeenCalled(); }); - it('Invokes prototype methods with the instance as receiver when enableRpcTracePropagation is true', () => { + it('Invokes prototype methods with the instance as receiver', () => { const testClass = class { method() { return this; } }; - const instrumented = instrumentDurableObjectWithSentry( - vi.fn().mockReturnValue({ enableRpcTracePropagation: true }), - testClass as any, - ); + const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any); const obj = Reflect.construct(instrumented, []); // The instance is not proxied, so the receiver is the instance itself — this is what keeps @@ -254,10 +244,7 @@ describe('instrumentDurableObjectWithSentry', () => { return 'rpc'; } }; - const instrumented = instrumentDurableObjectWithSentry( - vi.fn().mockReturnValue({ enableRpcTracePropagation: true }), - testClass as any, - ); + const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any); const obj = Reflect.construct(instrumented, []); // Built-in DO methods are set as own properties (not on prototype) @@ -278,10 +265,7 @@ describe('instrumentDurableObjectWithSentry', () => { return 'result'; } }; - const instrumented = instrumentDurableObjectWithSentry( - vi.fn().mockReturnValue({ enableRpcTracePropagation: true }), - testClass as any, - ); + const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any); const obj = Reflect.construct(instrumented, []); // constructor must remain the original class reference for identity/type checks @@ -317,7 +301,9 @@ describe('instrumentDurableObjectWithSentry', () => { expect(getInstrumented(obj.alarm)).toBeTruthy(); }); - it('Does not instrument RPC methods when enableRpcTracePropagation is not set', () => { + // A receiver has no propagation option to switch on: it continues an incoming trace whenever + // one arrives, so its RPC methods are always instrumented. + it('Instruments RPC methods without any propagation option', () => { const testClass = class { rpcMethod() { return 'result'; @@ -326,8 +312,7 @@ describe('instrumentDurableObjectWithSentry', () => { const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any); const obj = Reflect.construct(instrumented, []); - // RPC method should not be wrapped - expect(getInstrumented(obj.rpcMethod)).toBeFalsy(); + expect(getInstrumented(obj.rpcMethod)).toBeTruthy(); expect(obj.rpcMethod()).toBe('result'); }); @@ -349,10 +334,7 @@ describe('instrumentDurableObjectWithSentry', () => { }); const originalSealedMethod = testClass.prototype.sealedMethod; - const instrumented = instrumentDurableObjectWithSentry( - vi.fn().mockReturnValue({ enableRpcTracePropagation: true }), - testClass as any, - ); + const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any); let obj: any; expect(() => { @@ -377,10 +359,7 @@ describe('instrumentDurableObjectWithSentry', () => { // Capture the original before construction wraps the prototype const originalRpcMethod = testClass.prototype.rpcMethod; - const instrumented = instrumentDurableObjectWithSentry( - vi.fn().mockReturnValue({ enableRpcTracePropagation: true }), - testClass as any, - ); + const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any); const obj = Reflect.construct(instrumented, []); // Object.prototype methods should NOT be wrapped with Sentry tracing. @@ -426,10 +405,7 @@ describe('instrumentDurableObjectWithSentry', () => { const originalFetchData = FrameworkLike.prototype.fetchData; - const instrumented = instrumentDurableObjectWithSentry( - vi.fn().mockReturnValue({ enableRpcTracePropagation: true }), - FrameworkLike as any, - ); + const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), FrameworkLike as any); const obj = Reflect.construct(instrumented, []) as FrameworkLike; // Left as the framework installed it, so its identity-keyed dispatch keeps resolving @@ -463,10 +439,7 @@ describe('instrumentDurableObjectWithSentry', () => { } } - const instrumented = instrumentDurableObjectWithSentry( - vi.fn().mockReturnValue({ enableRpcTracePropagation: true }), - FrameworkLike as any, - ); + const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), FrameworkLike as any); Reflect.construct(instrumented, []); const second = Reflect.construct(instrumented, []) as FrameworkLike; @@ -485,10 +458,7 @@ describe('instrumentDurableObjectWithSentry', () => { } }; - const instrumented = instrumentDurableObjectWithSentry( - vi.fn().mockReturnValue({ enableRpcTracePropagation: true }), - testClass as any, - ); + const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any); Reflect.construct(instrumented, []); expect(testClass.prototype.rpcMethod.name).toBe('rpcMethod'); @@ -520,10 +490,7 @@ describe('instrumentDurableObjectWithSentry', () => { rpcMethod: testClass.prototype.rpcMethod, }; - const instrumented = instrumentDurableObjectWithSentry( - vi.fn().mockReturnValue({ enableRpcTracePropagation: true }), - testClass as any, - ); + const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any); Reflect.construct(instrumented, []); expect(testClass.prototype.connect).toBe(originals.connect); @@ -553,10 +520,7 @@ describe('instrumentDurableObjectWithSentry', () => { } } - const instrumented = instrumentAgentWithSentry( - vi.fn().mockReturnValue({ enableRpcTracePropagation: true }), - PartyServerLike as any, - ); + const instrumented = instrumentAgentWithSentry(vi.fn().mockReturnValue({}), PartyServerLike as any); const obj = Reflect.construct(instrumented, []) as PartyServerLike; // This is how native RPC invokes the method: resolved on the prototype, called with the @@ -578,10 +542,7 @@ describe('instrumentDurableObjectWithSentry', () => { } } - const instrumented = instrumentDurableObjectWithSentry( - vi.fn().mockReturnValue({ enableRpcTracePropagation: true }), - WithSecret as any, - ); + const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), WithSecret as any); const obj = Reflect.construct(instrumented, []) as WithSecret; const rpcMeta = { diff --git a/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts b/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts index 72f9d0774507..45378826bde4 100644 --- a/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts +++ b/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts @@ -74,7 +74,7 @@ describe('instrumentEnv', () => { expect(instrumented.UNKNOWN).toBe(unknownBinding); }); - it('does not instrument DurableObjectNamespace when enableRpcTracePropagation is disabled', () => { + it('does not instrument DurableObjectNamespace when rpcTracePropagationTargets is empty', () => { const doNamespace = { idFromName: vi.fn(), idFromString: vi.fn(), @@ -89,7 +89,35 @@ describe('instrumentEnv', () => { expect(instrumentDurableObjectNamespace).not.toHaveBeenCalled(); }); - it('detects and instruments DurableObjectNamespace bindings when enableRpcTracePropagation is enabled', () => { + it('instruments only the DurableObjectNamespace bindings named in the allowlist', () => { + const allowed = { idFromName: vi.fn(), idFromString: vi.fn(), get: vi.fn(), newUniqueId: vi.fn() }; + const denied = { idFromName: vi.fn(), idFromString: vi.fn(), get: vi.fn(), newUniqueId: vi.fn() }; + const env = { COUNTER: allowed, SESSIONS: denied }; + const instrumented = instrumentEnv(env, { rpcTracePropagationTargets: ['COUNTER'] }); + + expect((instrumented.COUNTER as any).__instrumented).toBe(true); + expect(instrumented.SESSIONS).toBe(denied); + expect(instrumentDurableObjectNamespace).toHaveBeenCalledTimes(1); + expect(instrumentDurableObjectNamespace).toHaveBeenCalledWith(allowed); + }); + + it('matches allowlisted binding names exactly rather than as substrings', () => { + const doNamespace = { idFromName: vi.fn(), idFromString: vi.fn(), get: vi.fn(), newUniqueId: vi.fn() }; + const env = { MY_COUNTER: doNamespace }; + const instrumented = instrumentEnv(env, { rpcTracePropagationTargets: ['COUNTER'] }); + + expect(instrumented.MY_COUNTER).toBe(doNamespace); + }); + + it('supports regular expressions in the allowlist', () => { + const doNamespace = { idFromName: vi.fn(), idFromString: vi.fn(), get: vi.fn(), newUniqueId: vi.fn() }; + const env = { SVC_ORDERS: doNamespace }; + const instrumented = instrumentEnv(env, { rpcTracePropagationTargets: [/^SVC_/] }); + + expect((instrumented.SVC_ORDERS as any).__instrumented).toBe(true); + }); + + it('detects and instruments DurableObjectNamespace bindings when rpcTracePropagationTargets matches', () => { const doNamespace = { idFromName: vi.fn(), idFromString: vi.fn(), @@ -97,7 +125,7 @@ describe('instrumentEnv', () => { newUniqueId: vi.fn(), }; const env = { COUNTER: doNamespace }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); + const instrumented = instrumentEnv(env, { rpcTracePropagationTargets: [/.*/] }); const result = instrumented.COUNTER; expect(instrumentDurableObjectNamespace).toHaveBeenCalledWith(doNamespace); @@ -112,7 +140,7 @@ describe('instrumentEnv', () => { newUniqueId: vi.fn(), }; const env = { COUNTER: doNamespace }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); + const instrumented = instrumentEnv(env, { rpcTracePropagationTargets: [/.*/] }); const first = instrumented.COUNTER; const second = instrumented.COUNTER; @@ -135,7 +163,7 @@ describe('instrumentEnv', () => { newUniqueId: vi.fn(), }; const env = { COUNTER: doNamespace1, SESSIONS: doNamespace2 }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); + const instrumented = instrumentEnv(env, { rpcTracePropagationTargets: [/.*/] }); instrumented.COUNTER; instrumented.SESSIONS; @@ -145,7 +173,7 @@ describe('instrumentEnv', () => { expect(instrumentDurableObjectNamespace).toHaveBeenCalledWith(doNamespace2); }); - it('does not wrap JSRPC proxy when enableRpcTracePropagation is disabled', () => { + it('does not wrap JSRPC proxy when rpcTracePropagationTargets is empty', () => { const mockFetch = vi.fn(); const jsrpcProxy = new Proxy( { fetch: mockFetch }, @@ -168,7 +196,7 @@ describe('instrumentEnv', () => { expect(instrumentDurableObjectNamespace).not.toHaveBeenCalled(); }); - it('wraps JSRPC proxy with a Proxy that instruments fetch when enableRpcTracePropagation is enabled', () => { + it('wraps JSRPC proxy with a Proxy that instruments fetch when rpcTracePropagationTargets matches', () => { const mockFetch = vi.fn(); const jsrpcProxy = new Proxy( { fetch: mockFetch }, @@ -183,7 +211,7 @@ describe('instrumentEnv', () => { }, ); const env = { SERVICE: jsrpcProxy }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); + const instrumented = instrumentEnv(env, { rpcTracePropagationTargets: [/.*/] }); const result = instrumented.SERVICE; // Should NOT be the same reference — it's wrapped in a Proxy @@ -248,7 +276,7 @@ describe('instrumentEnv', () => { newUniqueId: vi.fn(), }; const env = { MY_QUEUE: queue, COUNTER: doNamespace }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); + const instrumented = instrumentEnv(env, { rpcTracePropagationTargets: [/.*/] }); // Access both — DO instrumentation only fires on property access expect(instrumented.MY_QUEUE).not.toBe(queue); @@ -342,7 +370,7 @@ describe('instrumentEnv', () => { ); } - it('does not instrument mTLS Fetcher when enableRpcTracePropagation is disabled', () => { + it('does not instrument mTLS Fetcher when rpcTracePropagationTargets is empty', () => { const mockFetch = vi.fn(); const mtlsFetcher = createMtlsFetcherProxy(mockFetch); const env = { MY_CERT: mtlsFetcher }; @@ -361,7 +389,7 @@ describe('instrumentEnv', () => { const mockFetch = vi.fn().mockResolvedValue(new Response('mtls-response')); const mtlsFetcher = createMtlsFetcherProxy(mockFetch); const env = { MY_CERT: mtlsFetcher }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); + const instrumented = instrumentEnv(env, { rpcTracePropagationTargets: [/.*/] }); const response = await instrumented.MY_CERT.fetch('https://example.com/api', { headers: { Authorization: 'Bearer client-cert-token' }, @@ -378,7 +406,7 @@ describe('instrumentEnv', () => { }); describe('JSRPC RPC method instrumentation', () => { - it('does not inject Sentry RPC meta by default (enableRpcTracePropagation not set)', () => { + it('does not inject Sentry RPC meta by default (rpcTracePropagationTargets not set)', () => { vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', baggage: 'sentry-environment=production', @@ -401,11 +429,11 @@ describe('instrumentEnv', () => { instrumented.SERVICE.myRpcMethod('arg1', 42); - // Without enableRpcTracePropagation, no metadata should be injected + // Without rpcTracePropagationTargets, no metadata should be injected expect(rpcMethod).toHaveBeenCalledWith('arg1', 42); }); - it('injects Sentry RPC meta when enableRpcTracePropagation is true', () => { + it('injects Sentry RPC meta when rpcTracePropagationTargets matches', () => { vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', baggage: 'sentry-environment=production', @@ -424,7 +452,7 @@ describe('instrumentEnv', () => { }, ); const env = { SERVICE: jsrpcProxy }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); + const instrumented = instrumentEnv(env, { rpcTracePropagationTargets: [/.*/] }); instrumented.SERVICE.myRpcMethod('arg1', 42); @@ -455,7 +483,7 @@ describe('instrumentEnv', () => { }, ); const env = { SERVICE: jsrpcProxy }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); + const instrumented = instrumentEnv(env, { rpcTracePropagationTargets: [/.*/] }); instrumented.SERVICE.fetch('https://example.com'); @@ -480,11 +508,50 @@ describe('instrumentEnv', () => { }, ); const env = { SERVICE: jsrpcProxy }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); + const instrumented = instrumentEnv(env, { rpcTracePropagationTargets: [/.*/] }); instrumented.SERVICE.myRpcMethod('arg1'); expect(rpcMethod).toHaveBeenCalledWith('arg1'); }); + + // A receiver without Sentry never strips the trailing metadata argument, so a caller has to be + // able to limit propagation to the bindings it knows are instrumented. + // See https://github.com/getsentry/sentry-javascript/issues/23233. + it('injects meta only into JSRPC calls on allowlisted bindings', () => { + vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: 'sentry-environment=production', + }); + + const allowedMethod = vi.fn(); + const deniedMethod = vi.fn(); + const createJsrpcBinding = (rpcMethod: ReturnType) => + new Proxy( + { fetch: vi.fn(), myRpcMethod: rpcMethod }, + { + get(target, prop) { + if (prop in target) { + return Reflect.get(target, prop); + } + return () => {}; + }, + }, + ); + + const env = { ORDERS: createJsrpcBinding(allowedMethod), EXTERNAL: createJsrpcBinding(deniedMethod) }; + const instrumented = instrumentEnv(env, { rpcTracePropagationTargets: ['ORDERS'] }); + + instrumented.ORDERS.myRpcMethod('first'); + instrumented.EXTERNAL.myRpcMethod('first'); + + expect(allowedMethod).toHaveBeenCalledWith('first', { + __sentry_rpc_meta__: { + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: 'sentry-environment=production', + }, + }); + expect(deniedMethod).toHaveBeenCalledWith('first'); + }); }); }); diff --git a/packages/cloudflare/test/instrumentations/instrumentWorkerEntrypoint.test.ts b/packages/cloudflare/test/instrumentations/instrumentWorkerEntrypoint.test.ts index 116c9e9637fe..5330ca2c11c3 100644 --- a/packages/cloudflare/test/instrumentations/instrumentWorkerEntrypoint.test.ts +++ b/packages/cloudflare/test/instrumentations/instrumentWorkerEntrypoint.test.ts @@ -300,10 +300,7 @@ describe('instrumentWorkerEntrypoint', () => { } } const obj = Reflect.construct( - instrumentWorkerEntrypoint( - () => ({ enableRpcTracePropagation: true }), - TestClass as unknown as WorkerEntrypointConstructor, - ), + instrumentWorkerEntrypoint(() => ({}), TestClass as unknown as WorkerEntrypointConstructor), [createMockExecutionContext(), {}], ); @@ -315,7 +312,7 @@ describe('instrumentWorkerEntrypoint', () => { expect(obj.readValue(rpcMeta)).toBe('secret'); }); - it('strips RPC metadata even when trace propagation is disabled', () => { + it('strips RPC metadata without any propagation option', () => { const rpcMeta = { __sentry_rpc_meta__: { 'sentry-trace': 'trace-data' } }; const TestClass = class extends WorkerEntrypoint { inspect(...args: unknown[]) { @@ -323,10 +320,7 @@ describe('instrumentWorkerEntrypoint', () => { } }; const obj = Reflect.construct( - instrumentWorkerEntrypoint( - () => ({ enableRpcTracePropagation: false }), - TestClass as unknown as WorkerEntrypointConstructor, - ), + instrumentWorkerEntrypoint(() => ({}), TestClass as unknown as WorkerEntrypointConstructor), [createMockExecutionContext(), {}], ); @@ -541,7 +535,7 @@ describe('instrumentWorkerEntrypoint', () => { vi.clearAllMocks(); }); - it('passes instrumented env to the constructor when enableRpcTracePropagation is enabled', () => { + it('passes instrumented env to the constructor when rpcTracePropagationTargets matches', () => { const mockContext = createMockExecutionContext(); const doNamespace = { idFromName: vi.fn(), @@ -563,7 +557,7 @@ describe('instrumentWorkerEntrypoint', () => { }; const instrumented = instrumentWorkerEntrypoint( - () => ({ enableRpcTracePropagation: true }), + () => ({ rpcTracePropagationTargets: [/.*/] }), TestClass as unknown as WorkerEntrypointConstructor, ); Reflect.construct(instrumented, [mockContext, mockEnv]); @@ -571,7 +565,7 @@ describe('instrumentWorkerEntrypoint', () => { expect(constructorEnv).not.toBe(mockEnv); }); - it('exposes instrumented DurableObjectNamespace via this.env when enableRpcTracePropagation is enabled', async () => { + it('exposes instrumented DurableObjectNamespace via this.env when rpcTracePropagationTargets matches', async () => { vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', baggage: 'sentry-environment=production', @@ -602,7 +596,7 @@ describe('instrumentWorkerEntrypoint', () => { }; const instrumented = instrumentWorkerEntrypoint( - () => ({ enableRpcTracePropagation: true }), + () => ({ rpcTracePropagationTargets: [/.*/] }), TestClass as unknown as WorkerEntrypointConstructor, ); const obj = Reflect.construct(instrumented, [mockContext, mockEnv]); @@ -616,7 +610,7 @@ describe('instrumentWorkerEntrypoint', () => { }); }); - it('returns original DurableObjectNamespace via this.env when enableRpcTracePropagation is disabled', async () => { + it('returns original DurableObjectNamespace via this.env when rpcTracePropagationTargets is empty', async () => { vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', baggage: 'sentry-environment=production', @@ -646,17 +640,14 @@ describe('instrumentWorkerEntrypoint', () => { } }; - const instrumented = instrumentWorkerEntrypoint( - () => ({ enableRpcTracePropagation: false }), - TestClass as unknown as WorkerEntrypointConstructor, - ); + const instrumented = instrumentWorkerEntrypoint(() => ({}), TestClass as unknown as WorkerEntrypointConstructor); const obj = Reflect.construct(instrumented, [mockContext, mockEnv]); await obj.fetch(new Request('https://example.com')); expect(rpcMethod).toHaveBeenCalledWith('arg1'); }); - it('injects Sentry RPC meta into JSRPC calls via this.env when enableRpcTracePropagation is enabled', async () => { + it('injects Sentry RPC meta into JSRPC calls via this.env when rpcTracePropagationTargets matches', async () => { vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', baggage: 'sentry-environment=production', @@ -686,7 +677,7 @@ describe('instrumentWorkerEntrypoint', () => { }; const instrumented = instrumentWorkerEntrypoint( - () => ({ enableRpcTracePropagation: true }), + () => ({ rpcTracePropagationTargets: [/.*/] }), TestClass as unknown as WorkerEntrypointConstructor, ); const obj = Reflect.construct(instrumented, [mockContext, mockEnv]); @@ -700,7 +691,7 @@ describe('instrumentWorkerEntrypoint', () => { }); }); - it('does not inject Sentry RPC meta into JSRPC calls via this.env when enableRpcTracePropagation is disabled', async () => { + it('does not inject Sentry RPC meta into JSRPC calls via this.env when rpcTracePropagationTargets is empty', async () => { vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', baggage: 'sentry-environment=production', @@ -729,10 +720,7 @@ describe('instrumentWorkerEntrypoint', () => { } }; - const instrumented = instrumentWorkerEntrypoint( - () => ({ enableRpcTracePropagation: false }), - TestClass as unknown as WorkerEntrypointConstructor, - ); + const instrumented = instrumentWorkerEntrypoint(() => ({}), TestClass as unknown as WorkerEntrypointConstructor); const obj = Reflect.construct(instrumented, [mockContext, mockEnv]); await obj.fetch(new Request('https://example.com')); @@ -761,7 +749,7 @@ describe('instrumentWorkerEntrypoint', () => { }; const instrumented = instrumentWorkerEntrypoint( - () => ({ enableRpcTracePropagation: true }), + () => ({ rpcTracePropagationTargets: [/.*/] }), TestClass as unknown as WorkerEntrypointConstructor, ); const obj = Reflect.construct(instrumented, [mockContext, mockEnv]); @@ -788,7 +776,7 @@ describe('instrumentWorkerEntrypoint', () => { }; const instrumented = instrumentWorkerEntrypoint( - () => ({ enableRpcTracePropagation: true }), + () => ({ rpcTracePropagationTargets: [/.*/] }), TestClass as unknown as WorkerEntrypointConstructor, ); const obj = Reflect.construct(instrumented, [mockContext, mockEnv]); diff --git a/packages/cloudflare/test/utils/rpcPropagation.test.ts b/packages/cloudflare/test/utils/rpcPropagation.test.ts new file mode 100644 index 000000000000..156b30c68ed8 --- /dev/null +++ b/packages/cloudflare/test/utils/rpcPropagation.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest'; +import { createRpcPropagationResolver } from '../../src/utils/rpcPropagation'; + +describe('createRpcPropagationResolver', () => { + it('propagates to nothing when no options are available', () => { + const shouldPropagate = createRpcPropagationResolver(undefined); + + expect(shouldPropagate('MY_DO')).toBe(false); + }); + + it('propagates to nothing when the option is unset', () => { + const shouldPropagate = createRpcPropagationResolver({ rpcTracePropagationTargets: undefined }); + + expect(shouldPropagate('MY_DO')).toBe(false); + expect(shouldPropagate('EXTERNAL')).toBe(false); + }); + + it('propagates to nothing for an empty target list', () => { + const shouldPropagate = createRpcPropagationResolver({ rpcTracePropagationTargets: [] }); + + expect(shouldPropagate('MY_DO')).toBe(false); + }); + + it('propagates only to the targeted binding names', () => { + const shouldPropagate = createRpcPropagationResolver({ rpcTracePropagationTargets: ['MY_DO', 'EXTERNAL'] }); + + expect(shouldPropagate('MY_DO')).toBe(true); + expect(shouldPropagate('EXTERNAL')).toBe(true); + expect(shouldPropagate('OTHER')).toBe(false); + }); + + it('matches binding names exactly, never as a substring', () => { + const shouldPropagate = createRpcPropagationResolver({ rpcTracePropagationTargets: ['DB'] }); + + expect(shouldPropagate('DB')).toBe(true); + expect(shouldPropagate('MY_DB')).toBe(false); + expect(shouldPropagate('DB_REPLICA')).toBe(false); + }); + + it('supports regular expressions for pattern matching', () => { + const shouldPropagate = createRpcPropagationResolver({ rpcTracePropagationTargets: [/^SVC_/] }); + + expect(shouldPropagate('SVC_ORDERS')).toBe(true); + expect(shouldPropagate('SVC_USERS')).toBe(true); + expect(shouldPropagate('ORDERS')).toBe(false); + expect(shouldPropagate('PREFIXED_SVC_ORDERS')).toBe(false); + }); +}); From d68e2df727b293f33d90e55430384f03f1b6eef3 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Fri, 21 Aug 2026 22:14:30 +0300 Subject: [PATCH 2/2] fixup! feat(cloudflare)!: Replace enableRpcTracePropagation with rpcTracePropagationTargets --- MIGRATION.md | 16 ++++++++++++++-- docs/migration/v11-end-state.md | 16 +++++++++++++--- packages/cloudflare/test/options.test-d.ts | 2 +- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 5c3984abae46..af60176d222b 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -835,14 +835,26 @@ Sentry.init({ ); ``` -- The `instrumentPrototypeMethods` option of `instrumentDurableObjectWithSentry` was removed. Use `enableRpcTracePropagation` instead, which was introduced as its replacement in v10. +- The `enableRpcTracePropagation` option was removed. Trace context is no longer propagated to every binding on `env`. List the bindings you call in `rpcTracePropagationTargets` instead. Strings match a binding name exactly, regular expressions match by pattern. Receivers no longer take the option at all: an instrumented Durable Object or WorkerEntrypoint reads the trace context whenever a caller sends it. + +```diff + export default Sentry.withSentry( + (env) => ({ + dsn: env.SENTRY_DSN, +- enableRpcTracePropagation: true, ++ rpcTracePropagationTargets: ['ORDERS', /^SVC_/], + }), + handler, + ); +``` + +- The `instrumentPrototypeMethods` option of `instrumentDurableObjectWithSentry` was removed. A Durable Object instruments its RPC methods unconditionally now, so there is nothing to replace it with on the receiver. ```diff export const MyDO = Sentry.instrumentDurableObjectWithSentry( (env) => ({ dsn: env.SENTRY_DSN, - instrumentPrototypeMethods: true, -+ enableRpcTracePropagation: true, }), MyDOBase, ); diff --git a/docs/migration/v11-end-state.md b/docs/migration/v11-end-state.md index e127ce331818..cadb3555f34f 100644 --- a/docs/migration/v11-end-state.md +++ b/docs/migration/v11-end-state.md @@ -871,16 +871,26 @@ Sentry.init({ ); ``` -- The `enableRpcTracePropagation` option now defaults to `true`. Trace context is propagated across RPC calls (service bindings, Durable Objects, WorkerEntrypoints) unless you explicitly set `enableRpcTracePropagation: false`. +- The `enableRpcTracePropagation` option was removed. Trace context is no longer propagated to every binding on `env`. List the bindings you call in `rpcTracePropagationTargets` instead. Strings match a binding name exactly, regular expressions match by pattern. Receivers no longer take the option at all: an instrumented Durable Object or WorkerEntrypoint reads the trace context whenever a caller sends it. -- The `instrumentPrototypeMethods` option of `instrumentDurableObjectWithSentry` was removed. Use `enableRpcTracePropagation` instead, which was introduced as its replacement in v10. +```diff + export default Sentry.withSentry( + (env) => ({ + dsn: env.SENTRY_DSN, +- enableRpcTracePropagation: true, ++ rpcTracePropagationTargets: ['ORDERS', /^SVC_/], + }), + handler, + ); +``` + +- The `instrumentPrototypeMethods` option of `instrumentDurableObjectWithSentry` was removed. A Durable Object instruments its RPC methods unconditionally now, so there is nothing to replace it with on the receiver. ```diff export const MyDO = Sentry.instrumentDurableObjectWithSentry( (env) => ({ dsn: env.SENTRY_DSN, - instrumentPrototypeMethods: true, -+ enableRpcTracePropagation: true, }), MyDOBase, ); diff --git a/packages/cloudflare/test/options.test-d.ts b/packages/cloudflare/test/options.test-d.ts index a8ea84a92740..ea640619e3e2 100644 --- a/packages/cloudflare/test/options.test-d.ts +++ b/packages/cloudflare/test/options.test-d.ts @@ -105,7 +105,7 @@ describe('valid options keep compiling', () => { dsn: env.SENTRY_DSN, tracesSampleRate: 1, serverName: 'my-worker', - enableRpcTracePropagation: false, + rpcTracePropagationTargets: ['ORDERS', /^SVC_/], durableObjectSqlSpanAllowlist: ['cf_my_table', /^cf_reports_/], beforeSend: event => event, integrations: [],