Skip to content

feat(cloudflare): Add trace propagation for RPC method calls#20343

Open
JPeer264 wants to merge 2 commits intodevelopfrom
jp/real-rpc
Open

feat(cloudflare): Add trace propagation for RPC method calls#20343
JPeer264 wants to merge 2 commits intodevelopfrom
jp/real-rpc

Conversation

@JPeer264
Copy link
Copy Markdown
Member

@JPeer264 JPeer264 commented Apr 16, 2026

closes #19327
closes JS-1715

closes #16898
closes JS-680

closes #16760
closes JS-622

Summary

Most of the additions are tests, the main implementation is rather small

Adds trace propagation for Cloudflare Workers RPC method calls to Durable Objects.

This is admittedly a bit of a hack: Cap'n Proto (which powers Cloudflare RPC) has no native support for headers or metadata. To work around this, we append our trace data (sentry-trace + baggage) as a trailing argument object { __sentry: { trace, baggage } } to every RPC call. On the receiving DO side, we strip this argument before the user's method is invoked, so it's completely transparent.

Caveat: If the Durable Object is not instrumented with Sentry, the trailing __sentry argument will remain in the args array and be passed to the user's method. I would count this as ok since:

  • Users opting into RPC instrumentation are expected to instrument both sides
  • The extra argument is easy to ignore in most cases, unless users use ...args to retrieve all arguments

Otherwise, trace propagation should be seamless across Worker → DO and Worker → Worker → DO call chains.

How it works

As mentioned above a Sentry trace object is appended on each call

const id = env.MY_DURABLE_OBJECT.idFromName('test');
const stub = env.MY_DURABLE_OBJECT.get(id);

// User's RPC call
const result = await stub.sayHello('World');

// What is actually sent (transparent to user)
const result = await stub.sayHello('World', { __sentry: { trace, baggage } });

@JPeer264 JPeer264 self-assigned this Apr 16, 2026
Comment thread packages/cloudflare/src/utils/rpcOptions.ts
Comment thread packages/cloudflare/src/utils/rpcOptions.ts
Comment thread packages/cloudflare/src/durableobject.ts
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 16, 2026

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 25.98 kB - -
@sentry/browser - with treeshaking flags 24.46 kB - -
@sentry/browser (incl. Tracing) 43.91 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 45.54 kB - -
@sentry/browser (incl. Tracing, Profiling) 48.87 kB - -
@sentry/browser (incl. Tracing, Replay) 83.11 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 72.59 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 87.79 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 100.05 kB - -
@sentry/browser (incl. Feedback) 42.78 kB - -
@sentry/browser (incl. sendFeedback) 30.65 kB - -
@sentry/browser (incl. FeedbackAsync) 35.64 kB - -
@sentry/browser (incl. Metrics) 27.27 kB - -
@sentry/browser (incl. Logs) 27.4 kB - -
@sentry/browser (incl. Metrics & Logs) 28.09 kB - -
@sentry/react 27.73 kB - -
@sentry/react (incl. Tracing) 46.14 kB - -
@sentry/vue 30.83 kB - -
@sentry/vue (incl. Tracing) 45.72 kB - -
@sentry/svelte 26 kB - -
CDN Bundle 28.66 kB - -
CDN Bundle (incl. Tracing) 46.13 kB - -
CDN Bundle (incl. Logs, Metrics) 30.04 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 47.17 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 69.02 kB - -
CDN Bundle (incl. Tracing, Replay) 83.19 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 84.22 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 88.68 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 89.76 kB - -
CDN Bundle - uncompressed 83.94 kB - -
CDN Bundle (incl. Tracing) - uncompressed 137.84 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 88.08 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 141.26 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 211.66 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 255.29 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 258.68 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 268.2 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 271.59 kB - -
@sentry/nextjs (client) 48.66 kB - -
@sentry/sveltekit (client) 44.34 kB - -
@sentry/node-core 58.37 kB +0.02% +8 B 🔺
@sentry/node 175.69 kB +0.01% +13 B 🔺
@sentry/node - without tracing 98.32 kB +0.02% +12 B 🔺
@sentry/aws-serverless 115.35 kB +0.01% +11 B 🔺

View base workflow run

@JPeer264
Copy link
Copy Markdown
Member Author

Moved to draft, as tests are failing and I have to change 1-2 things that could reduce the amount lines added

JPeer264 added a commit that referenced this pull request Apr 16, 2026
…pagation (#20345)

follow up to #19991

It is better to release it first with an option to be enabled, that
would then also be in line with #20343, otherwise `.fetch()` RPC calls
would work without any option and the actual Cap'n'Proto RPC calls
wouldn't work without. That would be an odd experience.

### New option: `enableRpcTracePropagation`

> `instrumentPrototypeMethods` has been deprecated in favor of
`enableRpcTracePropagation`

Replaces the deprecated `instrumentPrototypeMethods` option with a
clearer name that describes what it actually does. This option must be
enabled on **both** the caller (Worker) and receiver (Durable Object)
sides for trace propagation to work.

It is also worth to mention that the implementation of "instrumenting
prototype methods" has changed to a Proxy.

```ts
// Worker side
export default Sentry.withSentry(
  (env) => ({
    dsn: env.SENTRY_DSN,
    enableRpcTracePropagation: true,
  }),
  handler,
);

// Durable Object side
export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
  (env) => ({
    dsn: env.SENTRY_DSN,
    enableRpcTracePropagation: true,
  }),
  MyDurableObjectBase,
);
```
andreiborza pushed a commit that referenced this pull request Apr 20, 2026
…pagation (#20345)

follow up to #19991

It is better to release it first with an option to be enabled, that
would then also be in line with #20343, otherwise `.fetch()` RPC calls
would work without any option and the actual Cap'n'Proto RPC calls
wouldn't work without. That would be an odd experience.

### New option: `enableRpcTracePropagation`

> `instrumentPrototypeMethods` has been deprecated in favor of
`enableRpcTracePropagation`

Replaces the deprecated `instrumentPrototypeMethods` option with a
clearer name that describes what it actually does. This option must be
enabled on **both** the caller (Worker) and receiver (Durable Object)
sides for trace propagation to work.

It is also worth to mention that the implementation of "instrumenting
prototype methods" has changed to a Proxy.

```ts
// Worker side
export default Sentry.withSentry(
  (env) => ({
    dsn: env.SENTRY_DSN,
    enableRpcTracePropagation: true,
  }),
  handler,
);

// Durable Object side
export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
  (env) => ({
    dsn: env.SENTRY_DSN,
    enableRpcTracePropagation: true,
  }),
  MyDurableObjectBase,
);
```
@JPeer264 JPeer264 force-pushed the jp/real-rpc branch 3 times, most recently from 8a3eeb3 to 08d395b Compare April 22, 2026 08:51
@JPeer264 JPeer264 marked this pull request as ready for review April 22, 2026 16:10
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f76e06c. Configure here.

Comment thread packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts
Comment thread packages/cloudflare/src/utils/rpcMeta.ts
Copy link
Copy Markdown
Member

@s1gr1d s1gr1d left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good approach for this - first I thought about using a more hidden Symbol or a non-enumerable property here but this wouldn't survive serialization with Cap'n Proto.

Just one comment to define the key a bit tighter.

return false;
}
const sentry = (value as SentryRpcMeta).__sentry;
return typeof sentry === 'object' && sentry !== null;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could make the check more strict here - just to make sure this is really from us.

Suggested change
return typeof sentry === 'object' && sentry !== null;
return (
typeof sentry === 'object' &&
sentry !== null &&
('sentry-trace' in sentry || 'baggage' in sentry)
);

* This enables transparent trace propagation across Cloudflare Workers RPC
* calls (Cap'n Proto), which have no native header/metadata support.
*/
const SENTRY_RPC_META_KEY = '__sentry';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also choosing a longer name here avoids collisions with user-defined values:

Suggested change
const SENTRY_RPC_META_KEY = '__sentry';
const SENTRY_RPC_META_KEY = '__sentry_rpc_meta__';

Copy link
Copy Markdown
Member

@isaacs isaacs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not much to add beyond what @s1gr1d already suggested, but possibly some opportunities to shrink the code a tiny bit more.

Comment on lines +149 to +159
if (typeof prop !== 'string' || BUILT_IN_DO_METHODS.has(prop)) {
return Reflect.get(proxyTarget, prop, receiver);
}

const cached = rpcMethodCache.get(prop);

if (cached) {
return cached;
}

const value = Reflect.get(proxyTarget, prop, receiver);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be golfed slightly (saw you were making some changes for bundle size)

Suggested change
if (typeof prop !== 'string' || BUILT_IN_DO_METHODS.has(prop)) {
return Reflect.get(proxyTarget, prop, receiver);
}
const cached = rpcMethodCache.get(prop);
if (cached) {
return cached;
}
const value = Reflect.get(proxyTarget, prop, receiver);
const value = Reflect.get(proxyTarget, prop, receiver);
if (typeof prop !== 'string' || BUILT_IN_DO_METHODS.has(prop)) {
return value
}
const cached = rpcMethodCache.get(prop);
if (cached) {
return cached;
}

@@ -213,6 +228,13 @@ export function wrapMethodWithSentry<T extends OriginalMethod>(
});
};

if (rpcMeta) {
return continueTrace(
{ sentryTrace: rpcMeta['sentry-trace'] || '', baggage: rpcMeta.baggage || '' },
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the only place we consume it, is there a reason it's called sentry-trace rather than sentryTrace? The css-case name makes sense when it's in HTTP headers, of course, but since this is always a plain old JS object, it seems like making it camelCase would be a little simpler.

Suggested change
{ sentryTrace: rpcMeta['sentry-trace'] || '', baggage: rpcMeta.baggage || '' },
rpcMeta,

Possibly would require setting the default '' values in the extractRpcMeta method, and obviously updating the type everywhere else of course.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants