scroll-restoration registers pagehide with a bare addEventListener, which breaks under a patched EventTarget
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 15.1k
- Forks
- 1.9k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 143
Description
Which project does this relate to?
Router
Describe the bug
setupScrollRestoration registers its pagehide handler with an unqualified call:
// packages/router-core/src/scroll-restoration.ts:222
addEventListener('pagehide', () => { … })
From an ES module that passes this === undefined. The native method resolves an undefined receiver to the global per WebIDL, so the call is legal and works normally — but only as long as nothing has replaced EventTarget.prototype.addEventListener with a plain JS function that reads its receiver.
TypeError: Invalid value used as weak map key
at WeakMap.set (<anonymous>)
at UserInteractionInstrumentation.addPatchedListener (…/@opentelemetry_instrumentation-user-interaction.js:487:19)
at addEventListenerPatched (…/@opentelemetry_instrumentation-user-interaction.js:533:16)
at setupScrollRestoration (…/@tanstack_react-router.js:1549:3)
at RouterCore.update (…/@tanstack_react-router.js:3133:25)
at new RouterCore (…/@tanstack_react-router.js:3817:8)
at new Router (…/@tanstack_react-router.js:6850:3)
at createRouter (…/@tanstack_react-router.js:6846:9)
at …/src/main.tsx:100:16
The throw is uncaught and synchronous, out of createRouter() — nothing renders at all. #root stays empty and the body holds two elements (the root div and the module script). There is also no useful console output: React reports this class of failure as a generic An error occurred in the <AwaitInner> component, logged via console.error("%s\n\n%s", …), so the substitution args carrying the real error are dropped by every forwarder in the chain. The stack above only exists because the reproducer hooks window.onerror.
The instrumentation is at fault for throwing on a legal call, and that is being fixed upstream (open-telemetry/opentelemetry-js-contrib#3639, PR telemetry/opentelemetry-js-contrib#3653). But the
bare call is the only one of its kind in the package — every other listener registration qualifies its receiver, including document.addEventListener two lines above this one — and it is fragile against any library that patches EventTarget (OTel, Zone.js, Sentry, analytics wrappers). window. on line 222 makes router construction independent of the WebIDL fallback.
Complete minimal reproducer
https://github.com/typedrat/tanstack-router-otel-user-interaction-repro
Steps to Reproduce the Bug
git clone https://github.com/typedrat/tanstack-router-otel-user-interaction-repro && cd tanstack-router-otel-user-interaction-repronpm install && npm run dev- Open http://localhost:5199 in a Chromium-based browser.
- The page is blank and the
TypeErrorabove is thrown. The app is ~70 lines in a singlesrc/main.tsx: two routes,createRouter({ routeTree, scrollRestoration: true }), and oneregisterInstrumentations(…)call.
Two independent one-line toggles each make it render, which isolates the
interaction:
| variant | result |
|---|---|
instrumentation registered, scrollRestoration: true |
blank page, TypeError |
instrumentation registered, scrollRestoration unset |
renders |
registerInstrumentations(…) commented out, scrollRestoration: true |
renders |
So this only affects apps that opt into scrollRestoration — that is the flag on
setupScrollRestoration's shouldSetupScrollRestoration guard, and the bare call
sits inside it.
Expected behavior
The router registers its pagehide handler on window and constructs normally regardless of what has patched EventTarget.prototype.addEventListener.
Screenshots or Videos
No response
Platform
- Router / Start Version:
@tanstack/react-router1.170.18 (@tanstack/router-core1.171.15; the call site is unchanged onmain) - OS: Linux
- Browser: Chrome
- Browser Version: 151.0.7922.108 (headless)
- Bundler: vite
- Bundler Version: 8.2.1
Also present with React 19.2.8, @opentelemetry/instrumentation-user-interaction 0.65.0, @opentelemetry/sdk-trace-web 2.10.0, and no zone.js in the tree. The no-Zone patch path is the one that breaks.
Additional context
Triggered in a real app by @opentelemetry/auto-instrumentations-web >= 0.66.0, which is the release that moved its instrumentation-user-interaction dependency to ^0.65.0.
We've worked around it by wrapping the upstream wrapper to reinstate the WebIDL fallback, and I can confirm that does resolve the issue.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/router-core/src/scroll-restoration.ts at the pagehide registration around line 222, and compare it with the qualified document listener above. Reproduce the failure with the linked minimal app using scrollRestoration and patched EventTarget instrumentation. Done means router construction succeeds and the page renders with the pagehide handler registered on window.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 1/5
- Estimated time
- Under an hour
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100