Skip to content

Commit 5e2ddf3

Browse files
authored
fix(utils): Fix false-positive circular references when normalizing Event objects (#3864)
When `captureException` is passed a non-`Error` object, we do our best to extract as much data as we can from that object. (That's what leads to the much-maligned "Non-Error exception [or promise rejection] captured with keys x, y, and z" error message.) A common case in which this occurs is when code of the form `Promise.reject(someEvent)` runs - common enough, in fact, that we handle `Event` objects separately. Specifically, we capture the event's `type`, `target` (the element which caused the event), and `currentTarget` (the element with the event listener on it) properties (none of which are enumerable), along with anything else on the event which _is_ enumerable. For most events, that "anything else" includes only one property: `isTrusted`, a boolean indicating whether or not the event is the result of a user action. For a long time, though, `isTrusted` has been showing up not as a boolean but as `[Circular ~]`. It turns out that's because when we try to grab the enumerable property values, we end up grabbing the entire event instead. (It's only shown up in the `isTrusted` value, but only because that's the only enumerable property on most `Event`s.) This fixes that, and adds a test for pulling data out of `Event` objects.
1 parent 8ef39cc commit 5e2ddf3

File tree

3 files changed

+72
-25
lines changed

3 files changed

+72
-25
lines changed

packages/utils/src/object.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,11 @@ function getWalkSource(
108108
[key: string]: any;
109109
} = {};
110110

111+
// Accessing event attributes can throw (see https://github.com/getsentry/sentry-javascript/issues/768 and
112+
// https://github.com/getsentry/sentry-javascript/issues/838), but accessing `type` hasn't been wrapped in a
113+
// try-catch in at least two years and no one's complained, so that's likely not an issue anymore
111114
source.type = event.type;
112115

113-
// Accessing event.target can throw (see getsentry/raven-js#838, #768)
114116
try {
115117
source.target = isElement(event.target)
116118
? htmlTreeAsString(event.target)
@@ -131,9 +133,9 @@ function getWalkSource(
131133
source.detail = event.detail;
132134
}
133135

134-
for (const i in event) {
135-
if (Object.prototype.hasOwnProperty.call(event, i)) {
136-
source[i] = event;
136+
for (const attr in event) {
137+
if (Object.prototype.hasOwnProperty.call(event, attr)) {
138+
source[attr] = event[attr];
137139
}
138140
}
139141

packages/utils/test/object.test.ts

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
import * as isModule from '../src/is';
16
import { dropUndefinedKeys, extractExceptionKeysForMessage, fill, normalize, urlEncode } from '../src/object';
7+
import { testOnlyIfNodeVersionAtLeast } from './testutils';
28

39
describe('fill()', () => {
410
test('wraps a method by calling a replacement function on it', () => {
@@ -119,28 +125,54 @@ describe('normalize()', () => {
119125
});
120126
});
121127

122-
test('extracts extra properties from error objects', () => {
123-
const obj = new Error('Wubba Lubba Dub Dub') as any;
124-
obj.reason = new TypeError("I'm pickle Riiick!");
125-
obj.extra = 'some extra prop';
126-
127-
obj.stack = 'x';
128-
obj.reason.stack = 'x';
129-
130-
// IE 10/11
131-
delete obj.description;
132-
delete obj.reason.description;
133-
134-
expect(normalize(obj)).toEqual({
135-
message: 'Wubba Lubba Dub Dub',
136-
name: 'Error',
137-
stack: 'x',
138-
reason: {
139-
message: "I'm pickle Riiick!",
140-
name: 'TypeError',
128+
describe('getWalkSource()', () => {
129+
test('extracts extra properties from error objects', () => {
130+
const obj = new Error('Wubba Lubba Dub Dub') as any;
131+
obj.reason = new TypeError("I'm pickle Riiick!");
132+
obj.extra = 'some extra prop';
133+
134+
obj.stack = 'x';
135+
obj.reason.stack = 'x';
136+
137+
// IE 10/11
138+
delete obj.description;
139+
delete obj.reason.description;
140+
141+
expect(normalize(obj)).toEqual({
142+
message: 'Wubba Lubba Dub Dub',
143+
name: 'Error',
141144
stack: 'x',
142-
},
143-
extra: 'some extra prop',
145+
reason: {
146+
message: "I'm pickle Riiick!",
147+
name: 'TypeError',
148+
stack: 'x',
149+
},
150+
extra: 'some extra prop',
151+
});
152+
});
153+
154+
testOnlyIfNodeVersionAtLeast(8)('extracts data from `Event` objects', () => {
155+
const isElement = jest.spyOn(isModule, 'isElement').mockReturnValue(true);
156+
const getAttribute = () => undefined;
157+
158+
const parkElement = { tagName: 'PARK', getAttribute };
159+
const treeElement = { tagName: 'TREE', parentNode: parkElement, getAttribute };
160+
const squirrelElement = { tagName: 'SQUIRREL', parentNode: treeElement, getAttribute };
161+
162+
const chaseEvent = new Event('chase');
163+
Object.defineProperty(chaseEvent, 'target', { value: squirrelElement });
164+
Object.defineProperty(chaseEvent, 'currentTarget', { value: parkElement });
165+
Object.defineProperty(chaseEvent, 'wagging', { value: true, enumerable: false });
166+
167+
expect(normalize(chaseEvent)).toEqual({
168+
currentTarget: 'park',
169+
isTrusted: false,
170+
target: 'park > tree > squirrel',
171+
type: 'chase',
172+
// notice that `wagging` isn't included because it's not enumerable and not one of the ones we specifically extract
173+
});
174+
175+
isElement.mockRestore();
144176
});
145177
});
146178

packages/utils/test/testutils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const testOnlyIfNodeVersionAtLeast = (minVersion: number): jest.It => {
2+
const currentNodeVersion = process.env.NODE_VERSION;
3+
4+
try {
5+
if (Number(currentNodeVersion?.split('.')[0]) < minVersion) {
6+
return it.skip;
7+
}
8+
} catch (oO) {
9+
// we can't tell, so err on the side of running the test
10+
}
11+
12+
return it;
13+
};

0 commit comments

Comments
 (0)