Files
34759c3e1a router instrumentation: add transition start context (2/7) (#94766)
## Stack Position

(2/7) in the router instrumentation stack.

1. #94755: router instrumentation: refactor client hook dispatch (1/7)
2. **#94766: router instrumentation: add transition start context
(2/7)**
3. #94756: router instrumentation: add transition commit events (3/7)
4. #94765: router instrumentation: add commit route and prefetch context
(4/7)
5. #94757: router instrumentation: add transition abort events (5/7)
6. #94758: router instrumentation: add route mismatch events (6/7)
7. #94737: router instrumentation: add transition settlement events
(7/7)

Parent: #94755
Next: #94756

## What This PR Does

Adds an experimental third argument to the existing start hook:

```ts
export function onRouterTransitionStart(url, navigationType, event) {}
```

The event contains:

- `id` and framework-captured Unix `timestamp` in milliseconds
- `fromRoutes`: route patterns visible when navigation is dispatched
- `prefetchIntent`: `full`, `auto`, or `none`

The primary `children` route is first, followed by visible parallel
slots in deterministic order.

## Design

Start owns the context that is authoritative before navigation work
begins: the screen being left and the caller's prefetch intent.
Destination routes and the cache result actually used are reported by
commit instead. Consumers correlate them by transition ID.

Without `experimental.instrumentationClientRouterTransitionEvents`, the
existing two-argument hook behavior is unchanged.

## Not In This PR

- Destination routes or observed prefetch result
- Commit, abort, mismatch, or settled hooks

## Reviewer Focus

- Route pattern extraction, including dynamic and parallel routes
- Primary route ordering
- Link intent propagation and experimental gating

## Validation

- `pnpm build-all`
- `pnpm --filter=next build`
- App Router instrumentation client E2E: 15/15

<!-- NEXT_JS_LLM_PR -->

---------

Co-authored-by: Jude Gao <[email protected]>
2026-06-19 14:13:33 -04:00

53 lines
1.9 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
// The router transition instrumentation only emits the rich transition event
// (which carries `fromRoutes`) when the experimental flag is enabled. That code
// path is gated behind `process.env.__NEXT_INSTRUMENTATION_CLIENT_ROUTER_TRANSITION_EVENTS`,
// which is replaced with a literal at build time so the disabled path can be
// dead-code-eliminated. `fromRoutes` is the marker: in client runtime code it
// appears only inside that gated branch, and object keys survive minification.
describe('instrumentation client router transition events - dead code elimination', () => {
describe('when the experimental flag is enabled', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
nextConfig: {
experimental: { instrumentationClientRouterTransitionEvents: true },
},
})
if (skipped) return
it('keeps the transition event payload in the client bundle', async () => {
const $ = await next.render$('/')
const chunkContents = await Promise.all(
$('script[src]')
.toArray()
.map((el) => next.fetch($(el).attr('src')).then((res) => res.text()))
)
expect(
chunkContents.some((content) => content.includes('fromRoutes'))
).toBe(true)
})
})
describe('when the experimental flag is disabled', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
})
if (skipped) return
it('removes the transition event payload from the client bundle', async () => {
const $ = await next.render$('/')
const chunkContents = await Promise.all(
$('script[src]')
.toArray()
.map((el) => next.fetch($(el).attr('src')).then((res) => res.text()))
)
expect(
chunkContents.some((content) => content.includes('fromRoutes'))
).toBe(false)
})
})
})