Skip to content

Commit 89115ab

Browse files
committed
fix(utils): Try-catch monkeypatching to handle frozen objects/functions
1 parent e24d7e0 commit 89115ab

File tree

3 files changed

+127
-16
lines changed

3 files changed

+127
-16
lines changed

packages/browser/test/unit/index.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getReportDialogEndpoint, SDK_VERSION } from '@sentry/core';
2+
import { WrappedFunction } from '@sentry/types';
23
import * as utils from '@sentry/utils';
34

45
import type { Event } from '../../src';
@@ -391,4 +392,14 @@ describe('wrap()', () => {
391392

392393
expect(result2).toBe(42);
393394
});
395+
396+
it('should ignore frozen functions', () => {
397+
const func = Object.freeze(() => 42);
398+
399+
// eslint-disable-next-line deprecation/deprecation
400+
wrap(func);
401+
402+
expect(func()).toBe(42);
403+
expect((func as WrappedFunction).__sentry_wrapped__).toBeUndefined();
404+
});
394405
});

packages/utils/src/object.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,7 @@ export function fill(source: { [key: string]: any }, name: string, replacementFa
2828
// Make sure it's a function first, as we need to attach an empty prototype for `defineProperties` to work
2929
// otherwise it'll throw "TypeError: Object.defineProperties called on non-object"
3030
if (typeof wrapped === 'function') {
31-
try {
32-
markFunctionWrapped(wrapped, original);
33-
} catch (_Oo) {
34-
// This can throw if multiple fill happens on a global object like XMLHttpRequest
35-
// Fixes https://github.com/getsentry/sentry-javascript/issues/2043
36-
}
31+
markFunctionWrapped(wrapped, original);
3732
}
3833

3934
source[name] = wrapped;
@@ -47,12 +42,14 @@ export function fill(source: { [key: string]: any }, name: string, replacementFa
4742
* @param value The value to which to set the property
4843
*/
4944
export function addNonEnumerableProperty(obj: { [key: string]: unknown }, name: string, value: unknown): void {
50-
Object.defineProperty(obj, name, {
51-
// enumerable: false, // the default, so we can save on bundle size by not explicitly setting it
52-
value: value,
53-
writable: true,
54-
configurable: true,
55-
});
45+
try {
46+
Object.defineProperty(obj, name, {
47+
// enumerable: false, // the default, so we can save on bundle size by not explicitly setting it
48+
value: value,
49+
writable: true,
50+
configurable: true,
51+
});
52+
} catch (o_O) {} // eslint-disable-line no-empty
5653
}
5754

5855
/**
@@ -63,9 +60,11 @@ export function addNonEnumerableProperty(obj: { [key: string]: unknown }, name:
6360
* @param original the original function that gets wrapped
6461
*/
6562
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);
63+
try {
64+
const proto = original.prototype || {};
65+
wrapped.prototype = original.prototype = proto;
66+
addNonEnumerableProperty(wrapped, '__sentry_original__', original);
67+
} catch (o_O) {} // eslint-disable-line no-empty
6968
}
7069

7170
/**

packages/utils/test/object.test.ts

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22
* @jest-environment jsdom
33
*/
44

5-
import { dropUndefinedKeys, extractExceptionKeysForMessage, fill, objectify, urlEncode } from '../src/object';
5+
import { WrappedFunction } from '@sentry/types';
6+
import {
7+
dropUndefinedKeys,
8+
extractExceptionKeysForMessage,
9+
fill,
10+
objectify,
11+
urlEncode,
12+
addNonEnumerableProperty,
13+
markFunctionWrapped,
14+
} from '../src/object';
615
import { testOnlyIfNodeVersionAtLeast } from './testutils';
716

817
describe('fill()', () => {
@@ -315,3 +324,95 @@ describe('objectify()', () => {
315324
expect(objectifiedNonPrimtive).toBe(notAPrimitive);
316325
});
317326
});
327+
328+
describe('addNonEnumerableProperty', () => {
329+
it('works with a plain object', () => {
330+
const obj: { foo?: string } = {};
331+
addNonEnumerableProperty(obj, 'foo', 'bar');
332+
expect(obj.foo).toBe('bar');
333+
});
334+
335+
it('works with a class', () => {
336+
class MyClass {
337+
public foo?: string;
338+
}
339+
const obj = new MyClass();
340+
addNonEnumerableProperty(obj as any, 'foo', 'bar');
341+
expect(obj.foo).toBe('bar');
342+
});
343+
344+
it('works with a function', () => {
345+
const func = jest.fn();
346+
addNonEnumerableProperty(func as any, 'foo', 'bar');
347+
expect((func as any).foo).toBe('bar');
348+
func();
349+
expect(func).toHaveBeenCalledTimes(1);
350+
});
351+
352+
it('works with an existing property object', () => {
353+
const obj = { foo: 'before' };
354+
addNonEnumerableProperty(obj, 'foo', 'bar');
355+
expect(obj.foo).toBe('bar');
356+
});
357+
358+
it('works with an existing readonly property object', () => {
359+
const obj = { foo: 'before' };
360+
361+
Object.defineProperty(obj, 'foo', {
362+
value: 'defined',
363+
writable: false,
364+
});
365+
366+
addNonEnumerableProperty(obj, 'foo', 'bar');
367+
expect(obj.foo).toBe('bar');
368+
});
369+
370+
it('does not error with a frozen object', () => {
371+
const obj = Object.freeze({ foo: 'before' });
372+
373+
addNonEnumerableProperty(obj, 'foo', 'bar');
374+
expect(obj.foo).toBe('before');
375+
});
376+
});
377+
378+
describe('markFunctionWrapped', () => {
379+
it('works with a function', () => {
380+
const originalFunc = jest.fn();
381+
const wrappedFunc = jest.fn();
382+
markFunctionWrapped(wrappedFunc, originalFunc);
383+
384+
expect((wrappedFunc as WrappedFunction).__sentry_original__).toBe(originalFunc);
385+
386+
wrappedFunc();
387+
388+
expect(wrappedFunc).toHaveBeenCalledTimes(1);
389+
expect(originalFunc).not.toHaveBeenCalled();
390+
});
391+
392+
it('works with a frozen original function', () => {
393+
const originalFunc = Object.freeze(jest.fn());
394+
const wrappedFunc = jest.fn();
395+
markFunctionWrapped(wrappedFunc, originalFunc);
396+
397+
expect((wrappedFunc as WrappedFunction).__sentry_original__).toBe(originalFunc);
398+
399+
wrappedFunc();
400+
401+
expect(wrappedFunc).toHaveBeenCalledTimes(1);
402+
expect(originalFunc).not.toHaveBeenCalled();
403+
});
404+
405+
it('works with a frozen wrapped function', () => {
406+
const originalFunc = Object.freeze(jest.fn());
407+
const wrappedFunc = Object.freeze(jest.fn());
408+
markFunctionWrapped(wrappedFunc, originalFunc);
409+
410+
// Skips adding the property, but also doesn't error
411+
expect((wrappedFunc as WrappedFunction).__sentry_original__).toBe(undefined);
412+
413+
wrappedFunc();
414+
415+
expect(wrappedFunc).toHaveBeenCalledTimes(1);
416+
expect(originalFunc).not.toHaveBeenCalled();
417+
});
418+
});

0 commit comments

Comments
 (0)