diff --git a/packages/utils/src/normalize.ts b/packages/utils/src/normalize.ts index 51566a2125a7..5e97ad6e069b 100644 --- a/packages/utils/src/normalize.ts +++ b/packages/utils/src/normalize.ts @@ -33,7 +33,7 @@ type ObjOrArray = { [key: string]: T }; */ export function normalize(input: unknown, depth: number = +Infinity, maxProperties: number = +Infinity): any { try { - // since we're at the outermost level, there is no key + // since we're at the outermost level, we don't provide a key return visit('', input, depth, maxProperties); } catch (err) { return { ERROR: `**non-serializable** (${err})` }; @@ -98,6 +98,15 @@ function visit( return stringified; } + // From here on, we can assert that `value` is either an object or an array. + + // Do not normalize objects that we know have already been normalized. As a general rule, the + // "__sentry_skip_normalization__" property should only be used sparingly and only should only be set on objects that + // have already been normalized. + if ((value as ObjOrArray)['__sentry_skip_normalization__']) { + return value as ObjOrArray; + } + // We're also done if we've reached the max depth if (depth === 0) { // At this point we know `serialized` is a string of the form `"[object XXXX]"`. Clean it up so it's just `"[XXXX]"`. diff --git a/packages/utils/test/normalize.test.ts b/packages/utils/test/normalize.test.ts index 756020b403de..e5d01de4e962 100644 --- a/packages/utils/test/normalize.test.ts +++ b/packages/utils/test/normalize.test.ts @@ -4,6 +4,7 @@ import * as isModule from '../src/is'; import { normalize } from '../src/normalize'; +import { addNonEnumerableProperty } from '../src/object'; import * as stacktraceModule from '../src/stacktrace'; describe('normalize()', () => { @@ -504,4 +505,52 @@ describe('normalize()', () => { qux: '[Function: qux]', }); }); + + describe('skips normalizing objects marked with a non-enumerable property __sentry_skip_normalization__', () => { + test('by leaving non-serializable values intact', () => { + const someFun = () => undefined; + const alreadyNormalizedObj = { + nan: NaN, + fun: someFun, + }; + + addNonEnumerableProperty(alreadyNormalizedObj, '__sentry_skip_normalization__', true); + + const result = normalize(alreadyNormalizedObj); + expect(result).toEqual({ + nan: NaN, + fun: someFun, + }); + }); + + test('by ignoring normalization depth', () => { + const alreadyNormalizedObj = { + three: { + more: { + layers: '!', + }, + }, + }; + + addNonEnumerableProperty(alreadyNormalizedObj, '__sentry_skip_normalization__', true); + + const obj = { + foo: { + bar: { + baz: alreadyNormalizedObj, + boo: { + bam: { + pow: 'poof', + }, + }, + }, + }, + }; + + const result = normalize(obj, 4); + + expect(result?.foo?.bar?.baz?.three?.more?.layers).toBe('!'); + expect(result?.foo?.bar?.boo?.bam?.pow).not.toBe('poof'); + }); + }); });