Skip to content

Commit be63dd9

Browse files
authored
fix(utils): Fix faulty references in dropUndefinedKeys (#5201)
1 parent d1ae441 commit be63dd9

File tree

2 files changed

+77
-41
lines changed

2 files changed

+77
-41
lines changed

packages/utils/src/object.ts

+40-22
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { WrappedFunction } from '@sentry/types';
44

55
import { htmlTreeAsString } from './browser';
66
import { isElement, isError, isEvent, isInstanceOf, isPlainObject, isPrimitive } from './is';
7-
import { memoBuilder, MemoFunc } from './memo';
87
import { truncate } from './string';
98

109
/**
@@ -204,42 +203,61 @@ export function extractExceptionKeysForMessage(exception: Record<string, unknown
204203
}
205204

206205
/**
207-
* Given any object, return the new object with removed keys that value was `undefined`.
206+
* Given any object, return a new object having removed all fields whose value was `undefined`.
208207
* Works recursively on objects and arrays.
209208
*
210209
* Attention: This function keeps circular references in the returned object.
211210
*/
212-
export function dropUndefinedKeys<T>(val: T): T {
211+
export function dropUndefinedKeys<T>(inputValue: T): T {
212+
// This map keeps track of what already visited nodes map to.
213+
// Our Set - based memoBuilder doesn't work here because we want to the output object to have the same circular
214+
// references as the input object.
215+
const memoizationMap = new Map<unknown, unknown>();
216+
213217
// This function just proxies `_dropUndefinedKeys` to keep the `memoBuilder` out of this function's API
214-
return _dropUndefinedKeys(val, memoBuilder());
218+
return _dropUndefinedKeys(inputValue, memoizationMap);
215219
}
216220

217-
function _dropUndefinedKeys<T>(val: T, memo: MemoFunc): T {
218-
const [memoize] = memo; // we don't need unmemoize because we don't need to visit nodes twice
219-
220-
if (isPlainObject(val)) {
221-
if (memoize(val)) {
222-
return val;
221+
function _dropUndefinedKeys<T>(inputValue: T, memoizationMap: Map<unknown, unknown>): T {
222+
if (isPlainObject(inputValue)) {
223+
// If this node has already been visited due to a circular reference, return the object it was mapped to in the new object
224+
const memoVal = memoizationMap.get(inputValue);
225+
if (memoVal !== undefined) {
226+
return memoVal as T;
223227
}
224-
const rv: { [key: string]: any } = {};
225-
for (const key of Object.keys(val)) {
226-
if (typeof val[key] !== 'undefined') {
227-
rv[key] = _dropUndefinedKeys(val[key], memo);
228+
229+
const returnValue: { [key: string]: any } = {};
230+
// Store the mapping of this value in case we visit it again, in case of circular data
231+
memoizationMap.set(inputValue, returnValue);
232+
233+
for (const key of Object.keys(inputValue)) {
234+
if (typeof inputValue[key] !== 'undefined') {
235+
returnValue[key] = _dropUndefinedKeys(inputValue[key], memoizationMap);
228236
}
229237
}
230-
return rv as T;
238+
239+
return returnValue as T;
231240
}
232241

233-
if (Array.isArray(val)) {
234-
if (memoize(val)) {
235-
return val;
242+
if (Array.isArray(inputValue)) {
243+
// If this node has already been visited due to a circular reference, return the array it was mapped to in the new object
244+
const memoVal = memoizationMap.get(inputValue);
245+
if (memoVal !== undefined) {
246+
return memoVal as T;
236247
}
237-
return (val as any[]).map(item => {
238-
return _dropUndefinedKeys(item, memo);
239-
}) as any;
248+
249+
const returnValue: unknown[] = [];
250+
// Store the mapping of this value in case we visit it again, in case of circular data
251+
memoizationMap.set(inputValue, returnValue);
252+
253+
inputValue.forEach((item: unknown) => {
254+
returnValue.push(_dropUndefinedKeys(item, memoizationMap));
255+
});
256+
257+
return returnValue as unknown as T;
240258
}
241259

242-
return val;
260+
return inputValue;
243261
}
244262

245263
/**

packages/utils/test/object.test.ts

+37-19
Original file line numberDiff line numberDiff line change
@@ -200,28 +200,34 @@ describe('dropUndefinedKeys()', () => {
200200
});
201201
});
202202

203-
test('objects with circular reference', () => {
204-
const dog: any = {
203+
test('should not throw on objects with circular reference', () => {
204+
const chicken: any = {
205205
food: undefined,
206206
};
207207

208-
const human = {
209-
brain: undefined,
210-
pets: dog,
208+
const egg = {
209+
edges: undefined,
210+
contains: chicken,
211211
};
212212

213-
const rat = {
214-
scares: human,
215-
weight: '4kg',
216-
};
213+
chicken.lays = egg;
217214

218-
dog.chases = rat;
215+
const droppedChicken = dropUndefinedKeys(chicken);
219216

220-
expect(dropUndefinedKeys(human)).toStrictEqual({
221-
pets: {
222-
chases: rat,
223-
},
224-
});
217+
// Removes undefined keys
218+
expect(Object.keys(droppedChicken)).toEqual(['lays']);
219+
expect(Object.keys(droppedChicken.lays)).toEqual(['contains']);
220+
221+
// Returns new object
222+
expect(chicken === droppedChicken).toBe(false);
223+
expect(chicken.lays === droppedChicken.lays).toBe(false);
224+
225+
// Returns new references within objects
226+
expect(chicken === droppedChicken.lays.contains).toBe(false);
227+
expect(egg === droppedChicken.lays.contains.lays).toBe(false);
228+
229+
// Keeps circular reference
230+
expect(droppedChicken.lays.contains === droppedChicken).toBe(true);
225231
});
226232

227233
test('arrays with circular reference', () => {
@@ -235,10 +241,22 @@ describe('dropUndefinedKeys()', () => {
235241

236242
egg[0] = chicken;
237243

238-
expect(dropUndefinedKeys(chicken)).toStrictEqual({
239-
lays: egg,
240-
weight: '1kg',
241-
});
244+
const droppedChicken = dropUndefinedKeys(chicken);
245+
246+
// Removes undefined keys
247+
expect(Object.keys(droppedChicken)).toEqual(['weight', 'lays']);
248+
expect(Object.keys(droppedChicken.lays)).toEqual(['0']);
249+
250+
// Returns new objects
251+
expect(chicken === droppedChicken).toBe(false);
252+
expect(egg === droppedChicken.lays).toBe(false);
253+
254+
// Returns new references within objects
255+
expect(chicken === droppedChicken.lays[0]).toBe(false);
256+
expect(egg === droppedChicken.lays[0].lays).toBe(false);
257+
258+
// Keeps circular reference
259+
expect(droppedChicken.lays[0] === droppedChicken).toBe(true);
242260
});
243261
});
244262

0 commit comments

Comments
 (0)