feat(cloudflare): Add trace propagation for RPC method calls#20343
feat(cloudflare): Add trace propagation for RPC method calls#20343
Conversation
size-limit report 📦
|
|
Moved to draft, as tests are failing and I have to change 1-2 things that could reduce the amount lines added |
…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, ); ```
…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, ); ```
8a3eeb3 to
08d395b
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ 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.
s1gr1d
left a comment
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
You could make the check more strict here - just to make sure this is really from us.
| 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'; |
There was a problem hiding this comment.
Maybe also choosing a longer name here avoids collisions with user-defined values:
| const SENTRY_RPC_META_KEY = '__sentry'; | |
| const SENTRY_RPC_META_KEY = '__sentry_rpc_meta__'; |
| 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); |
There was a problem hiding this comment.
This can be golfed slightly (saw you were making some changes for bundle size)
| 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 || '' }, | |||
There was a problem hiding this comment.
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.
| { 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.

closes #19327
closes JS-1715
closes #16898
closes JS-680
closes #16760
closes JS-622
Summary
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
__sentryargument will remain in the args array and be passed to the user's method. I would count this as ok since:...argsto retrieve all argumentsOtherwise, 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