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..d4e57c230d9e 100644 --- a/dev-packages/cloudflare-integration-tests/suites/durableobject-scope/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/durableobject-scope/index.ts @@ -74,8 +74,8 @@ class ScopeDurableObjectBase extends DurableObject { export const ScopeDurableObject = Sentry.instrumentDurableObjectWithSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, - tracesSampleRate: 1, enableRpcTracePropagation: true, + tracesSampleRate: 1, }), ScopeDurableObjectBase, ); @@ -84,7 +84,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['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 7de3d56d7046..ae25da441b96 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 @@ -25,8 +25,8 @@ class SyncAlarmDurableObjectBase extends DurableObject { export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, - tracesSampleRate: 1.0, enableRpcTracePropagation: true, + tracesSampleRate: 1.0, }), SyncAlarmDurableObjectBase, ); @@ -35,7 +35,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['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 0746e8c13a8e..31e5ae1e28ec 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 @@ -23,8 +23,8 @@ class AlarmDurableObjectBase extends DurableObject { export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, - tracesSampleRate: 1.0, enableRpcTracePropagation: true, + tracesSampleRate: 1.0, }), AlarmDurableObjectBase, ); @@ -33,7 +33,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['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..0c3b523f4e1c 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 @@ -36,8 +36,8 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, traceLifecycle: 'static', - tracesSampleRate: 1.0, enableRpcTracePropagation: true, + tracesSampleRate: 1.0, }), MyDurableObjectBase, ); @@ -47,7 +47,7 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['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/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts index 170b2fe5d499..39595a6d1b67 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/index.ts @@ -44,8 +44,8 @@ class TestDurableObjectBase extends DurableObject { export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, - tracesSampleRate: 1.0, enableRpcTracePropagation: true, + tracesSampleRate: 1.0, }), TestDurableObjectBase, ); @@ -54,7 +54,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['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..2ffa03ddca17 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 rpcTracePropagationBindings 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 cbb6de4f514c..57d10efa3b31 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 @@ -31,7 +31,7 @@ export default withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['ECHO_HEADERS_DO'], }), { async fetch(request, env) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/no-propagation-worker-do/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-fetch-no-bindings/index.ts similarity index 100% rename from dev-packages/cloudflare-integration-tests/suites/tracing/propagation/no-propagation-worker-do/index.ts rename to dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-fetch-no-bindings/index.ts 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/worker-do-fetch-no-bindings/test.ts similarity index 89% rename from dev-packages/cloudflare-integration-tests/suites/tracing/propagation/no-propagation-worker-do/test.ts rename to dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-fetch-no-bindings/test.ts index 287d62b25c1f..7978f946667c 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/worker-do-fetch-no-bindings/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('propagates trace from worker to durable object over stub.fetch() when rpcTracePropagationBindings is empty', async ({ signal, }) => { let workerTraceId: string | undefined; @@ -57,14 +57,13 @@ it('does not propagate trace from worker to durable object when enableRpcTracePr await runner.completed(); expect(workerTraceId).toBeDefined(); - expect(doTraceId).toBeDefined(); - expect(workerTraceId).not.toBe(doTraceId); + expect(doTraceId).toBe(workerTraceId); expect(workerSpanId).toBeDefined(); - expect(doParentSpanId).toBeUndefined(); + expect(doParentSpanId).toBe(workerSpanId); }); -it('does not propagate trace from queue handler to durable object when enableRpcTracePropagation is disabled', async ({ +it('propagates trace from queue handler to durable object over stub.fetch() when rpcTracePropagationBindings is empty', async ({ signal, }) => { let queueTraceId: string | undefined; @@ -139,14 +138,13 @@ it('does not propagate trace from queue handler to durable object when enableRpc await runner.completed(); expect(queueTraceId).toBeDefined(); - expect(doTraceId).toBeDefined(); - expect(queueTraceId).not.toBe(doTraceId); + expect(doTraceId).toBe(queueTraceId); expect(queueSpanId).toBeDefined(); - expect(doParentSpanId).toBeUndefined(); + expect(doParentSpanId).toBe(queueSpanId); }); -it('does not propagate trace from scheduled handler to durable object when enableRpcTracePropagation is disabled', async ({ +it('propagates trace from scheduled handler to durable object over stub.fetch() when rpcTracePropagationBindings is empty', async ({ signal, }) => { let scheduledTraceId: string | undefined; @@ -201,9 +199,8 @@ it('does not propagate trace from scheduled handler to durable object when enabl await runner.completed(); expect(scheduledTraceId).toBeDefined(); - expect(doTraceId).toBeDefined(); - expect(scheduledTraceId).not.toBe(doTraceId); + expect(doTraceId).toBe(scheduledTraceId); expect(scheduledSpanId).toBeDefined(); - expect(doParentSpanId).toBeUndefined(); + expect(doParentSpanId).toBe(scheduledSpanId); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/no-propagation-worker-do/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-fetch-no-bindings/wrangler.jsonc similarity index 100% rename from dev-packages/cloudflare-integration-tests/suites/tracing/propagation/no-propagation-worker-do/wrangler.jsonc rename to dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-fetch-no-bindings/wrangler.jsonc diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc-disabled/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc-disabled/index.ts index 941f988971bc..cada6d7ac40c 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc-disabled/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc-disabled/index.ts @@ -7,12 +7,8 @@ interface Env { } class MyDurableObjectBase extends DurableObject { - async fetch(request: Request): Promise { - const url = new URL(request.url); - if (url.pathname === '/hello') { - return new Response('Hello, World!'); - } - return new Response('Not found', { status: 404 }); + async sayHello(name: string): Promise { + return `Hello, ${name}!`; } } @@ -35,11 +31,14 @@ export default Sentry.withSentry( const id = env.MY_DURABLE_OBJECT.idFromName('test'); const stub = env.MY_DURABLE_OBJECT.get(id); - if (url.pathname === '/do/hello') { - // Call DO via fetch instead of RPC - const doResponse = await stub.fetch(new Request('http://do/hello')); - const text = await doResponse.text(); - return new Response(text); + if (url.pathname === '/rpc/hello') { + return new Response(await stub.sayHello('World')); + } + + // Sentinel: makes the absence of a DO transaction deterministic. It is sent after the RPC + // call, so once it arrives everything the RPC call could have produced has arrived too. + if (url.pathname === '/sentinel') { + return new Response('Sentinel'); } return new Response('Not found', { status: 404 }); 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..38e320c7a41d 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,10 +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 }) => { - let workerTraceId: string | undefined; - let doTraceId: string | undefined; - +it('does not trace an RPC method call when rpcTracePropagationBindings is empty', async ({ signal }) => { const runner = createRunner(__dirname) .expect(envelope => { const transactionEvent = envelope[1]?.[0]?.[1] as Event; @@ -13,54 +10,30 @@ it('does not propagate trace when enableRpcTracePropagation is disabled', async expect(transactionEvent).toEqual( expect.objectContaining({ contexts: expect.objectContaining({ - trace: expect.objectContaining({ - op: 'http.server', - }), + trace: expect.objectContaining({ op: 'http.server' }), }), + transaction: 'GET /rpc/hello', }), ); - - const txName = transactionEvent.transaction as string; - const traceId = transactionEvent.contexts?.trace?.trace_id as string; - - if (txName === 'GET /do/hello') { - workerTraceId = traceId; - } else if (txName === 'GET /hello') { - doTraceId = traceId; - } }) + // Ordered: a `sayHello` transaction from the receiver would arrive here and fail this + // expectation. Without the trailing Sentry argument the receiver never traces the call. .expect(envelope => { const transactionEvent = envelope[1]?.[0]?.[1] as Event; expect(transactionEvent).toEqual( expect.objectContaining({ contexts: expect.objectContaining({ - trace: expect.objectContaining({ - op: 'http.server', - }), + trace: expect.objectContaining({ op: 'http.server' }), }), + transaction: 'GET /sentinel', }), ); - - const txName = transactionEvent.transaction as string; - const traceId = transactionEvent.contexts?.trace?.trace_id as string; - - if (txName === 'GET /do/hello') { - workerTraceId = traceId; - } else if (txName === 'GET /hello') { - doTraceId = traceId; - } }) - .unordered() .start(signal); - const response = await runner.makeRequest('get', '/do/hello'); - expect(response).toBe('Hello, World!'); + expect(await runner.makeRequest('get', '/rpc/hello')).toBe('Hello, World!'); + expect(await runner.makeRequest('get', '/sentinel')).toBe('Sentinel'); await runner.completed(); - - // Both transactions should exist but have different trace IDs (no propagation) - expect(workerTraceId).toBeDefined(); - expect(doTraceId).toBeDefined(); - expect(workerTraceId).not.toBe(doTraceId); }); 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 07e8a33a1f69..b76f55118c27 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 @@ -19,8 +19,8 @@ class MyDurableObjectBase extends DurableObject { export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, - tracesSampleRate: 1.0, enableRpcTracePropagation: true, + tracesSampleRate: 1.0, }), MyDurableObjectBase, ); @@ -29,7 +29,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['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 32464998b413..989db01b97b1 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 @@ -16,8 +16,8 @@ class MyDurableObjectBase extends DurableObject { export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, - tracesSampleRate: 1.0, enableRpcTracePropagation: true, + tracesSampleRate: 1.0, }), MyDurableObjectBase, ); @@ -26,7 +26,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['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 a049a1c796b3..539984eb5f48 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 @@ -9,7 +9,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['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 ca18a69c530f..9868f129427d 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 @@ -15,8 +15,8 @@ class MyDurableObjectBase extends DurableObject { export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, - tracesSampleRate: 1.0, enableRpcTracePropagation: true, + tracesSampleRate: 1.0, }), MyDurableObjectBase, ); @@ -25,7 +25,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['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 3465449ba2fe..c38bce0872fe 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 @@ -9,7 +9,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['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 77218af60fa1..0355a5864688 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 @@ -44,8 +44,8 @@ class MySubWorkerEntrypointBase extends BaseEntrypoint { export const BindingEntrypoint = Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, - tracesSampleRate: 1.0, enableRpcTracePropagation: true, + tracesSampleRate: 1.0, initialScope: { tags: { initial_scope: 'applied' } }, beforeSend(event) { event.tags = { ...event.tags, before_send: 'applied' }; @@ -56,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, @@ -65,4 +67,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 b37c2ab1ffe5..58ddec5080f3 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 { @@ -28,7 +31,10 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, 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. + rpcTracePropagationBindings: ['SUB_WORKER'], }), { async fetch(request, env, ctx) { @@ -60,6 +66,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..623ea9779fcc 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 rpcTracePropagationBindings 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 bd19f421dcff..707a43270ae7 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/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc-disabled/index.ts index 07e8445ca772..b97179565a3a 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc-disabled/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc-disabled/index.ts @@ -7,12 +7,8 @@ interface Env { } class MyDurableObjectBase extends DurableObject { - async fetch(request: Request): Promise { - const url = new URL(request.url); - if (url.pathname === '/hello') { - return new Response('Hello, World!'); - } - return new Response('Not found', { status: 404 }); + async sayHello(name: string): Promise { + return `Hello, ${name}!`; } } @@ -30,10 +26,14 @@ class MyWorkerEntrypointBase extends WorkerEntrypoint { const id = (this.env as Env).MY_DURABLE_OBJECT.idFromName('test'); const stub = (this.env as Env).MY_DURABLE_OBJECT.get(id); - if (url.pathname === '/do/hello') { - const doResponse = await stub.fetch(new Request('http://do/hello')); - const text = await doResponse.text(); - return new Response(text); + if (url.pathname === '/rpc/hello') { + return new Response(await stub.sayHello('World')); + } + + // Sentinel: makes the absence of a DO transaction deterministic. It is sent after the RPC + // call, so once it arrives everything the RPC call could have produced has arrived too. + if (url.pathname === '/sentinel') { + return new Response('Sentinel'); } return new Response('Not found', { status: 404 }); 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..f0bef1e2e402 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,10 +2,9 @@ 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 }) => { - let workerTraceId: string | undefined; - let doTraceId: string | undefined; - +it('does not trace an RPC method call when rpcTracePropagationBindings is empty (WorkerEntrypoint)', async ({ + signal, +}) => { const runner = createRunner(__dirname) .expect(envelope => { const transactionEvent = envelope[1]?.[0]?.[1] as Event; @@ -13,54 +12,30 @@ it('does not propagate trace when enableRpcTracePropagation is disabled (WorkerE expect(transactionEvent).toEqual( expect.objectContaining({ contexts: expect.objectContaining({ - trace: expect.objectContaining({ - op: 'http.server', - }), + trace: expect.objectContaining({ op: 'http.server' }), }), + transaction: 'GET /rpc/hello', }), ); - - const txName = transactionEvent.transaction as string; - const traceId = transactionEvent.contexts?.trace?.trace_id as string; - - if (txName === 'GET /do/hello') { - workerTraceId = traceId; - } else if (txName === 'GET /hello') { - doTraceId = traceId; - } }) + // Ordered: a `sayHello` transaction from the receiver would arrive here and fail this + // expectation. Without the trailing Sentry argument the receiver never traces the call. .expect(envelope => { const transactionEvent = envelope[1]?.[0]?.[1] as Event; expect(transactionEvent).toEqual( expect.objectContaining({ contexts: expect.objectContaining({ - trace: expect.objectContaining({ - op: 'http.server', - }), + trace: expect.objectContaining({ op: 'http.server' }), }), + transaction: 'GET /sentinel', }), ); - - const txName = transactionEvent.transaction as string; - const traceId = transactionEvent.contexts?.trace?.trace_id as string; - - if (txName === 'GET /do/hello') { - workerTraceId = traceId; - } else if (txName === 'GET /hello') { - doTraceId = traceId; - } }) - .unordered() .start(signal); - const response = await runner.makeRequest('get', '/do/hello'); - expect(response).toBe('Hello, World!'); + expect(await runner.makeRequest('get', '/rpc/hello')).toBe('Hello, World!'); + expect(await runner.makeRequest('get', '/sentinel')).toBe('Sentinel'); await runner.completed(); - - // Both transactions should exist but have different trace IDs (no propagation) - expect(workerTraceId).toBeDefined(); - expect(doTraceId).toBeDefined(); - expect(workerTraceId).not.toBe(doTraceId); }); 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 7633be9e7c8e..1edde6d59e65 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 @@ -19,8 +19,8 @@ class MyDurableObjectBase extends DurableObject { export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, - tracesSampleRate: 1.0, enableRpcTracePropagation: true, + tracesSampleRate: 1.0, }), MyDurableObjectBase, ); @@ -49,7 +49,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['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 97bb6b28b3bd..768655d7bab9 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 @@ -15,8 +15,8 @@ class MyDurableObjectBase extends DurableObject { export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, - tracesSampleRate: 1.0, enableRpcTracePropagation: true, + tracesSampleRate: 1.0, }), MyDurableObjectBase, ); @@ -40,7 +40,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['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 40577610004f..25dcf4c4770b 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 @@ -24,7 +24,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['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 3eb2eb4331ed..91e7450307ca 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 @@ -37,7 +37,8 @@ export const MyWorkflow = Sentry.instrumentWorkflowWithSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + // The workflow is itself a caller: `run` reaches the Durable Object through `this.env`. + rpcTracePropagationBindings: ['MY_DURABLE_OBJECT'], }), MyWorkflowBase, ); @@ -46,7 +47,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['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 201a1073d983..cdefd6957a92 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 @@ -14,6 +14,7 @@ const sentryOptions = (env: Env) => ({ tunnel: `http://localhost:3031/`, tracesSampleRate: 1, enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['MyAgent', 'MyChatAgent', 'MyManualChatAgent'], // Keep gen_ai spans embedded in the transaction (instead of streamed as a separate envelope // container) so they can be asserted on `transaction.spans`. streamGenAiSpans: false, 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..1b1cc4dcefbd 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 @@ -6,8 +6,8 @@ export default (env: Env) => ({ dsn: env.E2E_TEST_DSN, environment: 'qa', tunnel: 'http://localhost:3031/', - tracesSampleRate: 1.0, enableRpcTracePropagation: true, + tracesSampleRate: 1.0, 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 e5c5c6df7aa4..05877f619e3b 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 @@ -103,7 +103,7 @@ export default Sentry.withSentry( // We are doing a lot of events at once in this test bufferSize: 1000, }, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['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 703483303c3f..9920cc9c6e83 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 @@ -101,7 +101,7 @@ export default Sentry.withSentry( // We are doing a lot of events at once in this test bufferSize: 1000, }, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['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 5edabc8e1b8b..3e0424bd9d7f 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 @@ -118,7 +118,7 @@ export default Sentry.withSentry( // We are doing a lot of events at once in this test bufferSize: 1000, }, - enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['MY_DURABLE_OBJECT'], }), MyWorker, ); diff --git a/packages/cloudflare/src/client.ts b/packages/cloudflare/src/client.ts index 84d0cbf52522..ce42b8416a93 100644 --- a/packages/cloudflare/src/client.ts +++ b/packages/cloudflare/src/client.ts @@ -1,4 +1,4 @@ -import type { ClientOptions, Options, ServerRuntimeClientOptions } from '@sentry/core'; +import type { ClientOptions, Options, ServerRuntimeClientOptions, TracePropagationTargets } from '@sentry/core'; import { _INTERNAL_clearAiProviderSkips, applySdkMetadata, @@ -188,47 +188,58 @@ interface BaseCloudflareOptions { skipOpenTelemetrySetup?: 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. + * + * Setting this takes precedence over `enableRpcTracePropagation`: an allow list is the more + * precise statement, so a worker that sets both propagates only to the listed bindings. An empty + * array propagates to nothing, which is how a receiver keeps `enableRpcTracePropagation: true` + * without propagating to anything itself. + * + * The receiver still needs `enableRpcTracePropagation: true` to continue the trace it is sent. + * + * @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, + * rpcTracePropagationBindings: ['ORDERS', /^SVC_/], * }), * handler, * ); + * ``` + */ + rpcTracePropagationBindings?: TracePropagationTargets; + + /** + * Whether trace context is propagated over RPC calls between Workers, Durable Objects, and + * Service Bindings. * - * // Durable Object side (receiver) - * export const MyDO = Sentry.instrumentDurableObjectWithSentry( - * (env) => ({ - * dsn: env.SENTRY_DSN, - * enableRpcTracePropagation: true, - * }), - * MyDOBase, - * ); + * On the caller side, `true` appends the trace context as a trailing argument to every RPC method + * call on `env`, including bindings whose receiver does not run Sentry and therefore never strips + * that argument again. On the receiver side, `true` continues an incoming trace, creates a span + * per RPC method invocation, and captures errors thrown by RPC methods. * - * // WorkerEntrypoint side (receiver) - * export const MyEntrypoint = Sentry.withSentry( - * env => ({ dsn: env.SENTRY_DSN, enableRpcTracePropagation: true }), - * MyEntrypointBase, - * ); - * ``` + * @deprecated Use `rpcTracePropagationBindings` to name the bindings you call. This option will + * be removed in a future major version. Receivers keep using it until then. + * + * @default false */ enableRpcTracePropagation?: boolean; diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index 11ac570388f0..3f3e96829df7 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -292,6 +292,7 @@ export function finalizeWithRpcInstrumentation( // When using the deprecated `instrumentPrototypeMethods` option, always create spans. // When using the new `enableRpcTracePropagation`, only create spans when RPC metadata is present. + // eslint-disable-next-line typescript/no-deprecated const alwaysTrace = options.enableRpcTracePropagation === undefined; rpcInstanceStates.set(obj, { options, context, alwaysTrace }); diff --git a/packages/cloudflare/src/instrumentations/instrumentDurableObjectNamespace.ts b/packages/cloudflare/src/instrumentations/instrumentDurableObjectNamespace.ts index 4c29f6e9595e..321a90dd3c4e 100644 --- a/packages/cloudflare/src/instrumentations/instrumentDurableObjectNamespace.ts +++ b/packages/cloudflare/src/instrumentations/instrumentDurableObjectNamespace.ts @@ -13,8 +13,12 @@ export const STUB_NON_RPC_METHODS = new Set(['fetch', 'connect', 'dup']); * - `namespace.idFromName(name)` / `namespace.idFromString(id)` / `namespace.newUniqueId()` with breadcrumbs * * @param namespace - The DurableObjectNamespace to instrument + * @param propagateRpcTrace - Whether RPC method calls on the returned stubs carry trace context */ -export function instrumentDurableObjectNamespace(namespace: DurableObjectNamespace): DurableObjectNamespace { +export function instrumentDurableObjectNamespace( + namespace: DurableObjectNamespace, + propagateRpcTrace = false, +): DurableObjectNamespace { return new Proxy(namespace, { get(target, prop, _receiver) { const value = Reflect.get(target, prop) as unknown; @@ -27,7 +31,7 @@ export function instrumentDurableObjectNamespace(namespace: DurableObjectNamespa return function (this: unknown, ...args: unknown[]) { const stub = Reflect.apply(value, target, args); - return instrumentDurableObjectStub(stub); + return instrumentDurableObjectStub(stub, propagateRpcTrace); }; } @@ -41,8 +45,9 @@ export function instrumentDurableObjectNamespace(namespace: DurableObjectNamespa * and propagate trace context across RPC calls. * * @param stub - The DurableObjectStub to instrument + * @param propagateRpcTrace - Whether RPC method calls carry trace context */ -function instrumentDurableObjectStub(stub: DurableObjectStub): DurableObjectStub { +function instrumentDurableObjectStub(stub: DurableObjectStub, propagateRpcTrace: boolean): DurableObjectStub { return new Proxy(stub, { get(target, prop) { const value = Reflect.get(target, prop); @@ -51,7 +56,12 @@ function instrumentDurableObjectStub(stub: DurableObjectStub): DurableObjectStub return instrumentFetcher((...args) => Reflect.apply(value, target, args)); } - if (typeof value === 'function' && typeof prop === 'string' && !STUB_NON_RPC_METHODS.has(prop)) { + if ( + propagateRpcTrace && + typeof value === 'function' && + typeof prop === 'string' && + !STUB_NON_RPC_METHODS.has(prop) + ) { return (...args: unknown[]) => Reflect.apply(value, target, appendRpcMeta(args)); } diff --git a/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts b/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts index bb53e124568e..bfb215d2e034 100644 --- a/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts +++ b/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts @@ -93,6 +93,7 @@ function instrumentMethod( true, ); + // eslint-disable-next-line typescript/no-deprecated if (!options.enableRpcTracePropagation) { return captureMethod; } diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts b/packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts index 474fbf831b86..e25cc52ba21e 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts @@ -12,7 +12,7 @@ import { } from '../../utils/isBinding'; import { instrumentD1 } from './instrumentD1'; import { appendRpcMeta } from '../../utils/rpcMeta'; -import { getEffectiveRpcPropagation } from '../../utils/rpcOptions'; +import { createRpcPropagationResolver } from '../../utils/rpcPropagation'; import { instrumentDurableObjectNamespace, STUB_NON_RPC_METHODS } from '../instrumentDurableObjectNamespace'; import { instrumentFetcher } from './instrumentFetcher'; import { instrumentQueueProducer } from './instrumentQueueProducer'; @@ -45,7 +45,7 @@ export function instrumentEnv>(env: Env, opt return env; } - const rpcPropagation = options ? getEffectiveRpcPropagation(options) : false; + const shouldPropagateRpcTrace = createRpcPropagationResolver(options); return new Proxy(env, { get(target, prop, receiver) { @@ -94,12 +94,10 @@ export function instrumentEnv>(env: Env, opt return instrumented; } - if (!rpcPropagation) { - return item; - } + const propagateRpcTrace = shouldPropagateRpcTrace(String(prop)); if (isDurableObjectNamespace(item)) { - const instrumented = instrumentDurableObjectNamespace(item); + const instrumented = instrumentDurableObjectNamespace(item, propagateRpcTrace); instrumentedBindings.set(item, instrumented); return instrumented; } @@ -113,7 +111,12 @@ export function instrumentEnv>(env: Env, opt return instrumentFetcher((...args) => Reflect.apply(value, target, args)); } - if (typeof value === 'function' && typeof p === 'string' && !STUB_NON_RPC_METHODS.has(p)) { + if ( + propagateRpcTrace && + typeof value === 'function' && + typeof p === 'string' && + !STUB_NON_RPC_METHODS.has(p) + ) { return (...args: unknown[]) => Reflect.apply(value, target, appendRpcMeta(args)); } diff --git a/packages/cloudflare/src/utils/rpcPropagation.ts b/packages/cloudflare/src/utils/rpcPropagation.ts new file mode 100644 index 000000000000..0e1c8557bbbc --- /dev/null +++ b/packages/cloudflare/src/utils/rpcPropagation.ts @@ -0,0 +1,34 @@ +import { stringMatchesSomePattern } from '@sentry/core'; +import type { CloudflareOptions } from '../client'; +import { getEffectiveRpcPropagation } from './rpcOptions'; + +const PROPAGATE_TO_NONE = (): boolean => false; +const PROPAGATE_TO_ALL = (): boolean => true; + +/** + * Builds the per-binding predicate that decides whether a binding takes part in RPC trace + * propagation. + * + * `rpcTracePropagationBindings` wins over `enableRpcTracePropagation` as soon as it is set, an + * allow list is the more precise statement. An empty list therefore propagates to nothing, even + * next to `enableRpcTracePropagation: true`, which is how a receiver opts out of being a caller. + * Only leaving it unset falls back to the boolean. + * + * Callers only. Receivers continue an incoming trace whenever `enableRpcTracePropagation` is on, + * so they have nothing to match against. + */ +export function createRpcPropagationResolver(options: CloudflareOptions | undefined): (bindingName: string) => boolean { + const bindings = options?.rpcTracePropagationBindings; + + if (bindings === undefined) { + return options && getEffectiveRpcPropagation(options) ? PROPAGATE_TO_ALL : PROPAGATE_TO_NONE; + } + + if (!bindings.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, bindings, true); +} diff --git a/packages/cloudflare/test/instrumentations/instrumentDurableObjectNamespace.test.ts b/packages/cloudflare/test/instrumentations/instrumentDurableObjectNamespace.test.ts index 67c6420147ac..05c78ae40089 100644 --- a/packages/cloudflare/test/instrumentations/instrumentDurableObjectNamespace.test.ts +++ b/packages/cloudflare/test/instrumentations/instrumentDurableObjectNamespace.test.ts @@ -195,7 +195,7 @@ describe('instrumentDurableObjectNamespace', () => { myRpcMethod: rpcMethod, }), }; - const instrumented = instrumentDurableObjectNamespace(namespace); + const instrumented = instrumentDurableObjectNamespace(namespace, true); const stub = instrumented.get({ toString: () => 'id', equals: () => false } as any); (stub as any).myRpcMethod('arg1', 42); @@ -221,7 +221,7 @@ describe('instrumentDurableObjectNamespace', () => { myRpcMethod: rpcMethod, }), }; - const instrumented = instrumentDurableObjectNamespace(namespace); + const instrumented = instrumentDurableObjectNamespace(namespace, true); const stub = instrumented.get({ toString: () => 'id', equals: () => false } as any); (stub as any).myRpcMethod('arg1'); @@ -229,6 +229,29 @@ describe('instrumentDurableObjectNamespace', () => { expect(rpcMethod).toHaveBeenCalledWith('arg1'); }); + it('does not inject meta when RPC trace propagation is off for the binding', () => { + vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + }); + + const rpcMethod = vi.fn(); + const { namespace: originalNamespace } = createMockNamespace(); + const namespace = { + ...originalNamespace, + get: vi.fn().mockReturnValue({ + id: { toString: () => 'mock-id', equals: () => false, name: 'test' }, + fetch: vi.fn(), + myRpcMethod: rpcMethod, + }), + }; + const instrumented = instrumentDurableObjectNamespace(namespace); + + const stub = instrumented.get({ toString: () => 'id', equals: () => false } as any); + (stub as any).myRpcMethod('arg1', 42); + + expect(rpcMethod).toHaveBeenCalledWith('arg1', 42); + }); + it('does not wrap built-in stub methods (connect, dup)', () => { vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ 'sentry-trace': 'abc-def-1', @@ -246,7 +269,7 @@ describe('instrumentDurableObjectNamespace', () => { dup: dupFn, }), }; - const instrumented = instrumentDurableObjectNamespace(namespace); + const instrumented = instrumentDurableObjectNamespace(namespace, true); const stub = instrumented.get({ toString: () => 'id', equals: () => false } as any); @@ -263,7 +286,7 @@ describe('instrumentDurableObjectNamespace', () => { ...originalNamespace, someProperty: 'value', }; - const instrumented = instrumentDurableObjectNamespace(namespace); + const instrumented = instrumentDurableObjectNamespace(namespace, true); expect((instrumented as any).someProperty).toBe('value'); }); diff --git a/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts b/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts index 72f9d0774507..f0a0380db3aa 100644 --- a/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts +++ b/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts @@ -3,9 +3,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { instrumentEnv } from '../../src/instrumentations/worker/instrumentEnv'; vi.mock('../../src/instrumentations/instrumentDurableObjectNamespace', () => ({ - instrumentDurableObjectNamespace: vi.fn((namespace: unknown) => ({ + instrumentDurableObjectNamespace: vi.fn((namespace: unknown, propagateRpcTrace: boolean) => ({ __instrumented: true, __original: namespace, + __propagateRpcTrace: propagateRpcTrace, })), STUB_NON_RPC_METHODS: new Set(['fetch', 'connect', 'dup']), })); @@ -74,7 +75,7 @@ describe('instrumentEnv', () => { expect(instrumented.UNKNOWN).toBe(unknownBinding); }); - it('does not instrument DurableObjectNamespace when enableRpcTracePropagation is disabled', () => { + it('instruments DurableObjectNamespace bindings without RPC propagation when the allowlist is empty', () => { const doNamespace = { idFromName: vi.fn(), idFromString: vi.fn(), @@ -84,24 +85,36 @@ describe('instrumentEnv', () => { const env = { COUNTER: doNamespace }; const instrumented = instrumentEnv(env); - // DO bindings pass through untouched when RPC propagation is disabled - expect(instrumented.COUNTER).toBe(doNamespace); - expect(instrumentDurableObjectNamespace).not.toHaveBeenCalled(); + expect((instrumented.COUNTER as any).__instrumented).toBe(true); + expect(instrumentDurableObjectNamespace).toHaveBeenCalledWith(doNamespace, false); }); - it('detects and instruments DurableObjectNamespace bindings when enableRpcTracePropagation is enabled', () => { - const doNamespace = { - idFromName: vi.fn(), - idFromString: vi.fn(), - get: vi.fn(), - newUniqueId: vi.fn(), - }; - const env = { COUNTER: doNamespace }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); + it('enables RPC propagation only for 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, { rpcTracePropagationBindings: ['COUNTER'] }); - const result = instrumented.COUNTER; - expect(instrumentDurableObjectNamespace).toHaveBeenCalledWith(doNamespace); - expect((result as any).__instrumented).toBe(true); + expect((instrumented.COUNTER as any).__instrumented).toBe(true); + expect((instrumented.SESSIONS as any).__instrumented).toBe(true); + expect(instrumentDurableObjectNamespace).toHaveBeenCalledWith(allowed, true); + expect(instrumentDurableObjectNamespace).toHaveBeenCalledWith(denied, false); + }); + + 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 }; + instrumentEnv(env, { rpcTracePropagationBindings: ['COUNTER'] }).MY_COUNTER; + + expect(instrumentDurableObjectNamespace).toHaveBeenCalledWith(doNamespace, false); + }); + + 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 }; + instrumentEnv(env, { rpcTracePropagationBindings: [/^SVC_/] }).SVC_ORDERS; + + expect(instrumentDurableObjectNamespace).toHaveBeenCalledWith(doNamespace, true); }); it('caches instrumented bindings across repeated access', () => { @@ -112,7 +125,7 @@ describe('instrumentEnv', () => { newUniqueId: vi.fn(), }; const env = { COUNTER: doNamespace }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); + const instrumented = instrumentEnv(env, { rpcTracePropagationBindings: [/.*/] }); const first = instrumented.COUNTER; const second = instrumented.COUNTER; @@ -135,20 +148,20 @@ describe('instrumentEnv', () => { newUniqueId: vi.fn(), }; const env = { COUNTER: doNamespace1, SESSIONS: doNamespace2 }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); + const instrumented = instrumentEnv(env, { rpcTracePropagationBindings: [/.*/] }); instrumented.COUNTER; instrumented.SESSIONS; expect(instrumentDurableObjectNamespace).toHaveBeenCalledTimes(2); - expect(instrumentDurableObjectNamespace).toHaveBeenCalledWith(doNamespace1); - expect(instrumentDurableObjectNamespace).toHaveBeenCalledWith(doNamespace2); + expect(instrumentDurableObjectNamespace).toHaveBeenCalledWith(doNamespace1, true); + expect(instrumentDurableObjectNamespace).toHaveBeenCalledWith(doNamespace2, true); }); - it('does not wrap JSRPC proxy when enableRpcTracePropagation is disabled', () => { - const mockFetch = vi.fn(); + it('wraps JSRPC bindings for fetch instrumentation even when the allowlist is empty', () => { + const rpcMethod = vi.fn(); const jsrpcProxy = new Proxy( - { fetch: mockFetch }, + { fetch: vi.fn(), myRpcMethod: rpcMethod }, { get(target, prop) { if (prop in target) { @@ -162,33 +175,11 @@ describe('instrumentEnv', () => { const env = { SERVICE: jsrpcProxy }; const instrumented = instrumentEnv(env); - const result = instrumented.SERVICE; - // Should be the same reference — not wrapped when propagation is disabled - expect(result).toBe(jsrpcProxy); - expect(instrumentDurableObjectNamespace).not.toHaveBeenCalled(); - }); - - it('wraps JSRPC proxy with a Proxy that instruments fetch when enableRpcTracePropagation is enabled', () => { - const mockFetch = vi.fn(); - const jsrpcProxy = new Proxy( - { fetch: mockFetch }, - { - get(target, prop) { - if (prop in target) { - return Reflect.get(target, prop); - } - // JSRPC behavior: return truthy for any property - return () => {}; - }, - }, - ); - const env = { SERVICE: jsrpcProxy }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); - - const result = instrumented.SERVICE; - // Should NOT be the same reference — it's wrapped in a Proxy + const result = instrumented.SERVICE as { myRpcMethod: (arg: string) => void }; expect(result).not.toBe(jsrpcProxy); - expect(instrumentDurableObjectNamespace).not.toHaveBeenCalled(); + + result.myRpcMethod('arg1'); + expect(rpcMethod).toHaveBeenCalledWith('arg1'); }); it('does not instrument JSRPC proxies as DurableObjectNamespace', () => { @@ -248,12 +239,12 @@ describe('instrumentEnv', () => { newUniqueId: vi.fn(), }; const env = { MY_QUEUE: queue, COUNTER: doNamespace }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); + const instrumented = instrumentEnv(env, { rpcTracePropagationBindings: [/.*/] }); // Access both — DO instrumentation only fires on property access expect(instrumented.MY_QUEUE).not.toBe(queue); instrumented.COUNTER; - expect(instrumentDurableObjectNamespace).toHaveBeenCalledWith(doNamespace); + expect(instrumentDurableObjectNamespace).toHaveBeenCalledWith(doNamespace, true); }); it('wraps RateLimit bindings in a proxy and forwards calls', async () => { @@ -342,13 +333,24 @@ describe('instrumentEnv', () => { ); } - it('does not instrument mTLS Fetcher when enableRpcTracePropagation is disabled', () => { - const mockFetch = vi.fn(); + it('instruments mTLS Fetcher fetch when rpcTracePropagationBindings is empty', async () => { + vi.spyOn(SentryCore, '_INTERNAL_getTracingHeadersForFetchRequest').mockReturnValue({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + }); + + const mockFetch = vi.fn().mockResolvedValue(new Response('ok')); const mtlsFetcher = createMtlsFetcherProxy(mockFetch); const env = { MY_CERT: mtlsFetcher }; const instrumented = instrumentEnv(env); - expect(instrumented.MY_CERT).toBe(mtlsFetcher); + expect(instrumented.MY_CERT).not.toBe(mtlsFetcher); + + await instrumented.MY_CERT.fetch('https://example.com/api'); + + const [, init] = mockFetch.mock.calls[0]!; + expect(new Headers(init?.headers).get('sentry-trace')).toBe( + '12345678901234567890123456789012-1234567890123456-1', + ); }); it('preserves existing headers and response on mTLS Fetcher fetch', async () => { @@ -361,7 +363,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, { rpcTracePropagationBindings: [/.*/] }); const response = await instrumented.MY_CERT.fetch('https://example.com/api', { headers: { Authorization: 'Bearer client-cert-token' }, @@ -378,7 +380,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 (rpcTracePropagationBindings not set)', () => { vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', baggage: 'sentry-environment=production', @@ -401,11 +403,11 @@ describe('instrumentEnv', () => { instrumented.SERVICE.myRpcMethod('arg1', 42); - // Without enableRpcTracePropagation, no metadata should be injected + // Without rpcTracePropagationBindings, 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 rpcTracePropagationBindings matches', () => { vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', baggage: 'sentry-environment=production', @@ -424,7 +426,7 @@ describe('instrumentEnv', () => { }, ); const env = { SERVICE: jsrpcProxy }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); + const instrumented = instrumentEnv(env, { rpcTracePropagationBindings: [/.*/] }); instrumented.SERVICE.myRpcMethod('arg1', 42); @@ -455,7 +457,7 @@ describe('instrumentEnv', () => { }, ); const env = { SERVICE: jsrpcProxy }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); + const instrumented = instrumentEnv(env, { rpcTracePropagationBindings: [/.*/] }); instrumented.SERVICE.fetch('https://example.com'); @@ -480,11 +482,50 @@ describe('instrumentEnv', () => { }, ); const env = { SERVICE: jsrpcProxy }; - const instrumented = instrumentEnv(env, { enableRpcTracePropagation: true }); + const instrumented = instrumentEnv(env, { rpcTracePropagationBindings: [/.*/] }); 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, { rpcTracePropagationBindings: ['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/utils/rpcPropagation.test.ts b/packages/cloudflare/test/utils/rpcPropagation.test.ts new file mode 100644 index 000000000000..e806eab26f0b --- /dev/null +++ b/packages/cloudflare/test/utils/rpcPropagation.test.ts @@ -0,0 +1,85 @@ +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({ rpcTracePropagationBindings: undefined }); + + expect(shouldPropagate('MY_DO')).toBe(false); + expect(shouldPropagate('EXTERNAL')).toBe(false); + }); + + it('falls back to enableRpcTracePropagation when no bindings are listed', () => { + // eslint-disable-next-line typescript/no-deprecated + const shouldPropagate = createRpcPropagationResolver({ enableRpcTracePropagation: true }); + + expect(shouldPropagate('MY_DO')).toBe(true); + expect(shouldPropagate('EXTERNAL')).toBe(true); + }); + + it('falls back to instrumentPrototypeMethods when no bindings are listed', () => { + // eslint-disable-next-line typescript/no-deprecated + const shouldPropagate = createRpcPropagationResolver({ instrumentPrototypeMethods: true }); + + expect(shouldPropagate('MY_DO')).toBe(true); + }); + + it('lets the binding list win over enableRpcTracePropagation', () => { + const shouldPropagate = createRpcPropagationResolver({ + // eslint-disable-next-line typescript/no-deprecated + enableRpcTracePropagation: true, + rpcTracePropagationBindings: ['MY_DO'], + }); + + expect(shouldPropagate('MY_DO')).toBe(true); + expect(shouldPropagate('EXTERNAL')).toBe(false); + }); + + it('propagates to nothing for an empty target list', () => { + const shouldPropagate = createRpcPropagationResolver({ rpcTracePropagationBindings: [] }); + + expect(shouldPropagate('MY_DO')).toBe(false); + }); + + it('propagates to nothing for an empty target list next to enableRpcTracePropagation', () => { + const shouldPropagate = createRpcPropagationResolver({ + // eslint-disable-next-line typescript/no-deprecated + enableRpcTracePropagation: true, + rpcTracePropagationBindings: [], + }); + + expect(shouldPropagate('MY_DO')).toBe(false); + expect(shouldPropagate('EXTERNAL')).toBe(false); + }); + + it('propagates only to the targeted binding names', () => { + const shouldPropagate = createRpcPropagationResolver({ rpcTracePropagationBindings: ['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({ rpcTracePropagationBindings: ['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({ rpcTracePropagationBindings: [/^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); + }); +});