Skip to content

refactor [NT-3339]: Refactor auto page emitter architecture#303

Merged
Lotfi Anwar L Arif (Lotfi-Arif) merged 5 commits into
mainfrom
refactor-auto-page-emitter-architecture
Jun 3, 2026
Merged

refactor [NT-3339]: Refactor auto page emitter architecture#303
Lotfi Anwar L Arif (Lotfi-Arif) merged 5 commits into
mainfrom
refactor-auto-page-emitter-architecture

Conversation

@Lotfi-Arif
Copy link
Copy Markdown
Contributor

@Lotfi-Arif Lotfi Anwar L Arif (Lotfi-Arif) commented Jun 3, 2026

Summary

Restructures the auto-page tracker stack so each router adapter (TanStack Router, React Router, Next.js App Router, Next.js Pages Router) owns its own page event payload, while useAutoPageEmitter becomes a generic emitter with a single responsibility: dedup + emit.

Each of the four router adapters now follows the same five-step structure:

  1. Read router state via the router's React API.
  2. Build a router-derived payload from the router's authoritative state.
  3. Build the emission context for the consumer's getPagePayload callback.
  4. Compose the layers via buildAutoPagePayload inside a buildPayload callback.
  5. Hand routeKey + buildPayload to useAutoPageEmitter.

What this fixes

The TanStack stale-URL bug from NT-3339 — page events reflecting the previous route after navigation — is fixed as a side effect of step 2. The same fix is applied uniformly across all four adapters; none of them now rely on window.location for path/url/search data. The Next.js App Router adapter intentionally omits hash (Next does not expose it), so the SDK still fills it in from window.location.hash, which is not subject to the same staleness.

…d add buildAutoPagePayload helper

Shrink useAutoPageEmitter to a router-agnostic emitter that owns dedup
and emission only. The hook now accepts { enabled, routeKey, buildPayload }
and no longer knows about routers, route contexts, payload merging, or the
consumer's pagePayload/getPagePayload props.

Move payload composition into a new buildAutoPagePayload helper that merges
three explicit layers (router-derived, consumer static, consumer dynamic)
and forwards the emission context to getPagePayload. composePagePayload is
extended to accept any number of layers so the helper can compose them
cleanly; the previous two-arg call sites continue to work.

These changes prepare the four router adapters to build their own finished
page payloads. The hook becomes trivial to test without a router; the merge
precedence becomes one explicit data flow inside the helper rather than
inline logic in the hook.
All four router adapters (TanStack, React Router, Next.js App Router,
Next.js Pages Router) now follow the same five-step structure:

  1. Read router state via the router's React API.
  2. Build a router-derived payload (path, url, search, hash, query) from
     authoritative router state, not a window.location read at send time.
  3. Build the emission context for the consumer's getPagePayload callback.
  4. Compose router payload + consumer static + consumer dynamic via the
     shared buildAutoPagePayload helper, inside a buildPayload callback.
  5. Hand routeKey + buildPayload to useAutoPageEmitter.

The router-derived payload becomes the bottom layer of the merge stack.
Consumer-supplied pagePayload and getPagePayload still take precedence on
key conflicts, so existing consumer code works unchanged.

This restructure also fixes a class of stale-URL bugs across all four
adapters (originally reported for TanStack Router): page event payloads
now reflect the route React just rendered, not whatever window.location
shows at emission time. The Next.js App Router adapter intentionally
omits hash, since Next does not expose it; getPageProperties() supplies
it from window.location.hash, which is not subject to the same staleness
as pathname/search.
- Add pagePayload.test.ts: standalone tests for composePagePayload's
  variadic merge semantics and buildAutoPagePayload's three-layer
  precedence + context forwarding. Pure helper, no React or routers.

- Rewrite useAutoPageEmitter.test.tsx for the slim hook surface
  ({ enabled, routeKey, buildPayload }). Adds tests that
  buildPayload only runs when an emission would actually happen
  (i.e. not on dedup short-circuit) and that isInitialEmission is
  signalled correctly across emissions.

- Update each router adapter test (tanstack-router, react-router,
  next-app, next-pages) to assert the new payload shape that includes
  the router-derived path / url / search / hash / query keys, alongside
  whatever static or dynamic consumer overrides each test supplies.

- Update OptimizationProvider.onStatesReady.test.tsx for the new hook
  call shape.
Comment thread packages/web/frameworks/react-web-sdk/src/router/next-pages.tsx Fixed
Comment thread packages/web/frameworks/react-web-sdk/src/router/next-pages.tsx Fixed
Comment thread packages/web/frameworks/react-web-sdk/src/router/react-router.tsx Fixed
CodeQL flagged remote property injection in the four router adapters
where user-controlled query keys were assigned to plain {} objects via
`obj[key] = value`. A malicious URL containing keys like __proto__ or
constructor could mutate the prototype chain, affecting downstream code
that reads from these query dictionaries.

Replace the imperative for-loop assignments with Object.fromEntries,
which uses [[DefineOwnProperty]] semantics and is not subject to the
same prototype-setter trap. This satisfies CodeQL's remote-property-
injection sink check without an unsafe type assertion.
@Lotfi-Arif Lotfi Anwar L Arif (Lotfi-Arif) merged commit b424fc0 into main Jun 3, 2026
37 checks passed
@Lotfi-Arif Lotfi Anwar L Arif (Lotfi-Arif) deleted the refactor-auto-page-emitter-architecture branch June 3, 2026 15:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants