Skip to content

Commit 5337ee6

Browse files
committed
fix: Wrap helper function revisited and tested
1 parent 37f15bc commit 5337ee6

File tree

2 files changed

+196
-3
lines changed

2 files changed

+196
-3
lines changed

packages/browser/src/integrations/helpers.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,16 @@ export function wrap(
3535
} = {},
3636
before?: SentryWrappedFunction,
3737
): any {
38+
if (!isFunction(fn)) {
39+
return fn;
40+
}
41+
3842
try {
3943
// We don't wanna wrap it twice
4044
if (fn.__sentry__) {
4145
return fn;
4246
}
47+
4348
// If this has already been wrapped in the past, return that wrapped function
4449
if (fn.__sentry_wrapped__) {
4550
return fn.__sentry_wrapped__;
@@ -51,17 +56,23 @@ export function wrap(
5156
return fn;
5257
}
5358

54-
const wrapped: SentryWrappedFunction = (...args: any[]) => {
59+
const wrapped: SentryWrappedFunction = function(this: any): void {
5560
if (before && isFunction(before)) {
56-
before.apply(undefined, args);
61+
before.apply(this, arguments);
5762
}
5863

5964
try {
6065
// Attempt to invoke user-land function
6166
// NOTE: If you are a Sentry user, and you are seeing this stack frame, it
6267
// means Raven caught an error invoking your application code. This is
6368
// expected behavior and NOT indicative of a bug with Raven.js.
64-
return fn.apply(undefined, args);
69+
const wrappedArguments = Array.from(arguments).map(arg => wrap(arg, options));
70+
71+
if (fn.handleEvent) {
72+
return fn.handleEvent.apply(this, wrappedArguments);
73+
} else {
74+
return fn.apply(this, wrappedArguments);
75+
}
6576
} catch (ex) {
6677
ignoreNextOnError();
6778

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { SentryWrappedFunction } from '@sentry/types';
2+
import { expect } from 'chai';
3+
import { SinonSpy, spy } from 'sinon';
4+
import { wrap } from '../../src/integrations/helpers';
5+
6+
describe('wrap()', () => {
7+
it('should wrap only functions', () => {
8+
const fn = () => 1337;
9+
const obj = { pickle: 'Rick' };
10+
const arr = ['Morty'];
11+
const str = 'Rick';
12+
const num = 42;
13+
14+
expect(wrap(fn)).not.equal(fn);
15+
// @ts-ignore
16+
expect(wrap(obj)).equal(obj);
17+
// @ts-ignore
18+
expect(wrap(arr)).equal(arr);
19+
// @ts-ignore
20+
expect(wrap(str)).equal(str);
21+
// @ts-ignore
22+
expect(wrap(num)).equal(num);
23+
});
24+
25+
it('bail out with the original if accessing custom props go bad', () => {
26+
const fn = (() => 1337) as SentryWrappedFunction;
27+
fn.__sentry__ = false;
28+
Object.defineProperty(fn, '__sentry_wrapped__', {
29+
get(): void {
30+
throw new Error('boom');
31+
},
32+
});
33+
34+
expect(wrap(fn)).equal(fn);
35+
36+
Object.defineProperty(fn, '__sentry__', {
37+
get(): void {
38+
throw new Error('boom');
39+
},
40+
configurable: true,
41+
});
42+
43+
expect(wrap(fn)).equal(fn);
44+
});
45+
46+
it('returns wrapped function if original was already wrapped', () => {
47+
const fn = (() => 1337) as SentryWrappedFunction;
48+
const wrapped = wrap(fn);
49+
50+
expect(wrap(fn)).equal(wrapped);
51+
});
52+
53+
it('returns same wrapped function if trying to wrap it again', () => {
54+
const fn = (() => 1337) as SentryWrappedFunction;
55+
56+
const wrapped = wrap(fn);
57+
58+
expect(wrap(wrapped)).equal(wrapped);
59+
});
60+
61+
it('calls "before" function when invoking wrapped function', () => {
62+
const fn = (() => 1337) as SentryWrappedFunction;
63+
const before = spy();
64+
65+
const wrapped = wrap(fn, {}, before);
66+
wrapped();
67+
68+
expect(before.called).equal(true);
69+
});
70+
71+
it('attaches metadata to original and wrapped functions', () => {
72+
const fn = (() => 1337) as SentryWrappedFunction;
73+
74+
const wrapped = wrap(fn);
75+
76+
expect(fn).to.have.property('__sentry_wrapped__');
77+
expect(fn.__sentry_wrapped__).equal(wrapped);
78+
79+
expect(wrapped).to.have.property('__sentry__');
80+
expect(wrapped.__sentry__).equal(true);
81+
82+
expect(wrapped).to.have.property('__sentry_original__');
83+
expect(wrapped.__sentry_original__).equal(fn);
84+
});
85+
86+
it('copies over original functions properties', () => {
87+
const fn = (() => 1337) as SentryWrappedFunction;
88+
fn.some = 1337;
89+
fn.property = 'Rick';
90+
91+
const wrapped = wrap(fn);
92+
93+
expect(wrapped).to.have.property('some');
94+
expect(wrapped.some).equal(1337);
95+
expect(wrapped).to.have.property('property');
96+
expect(wrapped.property).equal('Rick');
97+
});
98+
99+
it('doesnt break when accessing original functions properties blows up', () => {
100+
const fn = (() => 1337) as SentryWrappedFunction;
101+
Object.defineProperty(fn, 'some', {
102+
get(): void {
103+
throw new Error('boom');
104+
},
105+
});
106+
107+
const wrapped = wrap(fn);
108+
109+
expect(wrapped).to.not.have.property('some');
110+
});
111+
112+
it('recrusively wraps arguments that are functions', () => {
113+
const fn = (() => 1337) as SentryWrappedFunction;
114+
const fnArgA = () => 1337;
115+
const fnArgB = () => 1337;
116+
117+
const wrapped = wrap(fn);
118+
wrapped(fnArgA, fnArgB);
119+
120+
expect(fnArgA).to.have.property('__sentry_wrapped__');
121+
expect(fnArgB).to.have.property('__sentry_wrapped__');
122+
});
123+
124+
it('calls either `handleEvent` property if it exists or the original function', () => {
125+
interface SinonEventSpy extends SinonSpy {
126+
handleEvent: SinonSpy;
127+
}
128+
129+
const fn = spy();
130+
const eventFn = spy() as SinonEventSpy;
131+
eventFn.handleEvent = spy();
132+
133+
wrap(fn)(123, 'Rick');
134+
wrap(eventFn)(123, 'Morty');
135+
136+
expect(fn.called).equal(true);
137+
expect(fn.getCalls()[0].args[0]).equal(123);
138+
expect(fn.getCalls()[0].args[1]).equal('Rick');
139+
140+
expect(eventFn.handleEvent.called).equal(true);
141+
expect(eventFn.handleEvent.getCalls()[0].args[0]).equal(123);
142+
expect(eventFn.handleEvent.getCalls()[0].args[1]).equal('Morty');
143+
144+
expect(eventFn.called).equal(false);
145+
});
146+
147+
it('preserves `this` context for all the calls', () => {
148+
const context = {
149+
fn(): void {
150+
expect(this).equal(context);
151+
},
152+
eventFn(): void {
153+
return;
154+
},
155+
};
156+
// @ts-ignore
157+
context.eventFn.handleEvent = function(): void {
158+
expect(this).equal(context);
159+
};
160+
161+
// tslint:disable-next-line:no-unbound-method
162+
const wrappedFn = wrap(context.fn);
163+
// tslint:disable-next-line:no-unbound-method
164+
const wrappedEventFn = wrap(context.eventFn);
165+
166+
wrappedFn.call(context);
167+
wrappedEventFn.call(context);
168+
});
169+
170+
it('should rethrow caught exceptions', () => {
171+
const fn = () => {
172+
throw new Error('boom');
173+
};
174+
const wrapped = wrap(fn);
175+
176+
try {
177+
wrapped();
178+
} catch (error) {
179+
expect(error.message).equal('boom');
180+
}
181+
});
182+
});

0 commit comments

Comments
 (0)