Skip to content

Commit 940faf8

Browse files
authored
feat(browser): simplify wrap/fill (#4286)
1 parent fb87d34 commit 940faf8

File tree

8 files changed

+82
-87
lines changed

8 files changed

+82
-87
lines changed

packages/browser/src/helpers.ts

+32-40
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { captureException, getReportDialogEndpoint, withScope } from '@sentry/core';
22
import { DsnLike, Event as SentryEvent, Mechanism, Scope, WrappedFunction } from '@sentry/types';
3-
import { addExceptionMechanism, addExceptionTypeValue, getGlobalObject, logger } from '@sentry/utils';
3+
import {
4+
addExceptionMechanism,
5+
addExceptionTypeValue,
6+
addNonEnumerableProperty,
7+
getGlobalObject,
8+
getOriginalFunction,
9+
logger,
10+
markFunctionWrapped,
11+
} from '@sentry/utils';
412

513
const global = getGlobalObject<Window>();
614
let ignoreOnError: number = 0;
@@ -39,19 +47,28 @@ export function wrap(
3947
before?: WrappedFunction,
4048
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4149
): any {
50+
// for future readers what this does is wrap a function and then create
51+
// a bi-directional wrapping between them.
52+
//
53+
// example: wrapped = wrap(original);
54+
// original.__sentry_wrapped__ -> wrapped
55+
// wrapped.__sentry_original__ -> original
56+
4257
if (typeof fn !== 'function') {
4358
return fn;
4459
}
4560

4661
try {
47-
// We don't wanna wrap it twice
48-
if (fn.__sentry__) {
49-
return fn;
62+
// if we're dealing with a function that was previously wrapped, return
63+
// the original wrapper.
64+
const wrapper = fn.__sentry_wrapped__;
65+
if (wrapper) {
66+
return wrapper;
5067
}
5168

52-
// If this has already been wrapped in the past, return that wrapped function
53-
if (fn.__sentry_wrapped__) {
54-
return fn.__sentry_wrapped__;
69+
// We don't wanna wrap it twice
70+
if (getOriginalFunction(fn)) {
71+
return fn;
5572
}
5673
} catch (e) {
5774
// Just accessing custom props in some Selenium environments
@@ -73,14 +90,6 @@ export function wrap(
7390
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
7491
const wrappedArguments = args.map((arg: any) => wrap(arg, options));
7592

76-
if (fn.handleEvent) {
77-
// Attempt to invoke user-land function
78-
// NOTE: If you are a Sentry user, and you are seeing this stack frame, it
79-
// means the sentry.javascript SDK caught an error invoking your application code. This
80-
// is expected behavior and NOT indicative of a bug with sentry.javascript.
81-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
82-
return fn.handleEvent.apply(this, wrappedArguments);
83-
}
8493
// Attempt to invoke user-land function
8594
// NOTE: If you are a Sentry user, and you are seeing this stack frame, it
8695
// means the sentry.javascript SDK caught an error invoking your application code. This
@@ -91,19 +100,17 @@ export function wrap(
91100

92101
withScope((scope: Scope) => {
93102
scope.addEventProcessor((event: SentryEvent) => {
94-
const processedEvent = { ...event };
95-
96103
if (options.mechanism) {
97-
addExceptionTypeValue(processedEvent, undefined, undefined);
98-
addExceptionMechanism(processedEvent, options.mechanism);
104+
addExceptionTypeValue(event, undefined, undefined);
105+
addExceptionMechanism(event, options.mechanism);
99106
}
100107

101-
processedEvent.extra = {
102-
...processedEvent.extra,
108+
event.extra = {
109+
...event.extra,
103110
arguments: args,
104111
};
105112

106-
return processedEvent;
113+
return event;
107114
});
108115

109116
captureException(ex);
@@ -124,26 +131,11 @@ export function wrap(
124131
}
125132
} catch (_oO) {} // eslint-disable-line no-empty
126133

127-
fn.prototype = fn.prototype || {};
128-
sentryWrapped.prototype = fn.prototype;
129-
130-
Object.defineProperty(fn, '__sentry_wrapped__', {
131-
enumerable: false,
132-
value: sentryWrapped,
133-
});
134-
135134
// Signal that this function has been wrapped/filled already
136135
// for both debugging and to prevent it to being wrapped/filled twice
137-
Object.defineProperties(sentryWrapped, {
138-
__sentry__: {
139-
enumerable: false,
140-
value: true,
141-
},
142-
__sentry_original__: {
143-
enumerable: false,
144-
value: fn,
145-
},
146-
});
136+
markFunctionWrapped(sentryWrapped, fn);
137+
138+
addNonEnumerableProperty(fn, '__sentry_wrapped__', sentryWrapped);
147139

148140
// Restore original function name (not all browsers allow that)
149141
try {

packages/browser/src/integrations/trycatch.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Integration, WrappedFunction } from '@sentry/types';
2-
import { fill, getFunctionName, getGlobalObject } from '@sentry/utils';
2+
import { fill, getFunctionName, getGlobalObject, getOriginalFunction } from '@sentry/utils';
33

44
import { wrap } from '../helpers';
55

@@ -168,8 +168,9 @@ function _wrapXHR(originalSend: () => void): () => void {
168168
};
169169

170170
// If Instrument integration has been called before TryCatch, get the name of original function
171-
if (original.__sentry_original__) {
172-
wrapOptions.mechanism.data.handler = getFunctionName(original.__sentry_original__);
171+
const originalFunction = getOriginalFunction(original);
172+
if (originalFunction) {
173+
wrapOptions.mechanism.data.handler = getFunctionName(originalFunction);
173174
}
174175

175176
// Otherwise wrap directly

packages/browser/test/unit/integrations/helpers.test.ts

+1-30
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,13 @@ describe('internal wrap()', () => {
3131

3232
it('bail out with the original if accessing custom props go bad', () => {
3333
const fn = (() => 1337) as WrappedFunction;
34-
fn.__sentry__ = false;
3534
Object.defineProperty(fn, '__sentry_wrapped__', {
3635
get(): void {
3736
throw new Error('boom');
3837
},
3938
});
4039

4140
expect(wrap(fn)).toBe(fn);
42-
43-
Object.defineProperty(fn, '__sentry__', {
44-
get(): void {
45-
throw new Error('boom');
46-
},
47-
configurable: true,
48-
});
49-
50-
expect(wrap(fn)).toBe(fn);
5141
});
5242

5343
it('returns wrapped function if original was already wrapped', () => {
@@ -83,9 +73,6 @@ describe('internal wrap()', () => {
8373
expect(fn).toHaveProperty('__sentry_wrapped__');
8474
expect(fn.__sentry_wrapped__).toBe(wrapped);
8575

86-
expect(wrapped).toHaveProperty('__sentry__');
87-
expect(wrapped.__sentry__).toBe(true);
88-
8976
expect(wrapped).toHaveProperty('__sentry_original__');
9077
expect(wrapped.__sentry_original__).toBe(fn);
9178
});
@@ -128,27 +115,14 @@ describe('internal wrap()', () => {
128115
expect(fnArgB).toHaveProperty('__sentry_wrapped__');
129116
});
130117

131-
it('calls either `handleEvent` property if it exists or the original function', () => {
132-
interface SinonEventSpy extends SinonSpy {
133-
handleEvent: SinonSpy;
134-
}
135-
118+
it('calls the original function', () => {
136119
const fn = spy();
137-
const eventFn = spy() as SinonEventSpy;
138-
eventFn.handleEvent = spy();
139120

140121
wrap(fn)(123, 'Rick');
141-
wrap(eventFn)(123, 'Morty');
142122

143123
expect(fn.called).toBe(true);
144124
expect(fn.getCalls()[0].args[0]).toBe(123);
145125
expect(fn.getCalls()[0].args[1]).toBe('Rick');
146-
147-
expect(eventFn.handleEvent.called).toBe(true);
148-
expect(eventFn.handleEvent.getCalls()[0].args[0]).toBe(123);
149-
expect(eventFn.handleEvent.getCalls()[0].args[1]).toBe('Morty');
150-
151-
expect(eventFn.called).toBe(false);
152126
});
153127

154128
it('preserves `this` context for all the calls', () => {
@@ -192,14 +166,11 @@ describe('internal wrap()', () => {
192166
const wrapped = wrap(fn);
193167

194168
// Shouldn't show up in iteration
195-
expect(Object.keys(fn)).toEqual(expect.not.arrayContaining(['__sentry__']));
196169
expect(Object.keys(fn)).toEqual(expect.not.arrayContaining(['__sentry_original__']));
197170
expect(Object.keys(fn)).toEqual(expect.not.arrayContaining(['__sentry_wrapped__']));
198-
expect(Object.keys(wrapped)).toEqual(expect.not.arrayContaining(['__sentry__']));
199171
expect(Object.keys(wrapped)).toEqual(expect.not.arrayContaining(['__sentry_original__']));
200172
expect(Object.keys(wrapped)).toEqual(expect.not.arrayContaining(['__sentry_wrapped__']));
201173
// But should be accessible directly
202-
expect(wrapped.__sentry__).toBe(true);
203174
expect(wrapped.__sentry_original__).toBe(fn);
204175
expect(fn.__sentry_wrapped__).toBe(wrapped);
205176
});

packages/core/src/integration.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { addGlobalEventProcessor, getCurrentHub } from '@sentry/hub';
22
import { Integration, Options } from '@sentry/types';
3-
import { logger } from '@sentry/utils';
3+
import { addNonEnumerableProperty, logger } from '@sentry/utils';
44

55
export const installedIntegrations: string[] = [];
66

@@ -77,6 +77,6 @@ export function setupIntegrations<O extends Options>(options: O): IntegrationInd
7777
// set the `initialized` flag so we don't run through the process again unecessarily; use `Object.defineProperty`
7878
// because by default it creates a property which is nonenumerable, which we want since `initialized` shouldn't be
7979
// considered a member of the index the way the actual integrations are
80-
Object.defineProperty(integrations, 'initialized', { value: true });
80+
addNonEnumerableProperty(integrations, 'initialized', true);
8181
return integrations;
8282
}

packages/core/src/integrations/functiontostring.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Integration, WrappedFunction } from '@sentry/types';
2+
import { getOriginalFunction } from '@sentry/utils';
23

34
let originalFunctionToString: () => void;
45

@@ -23,7 +24,7 @@ export class FunctionToString implements Integration {
2324

2425
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2526
Function.prototype.toString = function(this: WrappedFunction, ...args: any[]): string {
26-
const context = this.__sentry_original__ || this;
27+
const context = getOriginalFunction(this) || this;
2728
return originalFunctionToString.apply(context, args);
2829
};
2930
}

packages/types/src/wrappedfunction.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/** JSDoc */
22
export interface WrappedFunction extends Function {
33
[key: string]: any;
4-
__sentry__?: boolean;
54
__sentry_wrapped__?: WrappedFunction;
65
__sentry_original__?: WrappedFunction;
76
}

packages/utils/src/misc.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { Event, Exception, Mechanism, StackFrame } from '@sentry/types';
33

44
import { getGlobalObject } from './global';
5+
import { addNonEnumerableProperty } from './object';
56
import { snipLine } from './string';
67

78
/**
@@ -277,9 +278,7 @@ export function checkOrSetAlreadyCaught(exception: unknown): boolean {
277278
try {
278279
// set it this way rather than by assignment so that it's not ennumerable and therefore isn't recorded by the
279280
// `ExtraErrorData` integration
280-
Object.defineProperty(exception, '__sentry_captured__', {
281-
value: true,
282-
});
281+
addNonEnumerableProperty(exception, '__sentry_captured__', true);
283282
} catch (err) {
284283
// `exception` is a primitive, so we can't mark it seen
285284
}

packages/utils/src/object.ts

+39-7
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,7 @@ export function fill(source: { [key: string]: any }, name: string, replacementFa
3131
// otherwise it'll throw "TypeError: Object.defineProperties called on non-object"
3232
if (typeof wrapped === 'function') {
3333
try {
34-
wrapped.prototype = wrapped.prototype || {};
35-
Object.defineProperties(wrapped, {
36-
__sentry_original__: {
37-
enumerable: false,
38-
value: original,
39-
},
40-
});
34+
markFunctionWrapped(wrapped, original);
4135
} catch (_Oo) {
4236
// This can throw if multiple fill happens on a global object like XMLHttpRequest
4337
// Fixes https://github.com/getsentry/sentry-javascript/issues/2043
@@ -47,6 +41,44 @@ export function fill(source: { [key: string]: any }, name: string, replacementFa
4741
source[name] = wrapped;
4842
}
4943

44+
/**
45+
* Defines a non enumerable property. This creates a non enumerable property on an object.
46+
*
47+
* @param func The function to set a property to
48+
* @param name the name of the special sentry property
49+
* @param value the property to define
50+
*/
51+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
52+
export function addNonEnumerableProperty(func: any, name: string, value: any): void {
53+
Object.defineProperty(func, name, {
54+
value: value,
55+
});
56+
}
57+
58+
/**
59+
* Remembers the original function on the wrapped function and
60+
* patches up the prototype.
61+
*
62+
* @param wrapped the wrapper function
63+
* @param original the original function that gets wrapped
64+
*/
65+
export function markFunctionWrapped(wrapped: WrappedFunction, original: WrappedFunction): void {
66+
const proto = original.prototype || {};
67+
wrapped.prototype = original.prototype = proto;
68+
addNonEnumerableProperty(wrapped, '__sentry_original__', original);
69+
}
70+
71+
/**
72+
* This extracts the original function if available. See
73+
* `markFunctionWrapped` for more information.
74+
*
75+
* @param func the function to unwrap
76+
* @returns the unwrapped version of the function if available.
77+
*/
78+
export function getOriginalFunction(func: WrappedFunction): WrappedFunction | undefined {
79+
return func.__sentry_original__;
80+
}
81+
5082
/**
5183
* Encodes given object into url-friendly format
5284
*

0 commit comments

Comments
 (0)