diff --git a/packages/utils/src/object.ts b/packages/utils/src/object.ts index d4974cecbf44..5957214ecc86 100644 --- a/packages/utils/src/object.ts +++ b/packages/utils/src/object.ts @@ -4,6 +4,7 @@ import { WrappedFunction } from '@sentry/types'; import { htmlTreeAsString } from './browser'; import { isElement, isError, isEvent, isInstanceOf, isPlainObject, isPrimitive } from './is'; +import { memoBuilder, MemoFunc } from './memo'; import { truncate } from './string'; /** @@ -205,20 +206,37 @@ export function extractExceptionKeysForMessage(exception: Record(val: T): T { + // This function just proxies `_dropUndefinedKeys` to keep the `memoBuilder` out of this function's API + return _dropUndefinedKeys(val, memoBuilder()); +} + +function _dropUndefinedKeys(val: T, memo: MemoFunc): T { + const [memoize] = memo; // we don't need unmemoize because we don't need to visit nodes twice + if (isPlainObject(val)) { + if (memoize(val)) { + return val; + } const rv: { [key: string]: any } = {}; for (const key of Object.keys(val)) { if (typeof val[key] !== 'undefined') { - rv[key] = dropUndefinedKeys(val[key]); + rv[key] = _dropUndefinedKeys(val[key], memo); } } return rv as T; } if (Array.isArray(val)) { - return (val as any[]).map(dropUndefinedKeys) as any; + if (memoize(val)) { + return val; + } + return (val as any[]).map(item => { + return _dropUndefinedKeys(item, memo); + }) as any; } return val; diff --git a/packages/utils/test/object.test.ts b/packages/utils/test/object.test.ts index e4fc4428ae04..65131fa0cd94 100644 --- a/packages/utils/test/object.test.ts +++ b/packages/utils/test/object.test.ts @@ -199,6 +199,47 @@ describe('dropUndefinedKeys()', () => { }, }); }); + + test('objects with circular reference', () => { + const dog: any = { + food: undefined, + }; + + const human = { + brain: undefined, + pets: dog, + }; + + const rat = { + scares: human, + weight: '4kg', + }; + + dog.chases = rat; + + expect(dropUndefinedKeys(human)).toStrictEqual({ + pets: { + chases: rat, + }, + }); + }); + + test('arrays with circular reference', () => { + const egg: any[] = []; + + const chicken = { + food: undefined, + weight: '1kg', + lays: egg, + }; + + egg[0] = chicken; + + expect(dropUndefinedKeys(chicken)).toStrictEqual({ + lays: egg, + weight: '1kg', + }); + }); }); describe('objectify()', () => {