Skip to content
Merged
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
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,8 +74,8 @@ class ScopeDurableObjectBase extends DurableObject<Env> {
export const ScopeDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
tracesSampleRate: 1,
}),
ScopeDurableObjectBase,
);
Expand All@@ -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) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,8 +25,8 @@ class SyncAlarmDurableObjectBase extends DurableObject<Env> {
export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
tracesSampleRate: 1.0,
}),
SyncAlarmDurableObjectBase,
);
Expand All@@ -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<Response> {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,8 +23,8 @@ class AlarmDurableObjectBase extends DurableObject<Env> {
export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
tracesSampleRate: 1.0,
}),
AlarmDurableObjectBase,
);
Expand All@@ -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<Response> {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
);
Expand All@@ -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) {
Expand Down
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -44,8 +44,8 @@ class TestDurableObjectBase extends DurableObject<Env> {
export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
tracesSampleRate: 1.0,
}),
TestDurableObjectBase,
);
Expand All@@ -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<Response> {
Expand Down
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -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) {
Expand Down
Original file line numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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@@ -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 });
Expand Down
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -19,8 +19,8 @@ class MyDurableObjectBase extends DurableObject<Env> {
export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
tracesSampleRate: 1.0,
}),
MyDurableObjectBase,
);
Expand All@@ -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) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,8 +16,8 @@ class MyDurableObjectBase extends DurableObject<Env> {
export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
tracesSampleRate: 1.0,
}),
MyDurableObjectBase,
);
Expand All@@ -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) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,8 +15,8 @@ class MyDurableObjectBase extends DurableObject<Env> {
export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
tracesSampleRate: 1.0,
}),
MyDurableObjectBase,
);
Expand All@@ -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) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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' };
Expand All@@ -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,
Expand All@@ -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<Env> {
get(key: string): { argumentCount: number; key: string } {
return { argumentCount: arguments.length, key };
}
}

export default BindingEntrypoint;
Loading
Loading