@@ -4,7 +4,6 @@ import { WrappedFunction } from '@sentry/types';
4
4
5
5
import { htmlTreeAsString } from './browser' ;
6
6
import { isElement , isError , isEvent , isInstanceOf , isPlainObject , isPrimitive } from './is' ;
7
- import { memoBuilder , MemoFunc } from './memo' ;
8
7
import { truncate } from './string' ;
9
8
10
9
/**
@@ -204,42 +203,61 @@ export function extractExceptionKeysForMessage(exception: Record<string, unknown
204
203
}
205
204
206
205
/**
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`.
208
207
* Works recursively on objects and arrays.
209
208
*
210
209
* Attention: This function keeps circular references in the returned object.
211
210
*/
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
+
213
217
// 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 ) ;
215
219
}
216
220
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 ;
223
227
}
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 ) ;
228
236
}
229
237
}
230
- return rv as T ;
238
+
239
+ return returnValue as T ;
231
240
}
232
241
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 ;
236
247
}
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 ;
240
258
}
241
259
242
- return val ;
260
+ return inputValue ;
243
261
}
244
262
245
263
/**
0 commit comments