Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1200,16 +1200,26 @@ Sentry.httpIntegration({
);
```

- The `enableRpcTracePropagation` option now defaults to `true`. Trace context is propagated across RPC calls (service bindings, Durable Objects, WorkerEntrypoints) unless you explicitly set `enableRpcTracePropagation: false`.
- The `enableRpcTracePropagation` option was removed. Trace context is no longer appended to every RPC call on `env`. List the bindings you call in `rpcTracePropagationBindings` instead. Strings match a binding name exactly, regular expressions match by pattern. The option covers RPC method calls only, because they carry the trace context as a trailing argument that a non-Sentry receiver would see as a real argument. `stub.fetch()` and service binding `fetch()` carry it in HTTP headers, so they propagate regardless of this option. Receivers no longer take the option at all: an instrumented Durable Object or WorkerEntrypoint reads the trace context whenever a caller sends it.

- The `instrumentPrototypeMethods` option of `instrumentDurableObjectWithSentry` was removed. Use `enableRpcTracePropagation` instead, which was introduced as its replacement in v10.
```diff
export default Sentry.withSentry(
(env) => ({
dsn: env.SENTRY_DSN,
- enableRpcTracePropagation: true,
+ rpcTracePropagationBindings: ['ORDERS', /^SVC_/],
}),
handler,
);
```

- The `instrumentPrototypeMethods` option of `instrumentDurableObjectWithSentry` was removed. A Durable Object's prototype methods are now wrapped unconditionally, so every RPC method is instrumented and there is no longer an option to turn this on. Delete the option from your config.

```diff
export const MyDO = Sentry.instrumentDurableObjectWithSentry(
(env) => ({
dsn: env.SENTRY_DSN,
- instrumentPrototypeMethods: true,
+ enableRpcTracePropagation: true,
}),
MyDOBase,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ export const CacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
CacheDurableObjectBase,
);
Expand All @@ -124,7 +123,6 @@ export const NoCacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
cacheClient: false,
enableRpcTracePropagation: true,
}),
NoCacheDurableObjectBase,
);
Expand All @@ -133,7 +131,7 @@ export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
rpcTracePropagationBindings: ['CACHE_DO', 'NO_CACHE_DO'],
}),
{
async fetch(request, env, ctx) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ export const ScopeDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
ScopeDurableObjectBase,
);
Expand All @@ -84,7 +83,7 @@ export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
rpcTracePropagationBindings: ['SCOPE_DO'],
}),
{
async fetch(request, env) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
}),
SyncAlarmDurableObjectBase,
);
Expand All @@ -37,7 +36,7 @@ export default Sentry.withSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
rpcTracePropagationBindings: ['TEST_DURABLE_OBJECT'],
}),
{
async fetch(request: Request, env: Env): Promise<Response> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
}),
AlarmDurableObjectBase,
);
Expand All @@ -35,7 +34,7 @@ export default Sentry.withSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
rpcTracePropagationBindings: ['TEST_DURABLE_OBJECT'],
}),
{
async fetch(request: Request, env: Env): Promise<Response> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
}),
MyDurableObjectBase,
);
Expand All @@ -47,7 +46,7 @@ export default Sentry.withSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
rpcTracePropagationBindings: ['MY_DURABLE_OBJECT'],
}),
{
async fetch(request, env) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
}),
TestDurableObjectBase,
);
Expand All @@ -42,7 +41,7 @@ export default Sentry.withSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
rpcTracePropagationBindings: ['TEST_DURABLE_OBJECT'],
}),
{
async fetch(_request: Request, env: Env): Promise<Response> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
}),
TestDurableObjectBase,
);
Expand All @@ -56,7 +55,7 @@ export default Sentry.withSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
rpcTracePropagationBindings: ['TEST_DURABLE_OBJECT'],
}),
{
async fetch(request: Request, env: Env): Promise<Response> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default withSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
rpcTracePropagationBindings: ['ECHO_HEADERS_DO'],
}),
{
async fetch(request, env) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@ interface Env {
}

class MyDurableObjectBase extends DurableObject<Env> {
async fetch(request: Request): Promise<Response> {
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<string> {
return `Hello, ${name}!`;
}
}

Expand All @@ -37,11 +33,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 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,38 @@ 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;

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<string>('get', '/do/hello');
expect(response).toBe('Hello, World!');
expect(await runner.makeRequest<string>('get', '/rpc/hello')).toBe('Hello, World!');
expect(await runner.makeRequest<string>('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);
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
}),
MyDurableObjectBase,
);
Expand All @@ -31,7 +30,7 @@ export default Sentry.withSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
rpcTracePropagationBindings: ['MY_DURABLE_OBJECT'],
}),
{
async fetch(request, env) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
}),
MyDurableObjectBase,
);
Expand All @@ -28,7 +27,7 @@ export default Sentry.withSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
rpcTracePropagationBindings: ['MY_DURABLE_OBJECT'],
}),
{
async fetch(request, env) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default Sentry.withSentry(
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
rpcTracePropagationBindings: ['ANOTHER_WORKER'],
}),
{
async fetch(request, env) {
Expand Down
Loading
Loading