Skip to content

Commit a797bcc

Browse files
committed
Add option to instrument rpc methods
1 parent d231d2a commit a797bcc

File tree

3 files changed

+131
-7
lines changed

3 files changed

+131
-7
lines changed

packages/cloudflare/src/client.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,29 @@ interface BaseCloudflareOptions {
6868
* @default false
6969
*/
7070
skipOpenTelemetrySetup?: boolean;
71+
72+
/**
73+
* Enable instrumentation of prototype methods for DurableObjects.
74+
*
75+
* When `true`, the SDK will wrap all methods on the DurableObject prototype chain
76+
* to automatically create spans and capture errors for RPC method calls.
77+
*
78+
* When an array of strings is provided, only the specified method names will be instrumented.
79+
*
80+
* This feature adds runtime overhead as it wraps methods at the prototype level.
81+
* Only enable this if you need automatic instrumentation of prototype methods.
82+
*
83+
* @default false
84+
* @example
85+
* ```ts
86+
* // Instrument all prototype methods
87+
* instrumentPrototypeMethods: true
88+
*
89+
* // Instrument only specific methods
90+
* instrumentPrototypeMethods: ['myMethod', 'anotherMethod']
91+
* ```
92+
*/
93+
instrumentPrototypeMethods?: boolean | string[];
7194
}
7295

7396
/**

packages/cloudflare/src/durableobject.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,14 +288,16 @@ export function instrumentDurableObjectWithSentry<
288288
configurable: false,
289289
});
290290

291-
instrumentPrototype(target);
291+
if (options?.instrumentPrototypeMethods) {
292+
instrumentPrototype(target, options.instrumentPrototypeMethods);
293+
}
292294

293295
return obj;
294296
},
295297
});
296298
}
297299

298-
function instrumentPrototype<T extends NewableFunction>(target: T): void {
300+
function instrumentPrototype<T extends NewableFunction>(target: T, methodsToInstrument: boolean | string[]): void {
299301
const proto = target.prototype;
300302

301303
// Get all methods from the prototype chain
@@ -311,6 +313,9 @@ function instrumentPrototype<T extends NewableFunction>(target: T): void {
311313
current = Object.getPrototypeOf(current);
312314
}
313315

316+
// Create a set for efficient lookups when methodsToInstrument is an array
317+
const methodsToInstrumentSet = Array.isArray(methodsToInstrument) ? new Set(methodsToInstrument) : null;
318+
314319
// Instrument each method on the prototype
315320
methodNames.forEach(methodName => {
316321
const originalMethod = (proto as Record<string, unknown>)[methodName];
@@ -319,6 +324,11 @@ function instrumentPrototype<T extends NewableFunction>(target: T): void {
319324
return;
320325
}
321326

327+
// If methodsToInstrument is an array, only instrument methods in that set
328+
if (methodsToInstrumentSet && !methodsToInstrumentSet.has(methodName)) {
329+
return;
330+
}
331+
322332
// Create a wrapper that gets context/options from the instance at runtime
323333
const wrappedMethod = function (this: any, ...args: any[]): unknown {
324334
const thisWithSentry = this as {

packages/cloudflare/test/durableobject.test.ts

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe('instrumentDurableObjectWithSentry', () => {
1010
});
1111

1212
it('Generic functionality', () => {
13-
const options = vi.fn();
13+
const options = vi.fn().mockReturnValue({});
1414
const instrumented = instrumentDurableObjectWithSentry(options, vi.fn());
1515
expect(instrumented).toBeTypeOf('function');
1616
expect(() => Reflect.construct(instrumented, [])).not.toThrow();
@@ -23,7 +23,7 @@ describe('instrumentDurableObjectWithSentry', () => {
2323
return 'sync-result';
2424
}
2525
};
26-
const obj = Reflect.construct(instrumentDurableObjectWithSentry(vi.fn(), testClass as any), []) as any;
26+
const obj = Reflect.construct(instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any), []) as any;
2727
expect(obj.method).toBe(obj.method);
2828

2929
const result = obj.method();
@@ -37,7 +37,7 @@ describe('instrumentDurableObjectWithSentry', () => {
3737
return 'async-result';
3838
}
3939
};
40-
const obj = Reflect.construct(instrumentDurableObjectWithSentry(vi.fn(), testClass as any), []) as any;
40+
const obj = Reflect.construct(instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), testClass as any), []) as any;
4141
expect(obj.asyncMethod).toBe(obj.asyncMethod);
4242

4343
const result = obj.asyncMethod();
@@ -56,9 +56,11 @@ describe('instrumentDurableObjectWithSentry', () => {
5656
.fn()
5757
.mockReturnValueOnce({
5858
orgId: 1,
59+
instrumentPrototypeMethods: true,
5960
})
6061
.mockReturnValueOnce({
6162
orgId: 2,
63+
instrumentPrototypeMethods: true,
6264
});
6365
const testClass = class {
6466
method() {}
@@ -79,7 +81,7 @@ describe('instrumentDurableObjectWithSentry', () => {
7981
expect(initCore).nthCalledWith(2, expect.any(Function), expect.objectContaining({ orgId: 2 }));
8082
});
8183

82-
it('All available durable object methods are instrumented', () => {
84+
it('All available durable object methods are instrumented when instrumentPrototypeMethods is enabled', () => {
8385
const testClass = class {
8486
propertyFunction = vi.fn();
8587

@@ -95,7 +97,7 @@ describe('instrumentDurableObjectWithSentry', () => {
9597

9698
webSocketError() {}
9799
};
98-
const instrumented = instrumentDurableObjectWithSentry(vi.fn(), testClass as any);
100+
const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({ instrumentPrototypeMethods: true }), testClass as any);
99101
const obj = Reflect.construct(instrumented, []);
100102
for (const method_name of [
101103
'propertyFunction',
@@ -135,4 +137,93 @@ describe('instrumentDurableObjectWithSentry', () => {
135137
await Promise.all(waitUntil.mock.calls.map(([p]) => p));
136138
expect(flush).toBeCalled();
137139
});
140+
141+
describe('instrumentPrototypeMethods option', () => {
142+
it('does not instrument prototype methods when option is not set', () => {
143+
const testClass = class {
144+
prototypeMethod() {
145+
return 'prototype-result';
146+
}
147+
};
148+
const options = vi.fn().mockReturnValue({});
149+
const instrumented = instrumentDurableObjectWithSentry(options, testClass as any);
150+
const obj = Reflect.construct(instrumented, []) as any;
151+
152+
expect(isInstrumented(obj.prototypeMethod)).toBeFalsy();
153+
});
154+
155+
it('does not instrument prototype methods when option is false', () => {
156+
const testClass = class {
157+
prototypeMethod() {
158+
return 'prototype-result';
159+
}
160+
};
161+
const options = vi.fn().mockReturnValue({ instrumentPrototypeMethods: false });
162+
const instrumented = instrumentDurableObjectWithSentry(options, testClass as any);
163+
const obj = Reflect.construct(instrumented, []) as any;
164+
165+
expect(isInstrumented(obj.prototypeMethod)).toBeFalsy();
166+
});
167+
168+
it('instruments all prototype methods when option is true', () => {
169+
const testClass = class {
170+
methodOne() {
171+
return 'one';
172+
}
173+
methodTwo() {
174+
return 'two';
175+
}
176+
};
177+
const options = vi.fn().mockReturnValue({ instrumentPrototypeMethods: true });
178+
const instrumented = instrumentDurableObjectWithSentry(options, testClass as any);
179+
const obj = Reflect.construct(instrumented, []) as any;
180+
181+
expect(isInstrumented(obj.methodOne)).toBeTruthy();
182+
expect(isInstrumented(obj.methodTwo)).toBeTruthy();
183+
});
184+
185+
it('instruments only specified methods when option is array', () => {
186+
const testClass = class {
187+
methodOne() {
188+
return 'one';
189+
}
190+
methodTwo() {
191+
return 'two';
192+
}
193+
methodThree() {
194+
return 'three';
195+
}
196+
};
197+
const options = vi.fn().mockReturnValue({ instrumentPrototypeMethods: ['methodOne', 'methodThree'] });
198+
const instrumented = instrumentDurableObjectWithSentry(options, testClass as any);
199+
const obj = Reflect.construct(instrumented, []) as any;
200+
201+
expect(isInstrumented(obj.methodOne)).toBeTruthy();
202+
expect(isInstrumented(obj.methodTwo)).toBeFalsy();
203+
expect(isInstrumented(obj.methodThree)).toBeTruthy();
204+
});
205+
206+
it('still instruments instance methods regardless of prototype option', () => {
207+
const testClass = class {
208+
propertyFunction = vi.fn();
209+
210+
fetch() {}
211+
alarm() {}
212+
webSocketMessage() {}
213+
webSocketClose() {}
214+
webSocketError() {}
215+
};
216+
const options = vi.fn().mockReturnValue({ instrumentPrototypeMethods: false });
217+
const instrumented = instrumentDurableObjectWithSentry(options, testClass as any);
218+
const obj = Reflect.construct(instrumented, []) as any;
219+
220+
// Instance methods should still be instrumented
221+
expect(isInstrumented(obj.propertyFunction)).toBeTruthy();
222+
expect(isInstrumented(obj.fetch)).toBeTruthy();
223+
expect(isInstrumented(obj.alarm)).toBeTruthy();
224+
expect(isInstrumented(obj.webSocketMessage)).toBeTruthy();
225+
expect(isInstrumented(obj.webSocketClose)).toBeTruthy();
226+
expect(isInstrumented(obj.webSocketError)).toBeTruthy();
227+
});
228+
});
138229
});

0 commit comments

Comments
 (0)