Skip to content

fix: Normalize data before passing it to transport #1826

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
- [core] feat: Deprecate `captureEvent`, prefer `sendEvent` for transports. `sendEvent` now takes a string (body)
instead of `Event` object.
- [core] feat: Use correct buffer for requests in transports
- [node] feat: Add file cache for providing pre/post context in frames
- [node] feat: New option `frameContextLines`, if set to `0` we do not provide source code pre/post context, default is
`7` lines pre/post
- [core]: ref: Change way how transports are initialized
- [core]: ref: Rename `RequestBuffer` to `PromiseBuffer`, also introduce limit
- [core]: ref: Make sure that captureMessage input is a primitive
- [core]: fix: Check if value is error object in extraErrorData integration
- [browser] fix: Prevent empty exception values
- [browser]: fix: Permission denied to access property name
- [node] feat: Add file cache for providing pre/post context in frames
- [node] feat: New option `frameContextLines`, if set to `0` we do not provide source code pre/post context, default is
`7` lines pre/post
- [utils] fix: Use custom serializer inside `serialize` method to prevent circular references

## 4.4.2

Expand Down
4 changes: 3 additions & 1 deletion packages/browser/src/integrations/trycatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,9 @@ export class TryCatch implements Integration {
}
}

/** JSDoc */
/**
* Safely extract function name from itself
*/
function getFunctionName(fn: any): string {
try {
return (fn && fn.name) || '<anonymous>';
Expand Down
67 changes: 25 additions & 42 deletions packages/utils/src/object.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SentryWrappedFunction } from '@sentry/types';
import { isNaN, isPlainObject, isUndefined } from './is';
import { truncate } from './string';

/**
* Just an Error object with arbitrary attributes attached to it.
Expand All @@ -11,17 +12,12 @@ interface ExtendedError extends Error {
/**
* Serializes the given object into a string.
* Like JSON.stringify, but doesn't throw on circular references.
* Based on a `json-stringify-safe` package and modified to handle Errors serialization.
*
* The object must be serializable, i.e.:
* - Only primitive types are allowed (object, array, number, string, boolean)
* - Its depth should be considerably low for performance reasons
*
* @param object A JSON-serializable object.
* @returns A string containing the serialized object.
*/
export function serialize<T>(object: T): string {
return JSON.stringify(object);
return JSON.stringify(object, serializer({ normalize: false }));
}

/**
Expand Down Expand Up @@ -105,34 +101,19 @@ function jsonSize(value: any): number {

/** JSDoc */
function serializeValue<T>(value: T): T | string {
const maxLength = 40;

if (typeof value === 'string') {
return value.length <= maxLength ? value : `${value.substr(0, maxLength - 1)}\u2026`;
} else if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'undefined') {
return value;
} else if (isNaN(value)) {
// NaN and undefined are not JSON.parseable, but we want to preserve this information
return '[NaN]';
} else if (isUndefined(value)) {
return '[undefined]';
}

const type = Object.prototype.toString.call(value);

// Node.js REPL notation
if (type === '[object Object]') {
if (typeof value === 'string') {
return truncate(value, 40);
} else if (type === '[object Object]') {
// Node.js REPL notation
return '[Object]';
}
if (type === '[object Array]') {
} else if (type === '[object Array]') {
// Node.js REPL notation
return '[Array]';
} else {
return normalizeValue(value) as T;
}
if (type === '[object Function]') {
const name = ((value as any) as (() => void)).name;
return name ? `[Function: ${name}]` : '[Function]';
}

return value;
}

/** JSDoc */
Expand Down Expand Up @@ -261,13 +242,15 @@ function objectifyError(error: ExtendedError): object {
}

/**
* standardizeValue()
* normalizeValue()
*
* translates undefined/NaN values to "[undefined]"/"[NaN]" respectively,
* serializes Error objects
* filter global objects
* Takes unserializable input and make it serializable friendly
*
* - translates undefined/NaN values to "[undefined]"/"[NaN]" respectively,
* - serializes Error objects
* - filter global objects
*/
function standardizeValue(value: any, key: any): any {
function normalizeValue(value: any, key?: any): any {
if (key === 'domain' && typeof value === 'object' && (value as { _events: any })._events) {
return '[Domain]';
}
Expand Down Expand Up @@ -305,25 +288,25 @@ function standardizeValue(value: any, key: any): any {
}

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

return value;
}

/**
* standardizer()
* serializer()
*
* Remove circular references,
* translates undefined/NaN values to "[undefined]"/"[NaN]" respectively,
* and takes care of Error objects serialization
*/
function standardizer(): (key: string, value: any) => any {
function serializer(options: { normalize: boolean } = { normalize: true }): (key: string, value: any) => any {
const stack: any[] = [];
const keys: string[] = [];

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

if (stack.indexOf(value) !== -1) {
// tslint:disable-next-line:no-parameter-reassignment
value = cycleStandardizer.call(this, key, value);
value = cycleserializer.call(this, key, value);
}
} else {
stack.push(value);
}

return standardizeValue(value, key);
return options.normalize ? normalizeValue(value, key) : value;
};
}

/**
* safeNormalize()
*
* Creates a copy of the input by applying standardizer function on it and parsing it back to unify the data
* Creates a copy of the input by applying serializer function on it and parsing it back to unify the data
*/
export function safeNormalize(input: any): any {
try {
return JSON.parse(JSON.stringify(input, standardizer()));
return JSON.parse(JSON.stringify(input, serializer({ normalize: true })));
} catch (_oO) {
return '**non-serializable**';
}
Expand Down
109 changes: 109 additions & 0 deletions packages/utils/test/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,115 @@ describe('serialize()', () => {
expect(serialize(entry.object)).toEqual(entry.serialized);
});
}

describe('cyclical structures', () => {
test('circular objects', () => {
const obj = { name: 'Alice' };
// @ts-ignore
obj.self = obj;
expect(serialize(obj)).toEqual(JSON.stringify({ name: 'Alice', self: '[Circular ~]' }));
});

test('circular objects with intermediaries', () => {
const obj = { name: 'Alice' };
// @ts-ignore
obj.identity = { self: obj };
expect(serialize(obj)).toEqual(JSON.stringify({ name: 'Alice', identity: { self: '[Circular ~]' } }));
});

test('circular objects deeper', () => {
const obj = { name: 'Alice', child: { name: 'Bob' } };
// @ts-ignore
obj.child.self = obj.child;
expect(serialize(obj)).toEqual(
JSON.stringify({
name: 'Alice',
child: { name: 'Bob', self: '[Circular ~.child]' },
}),
);
});

test('circular objects deeper with intermediaries', () => {
const obj = { name: 'Alice', child: { name: 'Bob' } };
// @ts-ignore
obj.child.identity = { self: obj.child };
expect(serialize(obj)).toEqual(
JSON.stringify({
name: 'Alice',
child: { name: 'Bob', identity: { self: '[Circular ~.child]' } },
}),
);
});

test('circular objects in an array', () => {
const obj = { name: 'Alice' };
// @ts-ignore
obj.self = [obj, obj];
expect(serialize(obj)).toEqual(
JSON.stringify({
name: 'Alice',
self: ['[Circular ~]', '[Circular ~]'],
}),
);
});

test('circular objects deeper in an array', () => {
const obj = {
name: 'Alice',
children: [{ name: 'Bob' }, { name: 'Eve' }],
};
// @ts-ignore
obj.children[0].self = obj.children[0];
// @ts-ignore
obj.children[1].self = obj.children[1];
expect(serialize(obj)).toEqual(
JSON.stringify({
name: 'Alice',
children: [
{ name: 'Bob', self: '[Circular ~.children.0]' },
{ name: 'Eve', self: '[Circular ~.children.1]' },
],
}),
);
});

test('circular arrays', () => {
const obj: object[] = [];
obj.push(obj);
obj.push(obj);
expect(serialize(obj)).toEqual(JSON.stringify(['[Circular ~]', '[Circular ~]']));
});

test('circular arrays with intermediaries', () => {
const obj: object[] = [];
obj.push({ name: 'Alice', self: obj });
obj.push({ name: 'Bob', self: obj });
expect(serialize(obj)).toEqual(
JSON.stringify([{ name: 'Alice', self: '[Circular ~]' }, { name: 'Bob', self: '[Circular ~]' }]),
);
});

test('repeated objects in objects', () => {
const obj = {};
const alice = { name: 'Alice' };
// @ts-ignore
obj.alice1 = alice;
// @ts-ignore
obj.alice2 = alice;
expect(serialize(obj)).toEqual(
JSON.stringify({
alice1: { name: 'Alice' },
alice2: { name: 'Alice' },
}),
);
});

test('repeated objects in arrays', () => {
const alice = { name: 'Alice' };
const obj = [alice, alice];
expect(serialize(obj)).toEqual(JSON.stringify([{ name: 'Alice' }, { name: 'Alice' }]));
});
});
});

describe('deserialize()', () => {
Expand Down