Skip to content

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

Merged
JPeer264 merged 4 commits intodevelopfrom
jp/real-rpc
Apr 27, 2026
Merged

feat(cloudflare): Add trace propagation for RPC method calls#20343
JPeer264 merged 4 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 📦

Path Size % Change Change
@sentry/browser 25.96 kB - -
@sentry/browser - with treeshaking flags 24.44 kB - -
@sentry/browser (incl. Tracing) 43.89 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 45.53 kB - -
@sentry/browser (incl. Tracing, Profiling) 48.84 kB - -
@sentry/browser (incl. Tracing, Replay) 83.09 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 72.59 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 87.77 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 100.38 kB - -
@sentry/browser (incl. Feedback) 43.18 kB - -
@sentry/browser (incl. sendFeedback) 30.77 kB - -
@sentry/browser (incl. FeedbackAsync) 35.93 kB - -
@sentry/browser (incl. Metrics) 27.25 kB - -
@sentry/browser (incl. Logs) 27.38 kB - -
@sentry/browser (incl. Metrics & Logs) 28.07 kB - -
@sentry/react 27.72 kB - -
@sentry/react (incl. Tracing) 46.13 kB - -
@sentry/vue 30.81 kB - -
@sentry/vue (incl. Tracing) 45.71 kB - -
@sentry/svelte 25.98 kB - -
CDN Bundle 28.66 kB - -
CDN Bundle (incl. Tracing) 46.12 kB - -
CDN Bundle (incl. Logs, Metrics) 30.03 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 47.17 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 68.99 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) 89 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 90.06 kB - -
CDN Bundle - uncompressed 83.91 kB - -
CDN Bundle (incl. Tracing) - uncompressed 137.82 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 88.06 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 141.23 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 211.63 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 255.26 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 258.66 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 268.96 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 272.35 kB - -
@sentry/nextjs (client) 48.62 kB - -
@sentry/sveltekit (client) 44.33 kB - -
@sentry/node-core 58.52 kB +0.02% +9 B 🔺
@sentry/node 175.84 kB +0.01% +14 B 🔺
@sentry/node - without tracing 98.32 kB +0.02% +12 B 🔺
@sentry/aws-serverless 115.52 kB -0.01% -3 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
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)
);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I would answer it like this one: #20343 (comment)

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.

yeah now with the longer key, it's VERY unlikely

Comment thread packages/cloudflare/src/utils/rpcMeta.ts Outdated
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 thread packages/cloudflare/src/durableobject.ts Outdated
@@ -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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The reason is that I retook the type SerializedTraceData from core, and there it is written in kebab case. I'd like to keep it with this case, even though I don't really like it and it adds couple of bytes 😭

@JPeer264 JPeer264 requested a review from s1gr1d April 27, 2026 07:17
@JPeer264 JPeer264 requested a review from isaacs April 27, 2026 07:17
Comment on lines +20 to +22
}

/**
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The implementation uses the key __sentry_rpc_meta__ to append RPC metadata, but tests and the PR description refer to __sentry. This mismatch will break metadata extraction.
Severity: MEDIUM

Suggested Fix

Ensure the key used for appending RPC metadata is consistent across the implementation, tests, and documentation. Align on either __sentry_rpc_meta__ or __sentry and update the code and tests accordingly to use the same constant.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/cloudflare/src/utils/rpcMeta.ts#L20-L22

Potential issue: The implementation in `rpcMeta.ts` appends Sentry RPC metadata to
function arguments using the key `__sentry_rpc_meta__`. However, the associated tests
and the pull request description indicate that the expected key is `__sentry`. This
discrepancy means that the logic to extract the metadata from the arguments list on the
receiving end will fail, as it will be looking for the wrong key. Consequently, the
metadata will not be correctly processed and will be passed as an extra argument to the
user's RPC method, which is not the intended behavior.

Comment thread packages/cloudflare/test/utils/rpcMeta.test.ts Outdated
Comment on lines +47 to +48
get(target, prop) {
const value = Reflect.get(target, prop);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The proxy get trap for DurableObjectStub and JSRPC proxies omits the receiver argument in Reflect.get, which can cause 'Illegal invocation' errors on native getters.
Severity: HIGH

Suggested Fix

In the proxy get traps, change the call from Reflect.get(target, prop) to Reflect.get(target, prop, target). This ensures that when accessing getters, the this context refers to the original object (target) rather than the proxy, satisfying internal brand checks.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location:
packages/cloudflare/src/instrumentations/instrumentDurableObjectNamespace.ts#L47-L48

Potential issue: The proxy `get` handlers in `instrumentDurableObjectNamespace.ts` and
`instrumentEnv.ts` call `Reflect.get(target, prop)` without specifying a `receiver`.
This can cause 'Illegal invocation: function called with incorrect `this` reference'
errors when accessing properties on proxied Cloudflare objects like `DurableObjectStub`.
These objects may have native getters that perform internal brand checks on `this`. An
existing instrumentation for `DurableObjectStorage` in the codebase already accounts for
this by explicitly passing the `target` as the receiver, but this pattern was not
followed in the new code.

Also affects:

  • packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts:62~63

Comment thread packages/cloudflare/src/durableobject.ts
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 1 potential issue.

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 7c90dce. Configure here.

Comment thread packages/cloudflare/src/durableobject.ts
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