Skip to content

Commit e18ab56

Browse files
committed
Instrument methods at prototype level not instance level
1 parent 6cf7f53 commit e18ab56

File tree

2 files changed

+85
-38
lines changed

2 files changed

+85
-38
lines changed

packages/cloudflare/src/durableobject.ts

Lines changed: 74 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,7 @@ function wrapMethodWithSentry<T extends OriginalMethod>(
110110
}
111111
: {};
112112

113-
// Only create these spans if they have a parent span.
114-
return startSpan({ name: wrapperOptions.spanName, attributes, onlyIfParent: true }, () => {
113+
return startSpan({ name: wrapperOptions.spanName, attributes }, () => {
115114
try {
116115
const result = Reflect.apply(target, thisArg, args);
117116

@@ -273,46 +272,87 @@ export function instrumentDurableObjectWithSentry<
273272
);
274273
}
275274
}
276-
const instrumentedPrototype = instrumentPrototype(target, options, context);
277-
Object.setPrototypeOf(obj, instrumentedPrototype);
275+
276+
// Store context and options on the instance for prototype methods to access
277+
Object.defineProperty(obj, '__SENTRY_CONTEXT__', {
278+
value: context,
279+
enumerable: false,
280+
writable: false,
281+
configurable: false,
282+
});
283+
284+
Object.defineProperty(obj, '__SENTRY_OPTIONS__', {
285+
value: options,
286+
enumerable: false,
287+
writable: false,
288+
configurable: false,
289+
});
290+
291+
instrumentPrototype(target);
278292

279293
return obj;
280294
},
281295
});
282296
}
283297

284-
function instrumentPrototype<T extends NewableFunction>(
285-
target: T,
286-
options: CloudflareOptions,
287-
context: MethodWrapperOptions['context'],
288-
): T {
289-
return new Proxy(target.prototype, {
290-
get(target, prop, receiver) {
291-
const value = Reflect.get(target, prop, receiver);
292-
if (prop === 'constructor' || typeof value !== 'function') {
293-
return value;
298+
function instrumentPrototype<T extends NewableFunction>(target: T): void {
299+
const proto = target.prototype;
300+
301+
// Get all methods from the prototype chain
302+
const methodNames = new Set<string>();
303+
let current = proto;
304+
305+
while (current && current !== Object.prototype) {
306+
Object.getOwnPropertyNames(current).forEach(name => {
307+
if (name !== 'constructor' && typeof current[name] === 'function') {
308+
methodNames.add(name);
309+
}
310+
});
311+
current = Object.getPrototypeOf(current);
312+
}
313+
314+
// Instrument each method on the prototype
315+
methodNames.forEach(methodName => {
316+
const originalMethod = proto[methodName];
317+
318+
if (!originalMethod || isInstrumented(originalMethod)) {
319+
return;
320+
}
321+
322+
// Create a wrapper that gets context/options from the instance at runtime
323+
const wrappedMethod = function (this: any, ...args: any[]) {
324+
const instanceContext = this.__SENTRY_CONTEXT__;
325+
const instanceOptions = this.__SENTRY_OPTIONS__;
326+
327+
if (!instanceOptions) {
328+
// Fallback to original method if no Sentry data found
329+
return originalMethod.apply(this, args);
294330
}
295-
const wrapped = wrapMethodWithSentry(
296-
{ options, context, spanName: prop.toString(), spanOp: 'rpc' },
297-
value,
331+
332+
// Use the existing wrapper but with instance-specific context/options
333+
const wrapper = wrapMethodWithSentry(
334+
{
335+
options: instanceOptions,
336+
context: instanceContext,
337+
spanName: methodName,
338+
spanOp: 'rpc',
339+
},
340+
originalMethod,
298341
undefined,
299-
true,
342+
true, // noMark = true since we'll mark the prototype method
300343
);
301-
const instrumented = new Proxy(wrapped, {
302-
get(target, p, receiver) {
303-
if ('__SENTRY_INSTRUMENTED__' === p) {
304-
return true;
305-
}
306-
return Reflect.get(target, p, receiver);
307-
},
308-
});
309-
Object.defineProperty(receiver, prop, {
310-
value: instrumented,
311-
enumerable: true,
312-
writable: true,
313-
configurable: true,
314-
});
315-
return instrumented;
316-
},
344+
345+
return wrapper.apply(this, args);
346+
};
347+
348+
markAsInstrumented(wrappedMethod);
349+
350+
// Replace the prototype method
351+
Object.defineProperty(proto, methodName, {
352+
value: wrappedMethod,
353+
enumerable: false,
354+
writable: true,
355+
configurable: true,
356+
});
317357
});
318358
}

packages/cloudflare/test/durableobject.test.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@ describe('instrumentDurableObjectWithSentry', () => {
4646
});
4747

4848
it('Instruments prototype methods without "sticking" to the options', () => {
49+
const mockContext = {
50+
waitUntil: vi.fn(),
51+
} as any;
52+
const mockEnv = {} as any; // Environment mock
4953
const initCore = vi.spyOn(SentryCore, 'initAndBind');
50-
vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined);
54+
const getClientSpy = vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined);
5155
const options = vi
5256
.fn()
5357
.mockReturnValueOnce({
@@ -59,8 +63,12 @@ describe('instrumentDurableObjectWithSentry', () => {
5963
const testClass = class {
6064
method() {}
6165
};
62-
(Reflect.construct(instrumentDurableObjectWithSentry(options, testClass as any), []) as any).method();
63-
(Reflect.construct(instrumentDurableObjectWithSentry(options, testClass as any), []) as any).method();
66+
const instance1 = Reflect.construct(instrumentDurableObjectWithSentry(options, testClass as any), [mockContext, mockEnv]) as any;
67+
instance1.method();
68+
69+
const instance2 = Reflect.construct(instrumentDurableObjectWithSentry(options, testClass as any), [mockContext, mockEnv]) as any;
70+
instance2.method();
71+
6472
expect(initCore).nthCalledWith(1, expect.any(Function), expect.objectContaining({ orgId: 1 }));
6573
expect(initCore).nthCalledWith(2, expect.any(Function), expect.objectContaining({ orgId: 2 }));
6674
});
@@ -83,7 +91,6 @@ describe('instrumentDurableObjectWithSentry', () => {
8391
};
8492
const instrumented = instrumentDurableObjectWithSentry(vi.fn(), testClass as any);
8593
const obj = Reflect.construct(instrumented, []);
86-
expect(Object.getPrototypeOf(obj), 'Prototype is instrumented').not.toBe(testClass.prototype);
8794
for (const method_name of [
8895
'propertyFunction',
8996
'fetch',

0 commit comments

Comments
 (0)