What
With span streaming enabled, a router.back() or router.forward() navigation in the Next.js app router produces a navigation span with navigation.type: 'browser.popstate' instead of router.back / router.forward.
Under traceLifecycle: 'static' the same navigations are tagged router.back / router.forward.
Evidence
Observed in the nextjs-app-dir E2E app (Next 14.2.35, production build) while porting it to span streaming in #23905. The streamed event dump for a full run contains no router.back, router.forward or router.traverse value at all — only browser.popstate (6), router.push (7) and router.replace (2). The corresponding navigation spans:
navigation /navigation/:param/router-back navigation.type=browser.popstate
navigation /navigation/:param/router-push navigation.type=browser.popstate (router.forward() case)
The transaction-based versions of these two tests pass on develop today, so this is specific to streaming.
Likely mechanism
In packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts, router-patch mode (which is what Next 14 uses, since it has no onRouterTransitionStart hook) starts a navigation span tagged router.back/router.forward with the placeholder name INCOMPLETE_APP_ROUTER_INSTRUMENTATION_TRANSACTION_NAME, because back()/forward() carry no href.
The popstate listener is then supposed to rename that same span:
if (currentRouterPatchingNavigationSpanRef.current?.isRecording()) {
currentRouterPatchingNavigationSpanRef.current.updateName(spanName);
// ... does not touch navigation.type
} else {
// creates a NEW span tagged 'browser.popstate'
}
Under streaming the ref appears to no longer be recording when popstate fires, so the else branch creates a second span tagged browser.popstate, and the original placeholder-named span is filtered out by the ignoreSpans entry that matches the placeholder name. The net effect is that the back/forward distinction is lost.
Impact
navigation.type no longer distinguishes back/forward from a plain popstate under streaming, for app router apps in router-patch mode.
Status
The two affected E2E tests (Creates a navigation span for \router.back()`and... `router.forward()``) are skipped in #23905 with a pointer to this issue, rather than having their expectations relaxed.
What
With span streaming enabled, a
router.back()orrouter.forward()navigation in the Next.js app router produces a navigation span withnavigation.type: 'browser.popstate'instead ofrouter.back/router.forward.Under
traceLifecycle: 'static'the same navigations are taggedrouter.back/router.forward.Evidence
Observed in the
nextjs-app-dirE2E app (Next 14.2.35, production build) while porting it to span streaming in #23905. The streamed event dump for a full run contains norouter.back,router.forwardorrouter.traversevalue at all — onlybrowser.popstate(6),router.push(7) androuter.replace(2). The corresponding navigation spans:The transaction-based versions of these two tests pass on
developtoday, so this is specific to streaming.Likely mechanism
In
packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts,router-patchmode (which is what Next 14 uses, since it has noonRouterTransitionStarthook) starts a navigation span taggedrouter.back/router.forwardwith the placeholder nameINCOMPLETE_APP_ROUTER_INSTRUMENTATION_TRANSACTION_NAME, becauseback()/forward()carry no href.The
popstatelistener is then supposed to rename that same span:Under streaming the ref appears to no longer be recording when
popstatefires, so theelsebranch creates a second span taggedbrowser.popstate, and the original placeholder-named span is filtered out by theignoreSpansentry that matches the placeholder name. The net effect is that the back/forward distinction is lost.Impact
navigation.typeno longer distinguishes back/forward from a plain popstate under streaming, for app router apps inrouter-patchmode.Status
The two affected E2E tests (
Creates a navigation span for \router.back()`and... `router.forward()``) are skipped in #23905 with a pointer to this issue, rather than having their expectations relaxed.