router instrumentation: refactor client hook dispatch (1/7) (#94755)
## Stack Position
(1/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)
## What This PR Does
Refactors client instrumentation loading so webpack and Turbopack
provide an ordered array of instrumentation modules to a shared
router-transition dispatcher.
The existing config remains unchanged:
```ts
const nextConfig = {
instrumentationClientInject: ['my-observability-library/client'],
}
```
## Motivation
This refactor lands first, ahead of the new lifecycle events, for two
reasons:
1. **Maintainability — the stack adds more hooks.** Previously the hook
fan-out was generated at build time (a composed
`onRouterTransitionStart` emitted by both the webpack loader and the
Turbopack import map). Each new event (commit, settled, abort, mismatch)
would require regenerating that composition in both bundlers, per hook.
Moving dispatch into one runtime module means new hooks are added once,
in a single place, with no new bundler composition logic per hook.
2. **Fault tolerance.** Dispatch now isolates failures across
instrumentation modules:
- A hook that throws is caught per module, so the remaining modules
still run for that navigation (previously, one throwing hook aborted the
rest).
- Modules whose exports are `null`/`undefined` (side-effect-only) are
filtered out instead of throwing (previously an unguarded existence
check could throw a `TypeError` and break hydration).
## Design
- Injected modules run in array order before the application
instrumentation file.
- With no injected modules, bundlers preserve the direct user
instrumentation alias and the runtime normalizes it to a one-element
array.
- Hooks are read from each module at dispatch time.
- A failing hook does not prevent later modules from running.
- Side-effect-only modules may export `null` or `undefined`.
- The dispatcher can support additional hooks without generating new
bundler composition logic for each hook.
## Not In This PR
- New event arguments or lifecycle hooks
- Transition IDs, timestamps, route data, or prefetch data
## Reviewer Focus
- Webpack and Turbopack produce the same ordered module array.
- Hook fan-out stays in the router-transition dispatcher.
- Ordering, dynamic hook lookup, and per-module error isolation are
preserved.
## Validation
- `pnpm --filter=next types`
- `cargo fmt --all -- --check`
- Instrumentation client E2E coverage in dev and production with webpack
and Turbopack
- Cache Components instrumentation client coverage
<!-- NEXT_JS_LLM_PR -->
Co-authored-by: Jude Gao <jude.gao@vercel.com>