fix(utils): Fix faulty references in dropUndefinedKeys
#5201
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
For a long time, our helper function
dropUndefinedKeys
(which might more accurately be nameddropKeysWithUndefinedValues
) had a bug: when presented with an object containing a circular reference, it was perfectly happy to follow that cycle and recurse infinitely right along with the object. This was fixed in #5163 using a memoizer - we keep a reference to each value we process, and if we hit one we've seen before, we stop recursing. No more infinite loops, yay!Unfortunately, that change introduced a new bug. By design,
dropUndefinedKeys
doesn't mutate the object it's given, instead performing a deep copy which just skips over any keys whose value isundefined
, eventually returning an object wholely separate from the original. If there's a circular reference in the original, its copy thus points from one spot in the new object to another spot in the new object and then back again. The cycle might be longer than two nodes, of course, but the overall point stands: every member of the cycle in the new object must itself be contained in (or equal to) the new object.The problem with the fix in #5163 is that it violates that rule. Before that change, when
dropUndefinedKeys
recursed infinitely, it wasn't creating a circular reference in the new object. Instead, it was creating infinitely nested copies of the members of the cycle. In other words, when presented withhen = { egg: { chicken: hen }}
,it was creating
henCopy = { egg: { chicken: { egg: { chicken: { egg: { chicken: { egg: ... }}}}}}}
.Since that change, and continuing with the same example, it processes
hen
andhen.egg
, but when it goes to processhen.egg.chicken
, it recognizes it as the samehen
it's already seen and instead of recursing into it just returns the existing one to complete the cycle. Unfortunately, that means it ends up creatinghenCopy = { egg: { chicken: hen }}
rather than
henCopy = { egg: { chicken: henCopy }}
,thereby violating the "all members of the new cycle must be part of the new object" rule.
This fixes that by using a map to store the new version of each value alongside its original. Now when a cycle is detected, instead of the original being returned, the copy is returned, completing the cycle entirely within the new object.
PR description courtesy of @lobsterkatie
Ref: https://getsentry.atlassian.net/browse/WEB-940
This PR should be followed up by: #5171