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
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,50 @@ interface Env {
TEST_DURABLE_OBJECT: DurableObjectNamespace;
}

// Regression test for https://github.com/getsentry/sentry-javascript/issues/17127
// This class mimics a real-world DO with private fields/methods and multiple public methods
class TestDurableObjectBase extends DurableObject<Env> {
// Real private field for internal state (not accessed by RPC methods due to proxy limitations)
#requestCount = 0;

public constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
}

// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility
// Real private method for internal use
#incrementCount(): void {
this.#requestCount++;
}

// Internal method that uses private fields (called from non-RPC context like alarm/fetch)
getRequestCount(): number {
return this.#requestCount;
}

// The method being called in tests via RPC
async sayHello(name: string): Promise<string> {
return `Hello, ${name}`;
}

// Other public methods that are not called - should not interfere with RPC
async getStatus(): Promise<string> {
return 'OK';
}

async processData(data: Record<string, unknown>): Promise<Record<string, unknown>> {
return { ...data, processed: true };
}

async multiply(a: number, b: number): Promise<number> {
return a * b;
}
}

export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
instrumentPrototypeMethods: true,
enableRpcTracePropagation: true,
}),
TestDurableObjectBase,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,40 @@ it('traces a durable object method', async ({ signal }) => {
await runner.makeRequest('get', '/hello');
await runner.completed();
});

// Regression test for https://github.com/getsentry/sentry-javascript/issues/17127
// The RPC receiver does not implement the method error on consecutive calls
it('handles consecutive RPC calls without throwing "RPC receiver does not implement method" error', async ({
signal,
}) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1];
expect(transactionEvent).toEqual(
expect.objectContaining({
transaction: 'sayHello',
}),
);
})
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1];
expect(transactionEvent).toEqual(
expect.objectContaining({
transaction: 'sayHello',
}),
);
})
.unordered()
.start(signal);

// First request - this always worked
const response1 = await runner.makeRequest<string>('get', '/hello');
expect(response1).toBe('Hello, world');

// Second consecutive request - this used to fail with:
// "The RPC receiver does not implement the method 'sayHello'"
const response2 = await runner.makeRequest<string>('get', '/hello');
expect(response2).toBe('Hello, world');

await runner.completed();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from 'cloudflare:workers';
import type { RpcTarget } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
MY_DURABLE_OBJECT: DurableObjectNamespace<MyDurableObjectBase>;
}

class MyDurableObjectBase extends DurableObject<Env> implements RpcTarget {
async sayHello(name: string): Promise<string> {
return `Hello, ${name}!`;
}
}

// enableRpcTracePropagation is NOT enabled, so RPC methods won't be instrumented
export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
// enableRpcTracePropagation: false (default)
}),
MyDurableObjectBase,
);

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
}),
{
async fetch(request, env) {
const url = new URL(request.url);
const id = env.MY_DURABLE_OBJECT.idFromName('test');
const stub = env.MY_DURABLE_OBJECT.get(id);

if (url.pathname === '/rpc/hello') {
const result = await stub.sayHello('World');
return new Response(result);
}

return new Response('Not found', { status: 404 });
},
} satisfies ExportedHandler<Env>,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { expect, it } from 'vitest';
import type { Event } from '@sentry/core';
import { createRunner } from '../../../../runner';

it('does not create RPC transaction when enableRpcTracePropagation is disabled', async ({ signal }) => {
let receivedTransactions: string[] = [];

const runner = createRunner(__dirname)
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1] as Event;

// Should only receive the worker HTTP transaction, not the DO RPC transaction
expect(transactionEvent).toEqual(
expect.objectContaining({
contexts: expect.objectContaining({
trace: expect.objectContaining({
op: 'http.server',
data: expect.objectContaining({
'sentry.origin': 'auto.http.cloudflare',
}),
origin: 'auto.http.cloudflare',
}),
}),
transaction: 'GET /rpc/hello',
}),
);
receivedTransactions.push(transactionEvent.transaction as string);
})
.start(signal);

// The RPC call should still work, just not be instrumented
const response = await runner.makeRequest<string>('get', '/rpc/hello');
expect(response).toBe('Hello, World!');

await runner.completed();

// Verify we only got the worker transaction, no RPC transaction
expect(receivedTransactions).toEqual(['GET /rpc/hello']);
expect(receivedTransactions).not.toContain('sayHello');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "cloudflare-worker-do-rpc-disabled",
"main": "index.ts",
"compatibility_date": "2025-06-17",
"compatibility_flags": ["nodejs_als"],
"migrations": [
{
"new_sqlite_classes": ["MyDurableObject"],
"tag": "v1",
},
],
"durable_objects": {
"bindings": [
{
"class_name": "MyDurableObject",
"name": "MY_DURABLE_OBJECT",
},
],
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from 'cloudflare:workers';
import type { RpcTarget } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
MY_DURABLE_OBJECT: DurableObjectNamespace<MyDurableObjectBase>;
}

class MyDurableObjectBase extends DurableObject<Env> implements RpcTarget {
async sayHello(name: string): Promise<string> {
return `Hello, ${name}!`;
}

async multiply(a: number, b: number): Promise<number> {
return a * b;
}
}

export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
}),
MyDurableObjectBase,
);

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
}),
{
async fetch(request, env) {
const url = new URL(request.url);
const id = env.MY_DURABLE_OBJECT.idFromName('test');
const stub = env.MY_DURABLE_OBJECT.get(id);

if (url.pathname === '/rpc/hello') {
const result = await stub.sayHello('World');
return new Response(result);
}

if (url.pathname === '/rpc/multiply') {
const result = await stub.multiply(6, 7);
return new Response(String(result));
}

return new Response('Not found', { status: 404 });
},
} satisfies ExportedHandler<Env>,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { expect, it } from 'vitest';
import type { Event } from '@sentry/core';
import { createRunner } from '../../../../runner';

it('propagates trace from worker to durable object via RPC method call', async ({ signal }) => {
let workerTraceId: string | undefined;
let workerSpanId: string | undefined;
let doTraceId: string | undefined;
let doParentSpanId: string | undefined;

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: 'rpc',
data: expect.objectContaining({
'sentry.origin': 'auto.faas.cloudflare.durable_object',
}),
origin: 'auto.faas.cloudflare.durable_object',
}),
}),
transaction: 'sayHello',
}),
);
doTraceId = transactionEvent.contexts?.trace?.trace_id as string;
doParentSpanId = transactionEvent.contexts?.trace?.parent_span_id as string;
})
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1] as Event;

expect(transactionEvent).toEqual(
expect.objectContaining({
contexts: expect.objectContaining({
trace: expect.objectContaining({
op: 'http.server',
data: expect.objectContaining({
'sentry.origin': 'auto.http.cloudflare',
}),
origin: 'auto.http.cloudflare',
}),
}),
transaction: 'GET /rpc/hello',
}),
);
workerTraceId = transactionEvent.contexts?.trace?.trace_id as string;
workerSpanId = transactionEvent.contexts?.trace?.span_id as string;
})
.unordered()
.start(signal);

const response = await runner.makeRequest<string>('get', '/rpc/hello');
expect(response).toBe('Hello, World!');

await runner.completed();

expect(workerTraceId).toBeDefined();
expect(doTraceId).toBeDefined();
expect(workerTraceId).toBe(doTraceId);

expect(workerSpanId).toBeDefined();
expect(doParentSpanId).toBeDefined();
expect(doParentSpanId).toBe(workerSpanId);
});

it('propagates trace for RPC method with multiple arguments', async ({ signal }) => {
let workerTraceId: string | undefined;
let workerSpanId: string | undefined;
let doTraceId: string | undefined;
let doParentSpanId: string | undefined;

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: 'rpc',
}),
}),
transaction: 'multiply',
}),
);
doTraceId = transactionEvent.contexts?.trace?.trace_id as string;
doParentSpanId = transactionEvent.contexts?.trace?.parent_span_id as string;
})
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1] as Event;

expect(transactionEvent).toEqual(
expect.objectContaining({
contexts: expect.objectContaining({
trace: expect.objectContaining({
op: 'http.server',
}),
}),
transaction: 'GET /rpc/multiply',
}),
);
workerTraceId = transactionEvent.contexts?.trace?.trace_id as string;
workerSpanId = transactionEvent.contexts?.trace?.span_id as string;
})
.unordered()
.start(signal);

const response = await runner.makeRequest<string>('get', '/rpc/multiply');
expect(response).toBe('42');

await runner.completed();

expect(workerTraceId).toBeDefined();
expect(doTraceId).toBeDefined();
expect(workerTraceId).toBe(doTraceId);

expect(workerSpanId).toBeDefined();
expect(doParentSpanId).toBeDefined();
expect(doParentSpanId).toBe(workerSpanId);
});
Loading
Loading