-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Correctly detect truly circular paths in JS objects, rather than objects with multiple references #1862
base: main
Are you sure you want to change the base?
Conversation
f4ffff9
to
beccaec
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a fun one, thanks for the PR! Just some test naming to flag and a little clarification to ensure my understanding. o-tracking is owned by the Data Platform team now but I'm happy to approve and release this on their behalf if they are
… objects with multiple references
beccaec
to
cb97aca
Compare
@notlee Thanks for your review. I've addressed your issues, but I don't think I've helped matters by force-pushing. GitHub is confused, or I've missed a step. Could you check over my changes please? Thank you! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this would work, but mostly because of the try/catch 😁 I've got a couple of nitpicks about the approach, and as we don't have proper circular detection anyway I'm wondering how good the resulting errors would be.
I'm still in favour of sending the events with some values replaced with error strings - I think that's nicer behaviour for a tracking library than dropping events?
@@ -222,99 +222,94 @@ function assignIfUndefined (subject, target) { | |||
} | |||
|
|||
/** | |||
* Used to find out all the paths which contain a circular reference. | |||
* Used to find out all the paths which contain a circular reference. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: I think this particular line now only applies to the contained function ;)
*/ | ||
function findCircularPathsIn(rootObject) { | ||
const traversedValues = new WeakSet(); | ||
function safelyStringifyJson(object) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: This function doesn't quite do this - should we change the name?
Right now it does three things:
- Try to JSON.stringify the object. If this succeeds, the object contains no circular references, and is returned.
- If the object failed to stringify, a safe-encoding routine is run, recording circular references.
- If circular references were found, an error is thrown.
So really the only reason for (2) is so that (3) can report the circular reference paths. And it also means this function is not safely stringifying the json - instead it's designed to error.
I think I'd be in favour of actually safely stringifying the data! I don't think we need to go as far as using the JSON schema's support, as it seems unlikely that we'd ever have a server try to reconstruct the original data. Instead I think we could replace it with a human-readable path reference, so the tracking data isn't lost; potentially also paired with a console.error - or perhaps instead an actual thrown error in a timeout, so that global error handlers can also catch/report?
oldPath = traversedValues.get(value); | ||
|
||
if (oldPath !== undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thought: much like the old function, I think this isn't actually detecting only circular references, is it? It's detecting object values seen before, by putting every object as a key in a weakmap and then using references if those objects are encountered again.
Your use of json.stringify() in a try/catch is hiding this from the tests I think 😁
(That said, I think the new function is a significant improvement over the old one because it has much better skipping of built-in types including null. But I think if we're relying on a list of circular references in the error, it won't be...)
This conversation uncovered an issue whereby multiple offline calls to
track()
could potentially share object references (for example, in the additionalcontext
object).The previous
containsCircularPaths()
implementation wasn't really looking for circular paths, but rather multiple references to the same object. See the original PR and issue.A use-case which is common in an offline-first application such as FT App - queueing multiple events to be stored and sent when next online - is somewhat likely to include a shared reference across the events at some point in the event tracking. This causes an
o-tracking does not support circular references in the analytics data
error, which discards the whole queue of events.Describe your changes
This implementation is based on https://github.com/douglascrockford/JSON-js/blob/master/cycle.js, and draws on the utility of
JSON.serialize()
's replacer parameter. It also makes use of the fact thatJSON.serialize()
will throw aTypeError
if there's a circular reference, so a normal attempt to serialize is made first, before even looking for circular references.Note
Note that the output of
jsonReplacer
could potentially be used in future to send a payload to Spoor with the circular references removed. Since this is a major change in behaviour, I've left that discussion for another time.Issue ticket number and link
Link to Figma designs
Checklist before requesting a review
percy
label for o-[COMPONENT] orchromatic
label for o3-[COMPONENT] on my PR before merging and after review. Find more details in CONTRIBUTING.md