Skip to content

scroll-restoration registers pagehide with a bare addEventListener, which breaks under a patched EventTarget #8024

Description

@typedrat

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

  1. git clone https://github.com/typedrat/tanstack-router-otel-user-interaction-repro && cd tanstack-router-otel-user-interaction-repro
  2. npm install && npm run dev
  3. Open http://localhost:5199 in a Chromium-based browser.
  4. The page is blank and the TypeError above is thrown. The app is ~70 lines in a single src/main.tsx: two routes, createRouter({ routeTree, scrollRestoration: true }), and one registerInstrumentations(…) 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-router 1.170.18 (@tanstack/router-core 1.171.15; the call site is unchanged on main)
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions