Skip to content

Commit d9f9bee

Browse files
committed
fix: Handle circular dependencies in serialize method
1 parent 3c45c2f commit d9f9bee

File tree

4 files changed

+132
-44
lines changed

4 files changed

+132
-44
lines changed

CHANGELOG.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
- [core] feat: Deprecate `captureEvent`, prefer `sendEvent` for transports. `sendEvent` now takes a string (body)
66
instead of `Event` object.
77
- [core] feat: Use correct buffer for requests in transports
8-
- [node] feat: Add file cache for providing pre/post context in frames
9-
- [node] feat: New option `frameContextLines`, if set to `0` we do not provide source code pre/post context, default is
10-
`7` lines pre/post
118
- [core]: ref: Change way how transports are initialized
129
- [core]: ref: Rename `RequestBuffer` to `PromiseBuffer`, also introduce limit
10+
- [core]: fix: Check if value is error object in extraErrorData integration
1311
- [browser] fix: Prevent empty exception values
12+
- [browser]: fix: Permission denied to access property name
13+
- [node] feat: Add file cache for providing pre/post context in frames
14+
- [node] feat: New option `frameContextLines`, if set to `0` we do not provide source code pre/post context, default is
15+
`7` lines pre/post
16+
- [utils] fix: Use custom serializer inside `serialize` method to prevent circular references
1417

1518
## 4.4.2
1619

packages/browser/src/integrations/trycatch.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,12 @@ export class TryCatch implements Integration {
210210
}
211211
}
212212

213+
/**
214+
* Safely extract function name from itself
215+
*/
213216
function getFunctionName(fn: any): string {
214217
try {
215-
return fn && fn.name || '<anonymous>';
218+
return (fn && fn.name) || '<anonymous>';
216219
} catch (e) {
217220
// Just accessing custom props in some Selenium environments
218221
// can cause a "Permission denied" exception (see raven-js#495).

packages/utils/src/object.ts

Lines changed: 13 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,12 @@ interface ExtendedError extends Error {
1111
/**
1212
* Serializes the given object into a string.
1313
* Like JSON.stringify, but doesn't throw on circular references.
14-
* Based on a `json-stringify-safe` package and modified to handle Errors serialization.
15-
*
16-
* The object must be serializable, i.e.:
17-
* - Only primitive types are allowed (object, array, number, string, boolean)
18-
* - Its depth should be considerably low for performance reasons
1914
*
2015
* @param object A JSON-serializable object.
2116
* @returns A string containing the serialized object.
2217
*/
2318
export function serialize<T>(object: T): string {
24-
return JSON.stringify(object);
19+
return JSON.stringify(object, serializer({ normalize: false }));
2520
}
2621

2722
/**
@@ -105,34 +100,12 @@ function jsonSize(value: any): number {
105100

106101
/** JSDoc */
107102
function serializeValue<T>(value: T): T | string {
108-
const maxLength = 40;
109-
110103
if (typeof value === 'string') {
104+
const maxLength = 40;
111105
return value.length <= maxLength ? value : `${value.substr(0, maxLength - 1)}\u2026`;
112-
} else if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'undefined') {
113-
return value;
114-
} else if (isNaN(value)) {
115-
// NaN and undefined are not JSON.parseable, but we want to preserve this information
116-
return '[NaN]';
117-
} else if (isUndefined(value)) {
118-
return '[undefined]';
106+
} else {
107+
return normalizeValue(value) as T;
119108
}
120-
121-
const type = Object.prototype.toString.call(value);
122-
123-
// Node.js REPL notation
124-
if (type === '[object Object]') {
125-
return '[Object]';
126-
}
127-
if (type === '[object Array]') {
128-
return '[Array]';
129-
}
130-
if (type === '[object Function]') {
131-
const name = ((value as any) as (() => void)).name;
132-
return name ? `[Function: ${name}]` : '[Function]';
133-
}
134-
135-
return value;
136109
}
137110

138111
/** JSDoc */
@@ -261,13 +234,13 @@ function objectifyError(error: ExtendedError): object {
261234
}
262235

263236
/**
264-
* standardizeValue()
237+
* normalizeValue()
265238
*
266239
* translates undefined/NaN values to "[undefined]"/"[NaN]" respectively,
267240
* serializes Error objects
268241
* filter global objects
269242
*/
270-
function standardizeValue(value: any, key: any): any {
243+
function normalizeValue(value: any, key?: any): any {
271244
if (key === 'domain' && typeof value === 'object' && (value as { _events: any })._events) {
272245
return '[Domain]';
273246
}
@@ -312,18 +285,18 @@ function standardizeValue(value: any, key: any): any {
312285
}
313286

314287
/**
315-
* standardizer()
288+
* serializer()
316289
*
317290
* Remove circular references,
318291
* translates undefined/NaN values to "[undefined]"/"[NaN]" respectively,
319292
* and takes care of Error objects serialization
320293
*/
321-
function standardizer(): (key: string, value: any) => any {
294+
function serializer(options: { normalize: boolean } = { normalize: true }): (key: string, value: any) => any {
322295
const stack: any[] = [];
323296
const keys: string[] = [];
324297

325298
/** recursive */
326-
function cycleStandardizer(_key: string, value: any): any {
299+
function cycleserializer(_key: string, value: any): any {
327300
if (stack[0] === value) {
328301
return '[Circular ~]';
329302
}
@@ -344,24 +317,24 @@ function standardizer(): (key: string, value: any) => any {
344317

345318
if (stack.indexOf(value) !== -1) {
346319
// tslint:disable-next-line:no-parameter-reassignment
347-
value = cycleStandardizer.call(this, key, value);
320+
value = cycleserializer.call(this, key, value);
348321
}
349322
} else {
350323
stack.push(value);
351324
}
352325

353-
return standardizeValue(value, key);
326+
return options.normalize ? normalizeValue(value, key) : value;
354327
};
355328
}
356329

357330
/**
358331
* safeNormalize()
359332
*
360-
* Creates a copy of the input by applying standardizer function on it and parsing it back to unify the data
333+
* Creates a copy of the input by applying serializer function on it and parsing it back to unify the data
361334
*/
362335
export function safeNormalize(input: any): any {
363336
try {
364-
return JSON.parse(JSON.stringify(input, standardizer()));
337+
return JSON.parse(JSON.stringify(input, serializer({ normalize: true })));
365338
} catch (_oO) {
366339
return '**non-serializable**';
367340
}

packages/utils/test/object.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,115 @@ describe('serialize()', () => {
2222
expect(serialize(entry.object)).toEqual(entry.serialized);
2323
});
2424
}
25+
26+
describe('cyclical structures', () => {
27+
test('circular objects', () => {
28+
const obj = { name: 'Alice' };
29+
// @ts-ignore
30+
obj.self = obj;
31+
expect(serialize(obj)).toEqual(JSON.stringify({ name: 'Alice', self: '[Circular ~]' }));
32+
});
33+
34+
test('circular objects with intermediaries', () => {
35+
const obj = { name: 'Alice' };
36+
// @ts-ignore
37+
obj.identity = { self: obj };
38+
expect(serialize(obj)).toEqual(JSON.stringify({ name: 'Alice', identity: { self: '[Circular ~]' } }));
39+
});
40+
41+
test('circular objects deeper', () => {
42+
const obj = { name: 'Alice', child: { name: 'Bob' } };
43+
// @ts-ignore
44+
obj.child.self = obj.child;
45+
expect(serialize(obj)).toEqual(
46+
JSON.stringify({
47+
name: 'Alice',
48+
child: { name: 'Bob', self: '[Circular ~.child]' },
49+
}),
50+
);
51+
});
52+
53+
test('circular objects deeper with intermediaries', () => {
54+
const obj = { name: 'Alice', child: { name: 'Bob' } };
55+
// @ts-ignore
56+
obj.child.identity = { self: obj.child };
57+
expect(serialize(obj)).toEqual(
58+
JSON.stringify({
59+
name: 'Alice',
60+
child: { name: 'Bob', identity: { self: '[Circular ~.child]' } },
61+
}),
62+
);
63+
});
64+
65+
test('circular objects in an array', () => {
66+
const obj = { name: 'Alice' };
67+
// @ts-ignore
68+
obj.self = [obj, obj];
69+
expect(serialize(obj)).toEqual(
70+
JSON.stringify({
71+
name: 'Alice',
72+
self: ['[Circular ~]', '[Circular ~]'],
73+
}),
74+
);
75+
});
76+
77+
test('circular objects deeper in an array', () => {
78+
const obj = {
79+
name: 'Alice',
80+
children: [{ name: 'Bob' }, { name: 'Eve' }],
81+
};
82+
// @ts-ignore
83+
obj.children[0].self = obj.children[0];
84+
// @ts-ignore
85+
obj.children[1].self = obj.children[1];
86+
expect(serialize(obj)).toEqual(
87+
JSON.stringify({
88+
name: 'Alice',
89+
children: [
90+
{ name: 'Bob', self: '[Circular ~.children.0]' },
91+
{ name: 'Eve', self: '[Circular ~.children.1]' },
92+
],
93+
}),
94+
);
95+
});
96+
97+
test('circular arrays', () => {
98+
const obj: object[] = [];
99+
obj.push(obj);
100+
obj.push(obj);
101+
expect(serialize(obj)).toEqual(JSON.stringify(['[Circular ~]', '[Circular ~]']));
102+
});
103+
104+
test('circular arrays with intermediaries', () => {
105+
const obj: object[] = [];
106+
obj.push({ name: 'Alice', self: obj });
107+
obj.push({ name: 'Bob', self: obj });
108+
expect(serialize(obj)).toEqual(
109+
JSON.stringify([{ name: 'Alice', self: '[Circular ~]' }, { name: 'Bob', self: '[Circular ~]' }]),
110+
);
111+
});
112+
113+
test('repeated objects in objects', () => {
114+
const obj = {};
115+
const alice = { name: 'Alice' };
116+
// @ts-ignore
117+
obj.alice1 = alice;
118+
// @ts-ignore
119+
obj.alice2 = alice;
120+
expect(serialize(obj)).toEqual(
121+
JSON.stringify({
122+
alice1: { name: 'Alice' },
123+
alice2: { name: 'Alice' },
124+
}),
125+
);
126+
});
127+
128+
test('repeated objects in arrays', () => {
129+
const alice = { name: 'Alice' };
130+
const obj = [alice, alice];
131+
expect(serialize(obj)).toEqual(JSON.stringify([{ name: 'Alice' }, { name: 'Alice' }]));
132+
});
133+
});
25134
});
26135

27136
describe('deserialize()', () => {

0 commit comments

Comments
 (0)