fix(analytics-browser): tolerate nullish querySelectorAll in mutation observers - #1919
Open
Mercy811 wants to merge 1 commit into
Open
Conversation
… observers The form interaction and file download plugins guarded that an added node's querySelectorAll exists and is callable, but not that its return value is iterable. When a patched querySelectorAll returns a nullish value, Array.from throws, and the throw propagates out of both nested forEach loops and escapes the MutationObserver callback as an unhandled error. Every form or link appended after the offending node in the same mutation batch is then never registered, so Form Started / Form Submitted and File Downloaded never fire for those elements. The observer survives for later batches, which makes the loss partial and invisible. Guard the return value, drop the unsound casts to array types, and wrap the per-node body in a try/catch that logs via config.loggerProvider so one malformed node cannot abort registration for the rest of the batch. Fixes #1918 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
size-limit report 📦
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Fixes SDK-190 / closes #1918.
formInteractionTrackingandfileDownloadTrackingguard that an added node'squerySelectorAllexists and is callable, but not that its return value is iterable:When a patched
querySelectorAllreturns a nullish value,Array.fromthrows. Because the throw happens inside nestedforEachcallbacks, it aborts iteration of bothmutation.addedNodesand the outermutationsarray, then escapes theMutationObservercallback as an unhandled error. Two consequences:Form Started/Form Submitted/File Downloadednever fire for those elements. The observer survives for later batches, so the loss is invisible — it looks like users simply didn't interact.TypeErrorattributed to the host page's bundle.Per #1918, production RUM shows 642 occurrences over 30 days across 21 browser/OS combinations, all
handling: unhandled, with retained occurrences dating to 2024-10-24. Theas HTMLFormElement[]cast is what kept the compiler from flagging the nullish case —querySelectorAllreturns aNodeList, not an array.file-download-tracking.tshad the byte-identical pattern for anchor tags and failed the same way, so both are fixed here.Changes
Per node, in both plugins:
as HTMLFormElement[]/as HTMLAnchorElement[]). The node is narrowed toElementsoquerySelectorAll('form')resolves toNodeListOf<HTMLFormElement>via the tag-name overload, and the result is annotated| null | undefined— a sound widening that admits the type lies at runtime, rather than a narrowing cast that hides it. Noanyis reintroduced, which matters becauseno-unsafe-*is active insrc/.try/catchthat logs viaconfig.loggerProvider.warn, so one malformed node cannot abort registration for the rest of the batch. This addresses the silent-data-loss half independently of this specific nullish case — e.g. a hostile node whosequerySelectorAllthrows outright.Array.fromis no longer needed:querySelectorAllreturns a staticNodeList, so there is nothing to snapshot, andNodeList.forEachis already relied on formutation.addedNodesin these same callbacks.Also corrects a copy-paste comment in the form plugin ("anchor tags" → "form tags") on the block being rewritten.
Tests
Four regression tests, two per plugin, in the existing suites:
querySelectorAllreturnsundefined, appended between two wrapped elements in a single mutation batch; asserts the later element is still registered and tracked.querySelectorAllthrows; asserts the warning is logged and the element appended after it is still registered.Verified these are genuine regression tests: with the
srcchanges stashed, exactly these 4 fail (with the production error,TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))atArray.from) and the 24 pre-existing tests pass. With the fix, all 28 pass, and both changed files stay at 100% statements/branches/functions/lines — no newistanbul ignoreneeded.Out of scope
querySelectUniqueElementsinplugin-autocapture-browser/src/helpers.tshas the same guard-existence-but-not-return-value gap, but it is unreachable: no production module imports it (only its own test does) and it is not re-exported from that package'sindex.ts. Fixing it would mean a second package release with no user-facing effect; the more appropriate follow-up is deleting it, whichpnpm lint:deps/ knip territory covers better than this PR.Checklist
🤖 Generated with Claude Code