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
@@ -0,0 +1,44 @@
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
SELF: Fetcher;
CONNECT_DO: DurableObjectNamespace<ConnectDurableObject>;
}

export class ConnectDurableObject extends DurableObject<Env> {}

function tryConnect(connect: () => Socket): string {
try {
const socket = connect();
socket.opened.catch(() => {});
socket.closed.catch(() => {});
return 'ok';
} catch (error) {
return (error as Error).message;
}
}

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
}),
{
async fetch(request, env) {
const url = new URL(request.url);

if (url.pathname === '/connect') {
const stub = env.CONNECT_DO.get(env.CONNECT_DO.idFromName('connect'));

return Response.json({
durableObject: tryConnect(() => stub.connect('127.0.0.1:9')),
service: tryConnect(() => env.SELF.connect('127.0.0.1:9')),
});
}

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

it('calls connect() on Durable Object stubs and service bindings with the binding as `this`', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect((envelope: Envelope) => {
const spanItem = envelope[1].find(item => item[0].type === 'span');
expect(spanItem).toBeDefined();
const segmentSpan = (spanItem![1] as SerializedStreamedSpanContainer).items.find(span => !!span.is_segment);
expect(segmentSpan).toMatchObject({
name: 'GET',
status: 'ok',
attributes: expect.objectContaining({ 'url.path': { type: 'string', value: '/connect' } }),
});
})
.start(signal);

const result = await runner.makeRequest<{ durableObject: string; service: string }>('get', '/connect');

expect(result?.durableObject).toBe('ok');
// Local workerd rejects CONNECT on a Worker after the `this` check, so only that check is asserted here.
expect(result?.service).not.toMatch(/Illegal invocation/);
await runner.completed();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "binding-connect-worker",
"main": "index.ts",
"compatibility_date": "2025-06-17",
"compatibility_flags": ["nodejs_compat"],
"migrations": [
{
"new_sqlite_classes": ["ConnectDurableObject"],
"tag": "v1",
},
],
"durable_objects": {
"bindings": [
{
"class_name": "ConnectDurableObject",
"name": "CONNECT_DO",
},
],
},
"services": [
{
"binding": "SELF",
"service": "binding-connect-worker",
},
],
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ function instrumentDurableObjectStub(stub: DurableObjectStub, propagateRpcTrace:
return instrumentFetcher((...args) => Reflect.apply(value, target, args));
}

if (prop === 'connect' && typeof value === 'function') {
return (...args: unknown[]) => Reflect.apply(value, target, args);
}

if (
propagateRpcTrace &&
typeof value === 'function' &&
Comment thread
JPeer264 marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ export function instrumentEnv<Env extends Record<string, unknown>>(env: Env, opt
return instrumentFetcher((...args) => Reflect.apply(value, target, args));
}

if (p === 'connect' && typeof value === 'function') {
return (...args: unknown[]) => Reflect.apply(value, target, args);
}

if (
propagateRpcTrace &&
typeof value === 'function' &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,11 +323,31 @@ describe('instrumentDurableObjectNamespace', () => {
const instrumented = instrumentDurableObjectNamespace(namespace, true);

const stub = instrumented.get({ toString: () => 'id', equals: () => false } as any);
(stub as any).connect('127.0.0.1:9');

// connect and dup should be the original functions, not wrapped
expect((stub as any).connect).toBe(connectFn);
expect(connectFn).toHaveBeenCalledWith('127.0.0.1:9');
expect((stub as any).dup).toBe(dupFn);
});

it('calls connect with the underlying stub as `this`', () => {
const { namespace: originalNamespace } = createMockNamespace();
const rawStub = {
id: { toString: () => 'mock-id', equals: () => false, name: 'test' },
fetch: vi.fn(),
connect(this: unknown) {
if (this !== rawStub) {
throw new TypeError('Illegal invocation: function called with incorrect `this` reference.');
}
return 'socket';
},
};
const namespace = { ...originalNamespace, get: vi.fn().mockReturnValue(rawStub) };
const instrumented = instrumentDurableObjectNamespace(namespace);

const stub = instrumented.get({ toString: () => 'id', equals: () => false } as any);

expect((stub as any).connect('127.0.0.1:9')).toBe('socket');
});
});

describe('non-function properties', () => {
Expand Down
24 changes: 24 additions & 0 deletions packages/cloudflare/test/instrumentations/instrumentEnv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,30 @@ describe('instrumentEnv', () => {
});
});

it('calls JSRPC connect with the underlying binding as `this`', () => {
const jsrpcTarget = {
fetch: vi.fn(),
connect(this: unknown, _address: string) {
if (this !== jsrpcProxy) {
throw new TypeError('Illegal invocation: function called with incorrect `this` reference.');
}
return 'socket';
},
};
const jsrpcProxy = new Proxy(jsrpcTarget, {
get(target, prop) {
if (prop in target) {
return Reflect.get(target, prop);
}
return () => {};
},
});
const env = { SERVICE: jsrpcProxy };
const instrumented = instrumentEnv(env, { rpcTracePropagationBindings: [/.*/] });

expect(instrumented.SERVICE.connect('127.0.0.1:9')).toBe('socket');
});

it('does not inject meta into JSRPC fetch calls', () => {
vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({
'sentry-trace': 'abc-def-1',
Expand Down
Loading