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 number Diff line number Diff line change
Expand Up @@ -6,22 +6,46 @@ 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> {
// Private field used by RPC methods - tests that private fields work with instrumentation
#greeting = 'Hello';

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

// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility
// RPC method that uses a private field - this would throw TypeError if the Proxy
// doesn't correctly bind `this` to the original object
async sayHello(name: string): Promise<string> {
return `Hello, ${name}`;
return `${this.#greeting}, ${name}`;
}

// RPC method that modifies a private field
async setGreeting(greeting: string): Promise<void> {
this.#greeting = greeting;
}

// 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 All @@ -36,6 +60,13 @@ export default {
return new Response(greeting);
}

// Test endpoint that modifies and reads a private field via RPC
if (request.url.includes('custom-greeting')) {
await stub.setGreeting('Howdy');
const greeting = await stub.sayHello('partner');
return new Response(greeting);
}

return new Response('Usual response');
},
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, it } from 'vitest';
import type { Event } from '@sentry/core';
import { createRunner } from '../../../runner';

it('traces a durable object method', async ({ signal }) => {
Expand All @@ -25,3 +26,74 @@ 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();
});

// Regression test: RPC methods that access private fields should work correctly.
// When enableRpcTracePropagation 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"
it('allows RPC methods to access private class fields', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1] as Event;
expect(transactionEvent).toEqual(
expect.objectContaining({
transaction: 'setGreeting',
}),
);
})
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1] as Event;
expect(transactionEvent).toEqual(
expect.objectContaining({
transaction: 'sayHello',
}),
);
})
.unordered()
.start(signal);

// This calls setGreeting (writes private field) then sayHello (reads private field)
// Would throw TypeError if `this` is the Proxy instead of the original object
const response = await runner.makeRequest<string>('get', '/custom-greeting');
expect(response).toBe('Howdy, partner');

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>,
);
Loading
Loading