Skip to content

Commit 3877eb7

Browse files
committed
core: add wrapMethod util
1 parent b93ef56 commit 3877eb7

File tree

2 files changed

+77
-0
lines changed

2 files changed

+77
-0
lines changed

packages/core/src/utils/object.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { isElement, isError, isEvent, isInstanceOf, isPrimitive } from './is';
1818
* args>)` or `origMethod.apply(this, [<other args>])` (rather than being called directly), again to preserve `this`.
1919
* @returns void
2020
*/
21+
2122
export function fill(source: { [key: string]: any }, name: string, replacementFactory: (...args: any[]) => any): void {
2223
if (!(name in source)) {
2324
return;
@@ -80,6 +81,37 @@ export function markFunctionWrapped(wrapped: WrappedFunction, original: WrappedF
8081
} catch {} // eslint-disable-line no-empty
8182
}
8283

84+
/**
85+
* Wrap a method on an object by name, only if it is not already wrapped.
86+
*
87+
* Note: to set the wrapped method as a non-enumerable property, pass
88+
* false as the `enumerable` argument. This could be detected, but only
89+
* by either walking up the prototype chain, or iterating over all fields
90+
* in a `for(in)` loop. Neither are an acceptable performance impact for
91+
* the rare case where we might patch a non-enumerable method.
92+
*/
93+
export function wrapMethod<O extends {}, T extends string & keyof O>(
94+
obj: O,
95+
field: T,
96+
wrapped: WrappedFunction,
97+
enumerable: boolean = true,
98+
): void {
99+
const original = obj[field];
100+
if (typeof original !== 'function') {
101+
throw new Error(`Cannot wrap method: ${field} is not a function`);
102+
}
103+
if (getOriginalFunction(original)) {
104+
throw new Error(`Attempting to wrap method ${field} multiple times`);
105+
}
106+
markFunctionWrapped(wrapped, original);
107+
Object.defineProperty(obj, field, {
108+
writable: true,
109+
configurable: true,
110+
enumerable,
111+
value: wrapped,
112+
});
113+
}
114+
83115
/**
84116
* This extracts the original function if available. See
85117
* `markFunctionWrapped` for more information.

packages/core/test/lib/utils/object.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
fill,
1212
markFunctionWrapped,
1313
objectify,
14+
wrapMethod,
1415
} from '../../../src/utils/object';
1516
import { testOnlyIfNodeVersionAtLeast } from '../../testutils';
1617

@@ -455,3 +456,47 @@ describe('markFunctionWrapped', () => {
455456
expect(originalFunc).not.toHaveBeenCalled();
456457
});
457458
});
459+
460+
describe('wrapMethod', () => {
461+
it('can wrap a method on an object', () => {
462+
const wrappedEnumerable = () => {};
463+
const originalEnumerable = () => {};
464+
const wrappedNotEnumerable = () => {};
465+
const originalNotEnumerable = () => {};
466+
const obj: Record<string, unknown> = {
467+
enumerable: originalEnumerable,
468+
};
469+
Object.defineProperty(obj, 'notEnumerable', {
470+
writable: true,
471+
configurable: true,
472+
enumerable: false,
473+
value: originalNotEnumerable,
474+
});
475+
wrapMethod(obj, 'notEnumerable', wrappedNotEnumerable, false);
476+
wrapMethod(obj, 'enumerable', wrappedEnumerable);
477+
// does not change enumerability
478+
expect(Object.keys(obj)).toStrictEqual(['enumerable']);
479+
expect(obj.notEnumerable).toBe(wrappedNotEnumerable);
480+
expect((obj.notEnumerable as WrappedFunction).__sentry_original__).toBe(originalNotEnumerable);
481+
expect(obj.enumerable).toBe(wrappedEnumerable);
482+
expect((obj.enumerable as WrappedFunction).__sentry_original__).toBe(originalEnumerable);
483+
});
484+
485+
it('throws if misused', () => {
486+
const wrapped = () => {};
487+
const original = () => {};
488+
const obj = {
489+
get m() {
490+
return original;
491+
},
492+
};
493+
wrapMethod(obj, 'm', wrapped);
494+
expect(() => {
495+
//@ts-expect-error verify type checking prevents this mistake
496+
wrapMethod(obj, 'foo', wrapped);
497+
}).toThrowError('Cannot wrap method: foo is not a function');
498+
expect(() => {
499+
wrapMethod(obj, 'm', wrapped);
500+
}).toThrowError('Attempting to wrap method m multiple times');
501+
});
502+
});

0 commit comments

Comments
 (0)