Repository navigation
fix(datadog): stop attributing GTM-injected vendor tag errors to Studio through the DOM guard frame - #1811
Conversation
There was a problem hiding this comment.
Code Review
This pull request addresses issue #1696 by bundling the translation DOM guard into the "vendor-datadog" chunk, which prevents vendor tags injected through GTM from carrying a Studio frame in their stack traces. Regression tests and a configuration test have been added to verify this behavior. The review feedback suggests robustifying the module ID matching in "vite.config.ts" by stripping potential query parameters before checking the file path.
| if (id.replace(/\\/g, '/').endsWith('/src/lib/installBrowserTranslationDomGuard.ts')) { | ||
| return 'vendor-datadog'; | ||
| } |
There was a problem hiding this comment.
Vite/Rollup can append query parameters to module IDs during resolution or transformation (e.g., ?worker, ?commonjs-entry, or other plugin-specific flags). If a query parameter is appended, endsWith on the raw id will fail to match, causing the translation DOM guard to be bundled into the main chunk instead of vendor-datadog. Stripping any query parameters before performing the path check ensures the bundling logic remains robust.
| if (id.replace(/\\/g, '/').endsWith('/src/lib/installBrowserTranslationDomGuard.ts')) { | |
| return 'vendor-datadog'; | |
| } | |
| const cleanId = id.split('?')[0].replace(/\\/g, '/'); | |
| if (cleanId.endsWith('/src/lib/installBrowserTranslationDomGuard.ts')) { | |
| return 'vendor-datadog'; | |
| } |
There was a problem hiding this comment.
Declining. The id this rule has to match never has a query. The guard is imported in exactly one place, a plain import … from '@/lib/installBrowserTranslationDomGuard' at src/main.tsx:5, so its module id is the bare file path. I checked that in a real build (described under Verification in the PR body): vite build --mode prod put Node.prototype.insertBefore=function(e,t){…} in vendor-datadog-*.js, and index-*.js contained no copy of it.
A query-suffixed id like ?raw, ?url or ?worker belongs to a different module that never runs the patch, so stripping the query would only pull that copy into the Datadog chunk. vite.config.test.ts pins how the real file path is placed.
🤖 Generated by Claude Code (Claude Opus 5.5); posted via @dawsontoth.
…io through the DOM guard frame GTM's LinkedIn Insight snippet calls `window.lintrk` before `snap.licdn.com` has loaded, and the `TypeError` was kept as a Studio error: 15 events, 4 sessions in 30 days, every one shaped `<anonymous>:1:8` -> `Node.insertBefore @ /assets/index-*.js` -> ~30 googletagmanager.com frames. Two frames on that stack scored as Studio's, and either one alone kept it: - `installBrowserTranslationDomGuard` (#1388) patches `Node.prototype.insertBefore` globally, so every script that inserts a node runs through it, and an inline `<script>` executes during the insertion. The guard lived in the app entry chunk, so its frame read as Studio code. - `www.googletagmanager.com` was not a known third-party host, so every GTM frame did too. `manualChunks` now builds the guard into `vendor-datadog`, the eager chunk `shouldKeepEvent` already treats as neutral instrumentation (the SDK's own fetch/XHR patches live there). Its frame is then recognised by URL in every browser, without trusting a function name: the guard's frame is named `Node.insertBefore`, `QI.Node.insertBefore` or `QI/Node.prototype.insertBefore` depending on the browser and build, and JavaScriptCore gives it no name at all. GTM's host joins `THIRD_PARTY_SCRIPT_FRAME`. Replaying 1,063 production error events with the guard frame moved to the new chunk changes 16 verdicts, all keep -> drop: the 15 `lintrk` events and one GTM-issued `Failed to fetch` that an extension blocked. A Studio error that passes through the guard still has React or Studio frames under it, so it is still kept. Closes #1696 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dc3edaa to
99b3393
Compare
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||
⊙ Problem
GTM's LinkedIn Insight snippet calls
window.lintrk(...)beforesnap.licdn.comhas loaded, which happens when an ad blocker blocks it or it is just slow. The resulting unhandledTypeErrorreached Error Tracking as a Studio error: 15 events from 4 sessions in the last 30 days, out of 63sourceerrors in total. Every one of them has this shape:Two frames on that stack scored as Studio's, and either one alone was enough to keep the error. First,
installBrowserTranslationDomGuard(#1388) patchesNode.prototype.insertBeforeglobally. Every script that inserts a node runs through the patch, and an inline<script>executes during the insertion. The guard was built into the app entry chunk, so its frame read as Studio code. Second,www.googletagmanager.comwas not a known third-party host, so every GTM frame also read as Studio code.💡 Solution
vite.config.ts'smanualChunksbuilds the guard intovendor-datadog, the already-eager chunk thatshouldKeepEventalready treats as neutral instrumentation (the Datadog SDK's fetch/XHR patches live there). The guard's frame now counts for neither side, in every browser, and adds no request.www.googletagmanager.comjoinsTHIRD_PARTY_SCRIPT_FRAME. The match requires the/after the host, so a host likewww.googletagmanager.com.example.netdoes not match.A real Studio error that passes through the guard is still kept. For example, if React's commit calls
insertBeforeand the native call throws, the stack has React frames under the guard, and those still count as Studio's.Stacked on #1807 (base
claude/1690-drop-dead-endpoint-gate), so merge that first. After the next prod deploy, success means thewindow.lintrk is not a functionfamily drops to 0 in Error Tracking, while errors with Studio frames under the guard frame still arrive.Closes #1696
⚖️ Alternatives
The planning review returned
better-alternative-exists, and I adopted its alternative. My plan put the guard in a newtranslation-dom-guardchunk. Codex pointed out that this adds a boot-critical request and one more static dependency that could 404 during a deploy, which would leave a blank page beforeinstallStaleDeployReloadis installed. The existingvendor-datadogchunk gives the same classification without either cost. The trade-off is a naming wart: a first-party module now lives in a chunk named after a vendor. Comments at both sites andvite.config.test.tspin the placement.Node.prototype.insertBefore = function (e, t) {…}no name at all (probed through JXA, which runs the system JSC). V8 and SpiderMonkey call itNode.insertBefore,QI.Node.insertBeforeorQI/Node.prototype.insertBefore, depending on build and browser. And the filter's existing contract is that only a frame's URL attributes it.snap.licdn.com) to the host list, which is the issue's first option. Rejected as insufficient: the loop returns "ours" at the guard frame before it reaches a GTM frame, so this alone drops 0 of the 15lintrkevents. I leftsnap.licdn.comout because it had 0 frames in 30 days (this error is licdn not loading).removeChildcrash that [RUM] NotFoundError: 'removeChild' called on non-child node in React DOM #1388 fixed.lintrk. Guarding the tag in GTM is still worth suggesting to whoever owns the container.🔧 Changes
vite.config.ts:manualChunksreturnsvendor-datadogfor any module id ending in/src/lib/installBrowserTranslationDomGuard.ts, before the existingnode_modulesearly return. I used a suffix match instead of an absolute-path comparison so that a symlinked checkout cannot silently leave the guard in the entry chunk.src/integrations/datadog/shouldKeepEvent.ts:THIRD_PARTY_SCRIPT_FRAMEgains GTM's host, and its doc no longer says "currently only Reo.dev".INSTRUMENTATION_FRAME's doc says why the guard lives in that chunk.vite.config.test.ts(new) andtsconfig.node.json: a unit test calls the real config factory and asserts that the guard maps tovendor-datadogand that the chunk file pattern is unchanged, so moving either side breaks the test.tsconfig.node.jsonincludes the new file sotsc -btype-checks it.https://fabric.harper.fast/…:1:N), and that frame still scores as Studio's. That is [RUM] Reo.dev is loaded and initialised twice — bundle loader plus a GTM tag; 60% of views open two Reo sessions #1762's "bare document URL" hardening, so the WebKit form of this error is still kept until it lands. The corpus has 0 WebKitlintrkevents.✅ Verification
End-to-end route: production build inspection plus corpus replay. A live check would need an ad-blocked GTM tag firing against a deployed build with RUM on.
VITE_TELEMETRY_ENABLED=true npx vite build --mode prod --outDir <scratch>(never served). It producedassets/vendor-datadog-4TDswomM.jscontainingNode.prototype.insertBefore=function(e,t){…}.index-*.jshas 0 occurrences ofNode.prototype.insertBeforeand imports from the Datadog chunk,index.htmlmodulepreloads the chunk, and the chunk imports nothing butrolldown-runtime, so there is no cycle.origin/stage's predicate and this branch's agree on 1,047. The 16 that change are all keep -> drop: 15lintrkevents (Chrome 10, Edge 3, Chrome Mobile 1, Firefox 1, so every browser naming of the guard frame matched) and 1TypeError: Failed to fetchwhose stack is an extension frame, the Datadog wrapper and GTM frames. No other verdict changed.npx vitest run src/integrations/datadog src/lib/installBrowserTranslationDomGuard.test.ts vite.config.test.ts src/configpassed 12 files / 225 tests. InshouldKeepEvent.test.ts, the newvendor tags injected through the DOM guardblock checks both directions. Dropped: the corpus shapes (Node.insertBefore,QI.Node.insertBefore, an unnamed frame, and the Firefox form) and a GTM-only stack. Kept: the same stack with the guard still inindex-*, a React-commitHierarchyRequestErrorthrough the guard and a Studio caller under it, a stack whose only located frame is the guard, and a host that merely starts with GTM's.manualChunksrule disabled, 6 tests fail: the fourlintrkdrops, the GTM-only drop, and the chunk-placement test.npx tsc -bexit 0,npx oxlint --format stylish .exit 0,npx dprint checkexit 0, andcommitlintpassed. The fullnpx vitest runon this branch (which includes refactor(datadog): remove shouldKeepEvent's instance-endpoint gate, which no RUM error can reach #1807) passed 396 files / 3,674 tests.--no-verifybecause of the machine, not the code. The hook'spnpm testtimed out 4 unrelated tests at 5s/30s while the load average was about 60–140 from other agents. Run alone, those 4 files passed 33/33, and the full suite had already passed at lower load.better-alternative-exists. I adopted its alternative, as described under Alternatives../vite.config, but this repo's imports are extensionless andtsconfig.node.jsondoes not enableallowImportingTsExtensions. Gemini's "major" was that a symlinked checkout could break the exact-path comparison. I fixed that with the suffix match above and rebuilt: the output is identical,vendor-datadog-4TDswomM.jsstill holds the guard, andindex-*.jshas 0 occurrences. I declined gemini's comment-trimming nit, because those comments state the cross-file chunk/filter invariant, which codex also judged acceptable. The Cursor leg could not run (the 1Password SSH agent refused itsgit fetch), and the Harper domain leg failed Claude OAuth. Both failures are infrastructure.🤖 Generated with Claude Code (Claude Opus 5.5); posted via @dawsontoth.
Related PRs: #1807 overlaps (this PR's base; merge it first), #1799 overlaps (open; edits
shouldKeepEvent.ts's interface, different lines), 1 others independentComplexity: medium
Review-Coverage: authored=claude; ran=gemini,codex; blocked=cursor-composer(no-receipt),domain(auth); declined=cursor-grok,cursor-kimi,cursor-muse; rounds=1; full=1 @ dc3edaa
Review-Attention: read ~3m (raised: degraded review) @ dc3edaa