Skip to content

Commit aff8415

Browse files
kamilogorekHazAT
authored andcommitted
fix: Normalize data before passing it to transport (#1826)
* fix: Handle circular dependencies in serialize method * Use truncate instead
1 parent 3146f94 commit aff8415

File tree

4 files changed

+143
-46
lines changed

4 files changed

+143
-46
lines changed

CHANGELOG.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
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
1310
- [core]: ref: Make sure that captureMessage input is a primitive
11+
- [core]: fix: Check if value is error object in extraErrorData integration
1412
- [browser] fix: Prevent empty exception values
13+
- [browser]: fix: Permission denied to access property name
14+
- [node] feat: Add file cache for providing pre/post context in frames
15+
- [node] feat: New option `frameContextLines`, if set to `0` we do not provide source code pre/post context, default is
16+
`7` lines pre/post
17+
- [utils] fix: Use custom serializer inside `serialize` method to prevent circular references
1518

1619
## 4.4.2
1720

packages/browser/src/integrations/trycatch.ts

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

213-
/** JSDoc */
213+
/**
214+
* Safely extract function name from itself
215+
*/
214216
function getFunctionName(fn: any): string {
215217
try {
216218
return (fn && fn.name) || '<anonymous>';

packages/utils/src/object.ts

Lines changed: 25 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { SentryWrappedFunction } from '@sentry/types';
22
import { isNaN, isPlainObject, isUndefined } from './is';
3+
import { truncate } from './string';
34

45
/**
56
* Just an Error object with arbitrary attributes attached to it.
@@ -11,17 +12,12 @@ interface ExtendedError extends Error {
1112
/**
1213
* Serializes the given object into a string.
1314
* 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
1915
*
2016
* @param object A JSON-serializable object.
2117
* @returns A string containing the serialized object.
2218
*/
2319
export function serialize<T>(object: T): string {
24-
return JSON.stringify(object);
20+
return JSON.stringify(object, serializer({ normalize: false }));
2521
}
2622

2723
/**
@@ -105,34 +101,19 @@ function jsonSize(value: any): number {
105101

106102
/** JSDoc */
107103
function serializeValue<T>(value: T): T | string {
108-
const maxLength = 40;
109-
110-
if (typeof value === 'string') {
111-
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]';
119-
}
120-
121104
const type = Object.prototype.toString.call(value);
122105

123-
// Node.js REPL notation
124-
if (type === '[object Object]') {
106+
if (typeof value === 'string') {
107+
return truncate(value, 40);
108+
} else if (type === '[object Object]') {
109+
// Node.js REPL notation
125110
return '[Object]';
126-
}
127-
if (type === '[object Array]') {
111+
} else if (type === '[object Array]') {
112+
// Node.js REPL notation
128113
return '[Array]';
114+
} else {
115+
return normalizeValue(value) as T;
129116
}
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;
136117
}
137118

138119
/** JSDoc */
@@ -261,13 +242,15 @@ function objectifyError(error: ExtendedError): object {
261242
}
262243

263244
/**
264-
* standardizeValue()
245+
* normalizeValue()
265246
*
266-
* translates undefined/NaN values to "[undefined]"/"[NaN]" respectively,
267-
* serializes Error objects
268-
* filter global objects
247+
* Takes unserializable input and make it serializable friendly
248+
*
249+
* - translates undefined/NaN values to "[undefined]"/"[NaN]" respectively,
250+
* - serializes Error objects
251+
* - filter global objects
269252
*/
270-
function standardizeValue(value: any, key: any): any {
253+
function normalizeValue(value: any, key?: any): any {
271254
if (key === 'domain' && typeof value === 'object' && (value as { _events: any })._events) {
272255
return '[Domain]';
273256
}
@@ -305,25 +288,25 @@ function standardizeValue(value: any, key: any): any {
305288
}
306289

307290
if (typeof value === 'function') {
308-
return `[Function] ${(value as () => void).name || '<unknown-function-name>'}`;
291+
return `[Function: ${(value as () => void).name || '<unknown-function-name>'}]`;
309292
}
310293

311294
return value;
312295
}
313296

314297
/**
315-
* standardizer()
298+
* serializer()
316299
*
317300
* Remove circular references,
318301
* translates undefined/NaN values to "[undefined]"/"[NaN]" respectively,
319302
* and takes care of Error objects serialization
320303
*/
321-
function standardizer(): (key: string, value: any) => any {
304+
function serializer(options: { normalize: boolean } = { normalize: true }): (key: string, value: any) => any {
322305
const stack: any[] = [];
323306
const keys: string[] = [];
324307

325308
/** recursive */
326-
function cycleStandardizer(_key: string, value: any): any {
309+
function cycleserializer(_key: string, value: any): any {
327310
if (stack[0] === value) {
328311
return '[Circular ~]';
329312
}
@@ -344,24 +327,24 @@ function standardizer(): (key: string, value: any) => any {
344327

345328
if (stack.indexOf(value) !== -1) {
346329
// tslint:disable-next-line:no-parameter-reassignment
347-
value = cycleStandardizer.call(this, key, value);
330+
value = cycleserializer.call(this, key, value);
348331
}
349332
} else {
350333
stack.push(value);
351334
}
352335

353-
return standardizeValue(value, key);
336+
return options.normalize ? normalizeValue(value, key) : value;
354337
};
355338
}
356339

357340
/**
358341
* safeNormalize()
359342
*
360-
* Creates a copy of the input by applying standardizer function on it and parsing it back to unify the data
343+
* Creates a copy of the input by applying serializer function on it and parsing it back to unify the data
361344
*/
362345
export function safeNormalize(input: any): any {
363346
try {
364-
return JSON.parse(JSON.stringify(input, standardizer()));
347+
return JSON.parse(JSON.stringify(input, serializer({ normalize: true })));
365348
} catch (_oO) {
366349
return '**non-serializable**';
367350
}

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)