diff --git a/lerna.json b/lerna.json index bf4542af2ed87..e7a2e2f6867bb 100644 --- a/lerna.json +++ b/lerna.json @@ -15,5 +15,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "16.2.0-canary.51" + "version": "16.2.0-canary.52" } \ No newline at end of file diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 6e8f2a87199cd..dc51bfbcddcc5 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index c1a094d141233..a4a5b72034b25 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "description": "ESLint configuration used by Next.js.", "license": "MIT", "repository": { @@ -12,7 +12,7 @@ "dist" ], "dependencies": { - "@next/eslint-plugin-next": "16.2.0-canary.51", + "@next/eslint-plugin-next": "16.2.0-canary.52", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index 1869a39ea362a..74055261c2d6e 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,7 +1,7 @@ { "name": "@next/eslint-plugin-internal", "private": true, - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "description": "ESLint plugin for working on Next.js.", "exports": { ".": "./src/eslint-plugin-internal.js" diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 4b58cec2d4af6..15c16f1cbfb66 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/font/package.json b/packages/font/package.json index b074617fb0144..b5971f0ef79bb 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,7 +1,7 @@ { "name": "@next/font", "private": true, - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 44646ea69a133..0e4be7a41f688 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index f289b2daa5868..bd125f804d45c 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 5f99d6e2dc112..e149952b26f43 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index b294c5ce030fa..d0192f8478537 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index b93add3a54ee5..618ac1cb54422 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index add0540f85125..e5d358a5813e5 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index a7ec189017ff6..a251cb0711ab8 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-routing/package.json b/packages/next-routing/package.json index b4f4070f8e60a..0540427903b15 100644 --- a/packages/next-routing/package.json +++ b/packages/next-routing/package.json @@ -1,6 +1,6 @@ { "name": "@next/routing", - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "keywords": [ "react", "next", diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json index c6eb114dc4a14..4343d61131351 100644 --- a/packages/next-rspack/package.json +++ b/packages/next-rspack/package.json @@ -1,6 +1,6 @@ { "name": "next-rspack", - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "repository": { "url": "vercel/next.js", "directory": "packages/next-rspack" diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index d321f237c5093..56bbfe6ebc245 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "private": true, "files": [ "native/" diff --git a/packages/next/package.json b/packages/next/package.json index 79b1ac26db301..6a98255b874f9 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -97,7 +97,7 @@ ] }, "dependencies": { - "@next/env": "16.2.0-canary.51", + "@next/env": "16.2.0-canary.52", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -162,11 +162,11 @@ "@modelcontextprotocol/sdk": "1.18.1", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/font": "16.2.0-canary.51", - "@next/polyfill-module": "16.2.0-canary.51", - "@next/polyfill-nomodule": "16.2.0-canary.51", - "@next/react-refresh-utils": "16.2.0-canary.51", - "@next/swc": "16.2.0-canary.51", + "@next/font": "16.2.0-canary.52", + "@next/polyfill-module": "16.2.0-canary.52", + "@next/polyfill-nomodule": "16.2.0-canary.52", + "@next/react-refresh-utils": "16.2.0-canary.52", + "@next/swc": "16.2.0-canary.52", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.51.1", "@rspack/core": "1.6.7", diff --git a/packages/next/src/build/templates/app-page.ts b/packages/next/src/build/templates/app-page.ts index f11cc78bbf0db..ee73afb484533 100644 --- a/packages/next/src/build/templates/app-page.ts +++ b/packages/next/src/build/templates/app-page.ts @@ -527,6 +527,9 @@ export async function handler( const method = req.method || 'GET' const tracer = getTracer() const activeSpan = tracer.getActiveScopeSpan() + const isWrappedByNextServer = Boolean( + routerServerContext?.isWrappedByNextServer + ) const render404 = async () => { // TODO: should route-module itself handle rendering the 404 @@ -1544,22 +1547,26 @@ export async function handler( // TODO: activeSpan code path is for when wrapped by // next-server can be removed when this is no longer used - if (activeSpan) { + if (isWrappedByNextServer && activeSpan) { await handleResponse(activeSpan) } else { - return await tracer.withPropagatedContext(req.headers, () => - tracer.trace( - BaseServerSpan.handleRequest, - { - spanName: `${method} ${srcPage}`, - kind: SpanKind.SERVER, - attributes: { - 'http.method': method, - 'http.target': req.url, + return await tracer.withPropagatedContext( + req.headers, + () => + tracer.trace( + BaseServerSpan.handleRequest, + { + spanName: `${method} ${srcPage}`, + kind: SpanKind.SERVER, + attributes: { + 'http.method': method, + 'http.target': req.url, + }, }, - }, - handleResponse - ) + handleResponse + ), + undefined, + !isWrappedByNextServer ) } } catch (err) { diff --git a/packages/next/src/build/templates/app-route.ts b/packages/next/src/build/templates/app-route.ts index e88c6d8e27fb4..966f29cc12099 100644 --- a/packages/next/src/build/templates/app-route.ts +++ b/packages/next/src/build/templates/app-route.ts @@ -203,6 +203,9 @@ export async function handler( const method = req.method || 'GET' const tracer = getTracer() const activeSpan = tracer.getActiveScopeSpan() + const isWrappedByNextServer = Boolean( + routerServerContext?.isWrappedByNextServer + ) const isMinimalMode = Boolean(getRequestMeta(req, 'minimalMode')) const incrementalCache = @@ -491,22 +494,26 @@ export async function handler( // TODO: activeSpan code path is for when wrapped by // next-server can be removed when this is no longer used - if (activeSpan) { + if (isWrappedByNextServer && activeSpan) { await handleResponse(activeSpan) } else { - await tracer.withPropagatedContext(req.headers, () => - tracer.trace( - BaseServerSpan.handleRequest, - { - spanName: `${method} ${srcPage}`, - kind: SpanKind.SERVER, - attributes: { - 'http.method': method, - 'http.target': req.url, + await tracer.withPropagatedContext( + req.headers, + () => + tracer.trace( + BaseServerSpan.handleRequest, + { + spanName: `${method} ${srcPage}`, + kind: SpanKind.SERVER, + attributes: { + 'http.method': method, + 'http.target': req.url, + }, }, - }, - handleResponse - ) + handleResponse + ), + undefined, + !isWrappedByNextServer ) } } catch (err) { diff --git a/packages/next/src/build/templates/pages-api.ts b/packages/next/src/build/templates/pages-api.ts index 08915e0d21063..9a647224893f3 100644 --- a/packages/next/src/build/templates/pages-api.ts +++ b/packages/next/src/build/templates/pages-api.ts @@ -80,6 +80,9 @@ export async function handler( const tracer = getTracer() const activeSpan = tracer.getActiveScopeSpan() + const isWrappedByNextServer = Boolean( + routerServerContext?.isWrappedByNextServer + ) const onRequestError = routeModule.instrumentationOnRequestError.bind(routeModule) @@ -151,22 +154,26 @@ export async function handler( // TODO: activeSpan code path is for when wrapped by // next-server can be removed when this is no longer used - if (activeSpan) { + if (isWrappedByNextServer && activeSpan) { await invokeRouteModule(activeSpan) } else { - await tracer.withPropagatedContext(req.headers, () => - tracer.trace( - BaseServerSpan.handleRequest, - { - spanName: `${method} ${srcPage}`, - kind: SpanKind.SERVER, - attributes: { - 'http.method': method, - 'http.target': req.url, + await tracer.withPropagatedContext( + req.headers, + () => + tracer.trace( + BaseServerSpan.handleRequest, + { + spanName: `${method} ${srcPage}`, + kind: SpanKind.SERVER, + attributes: { + 'http.method': method, + 'http.target': req.url, + }, }, - }, - invokeRouteModule - ) + invokeRouteModule + ), + undefined, + !isWrappedByNextServer ) } } catch (err) { diff --git a/packages/next/src/client/components/app-router.tsx b/packages/next/src/client/components/app-router.tsx index 689b8157e9770..a7b1c48442079 100644 --- a/packages/next/src/client/components/app-router.tsx +++ b/packages/next/src/client/components/app-router.tsx @@ -31,7 +31,10 @@ import { findHeadInCache } from './router-reducer/reducers/find-head-in-cache' import { unresolvedThenable } from './unresolved-thenable' import { removeBasePath } from '../remove-base-path' import { hasBasePath } from '../has-base-path' -import { getSelectedParams } from './router-reducer/compute-changed-path' +import { + extractSourcePageFromFlightRouterState, + getSelectedParams, +} from './router-reducer/compute-changed-path' import { useNavFailureHandler } from './nav-failure-handler' import { dispatchTraverseAction, @@ -191,6 +194,16 @@ function Router({ }, [cache, tree]) } + useEffect(() => { + const sourcePage = extractSourcePageFromFlightRouterState(state.tree) + + if (sourcePage !== undefined) { + window.next.__internal_src_page = sourcePage + } else { + delete window.next.__internal_src_page + } + }, [state.tree]) + useEffect(() => { // If the app is restored from bfcache, it's possible that // pushRef.mpaNavigation is true, which would mean that any re-render of this component diff --git a/packages/next/src/client/components/router-reducer/compute-changed-path.ts b/packages/next/src/client/components/router-reducer/compute-changed-path.ts index 9f689299a9530..954b185daa7d3 100644 --- a/packages/next/src/client/components/router-reducer/compute-changed-path.ts +++ b/packages/next/src/client/components/router-reducer/compute-changed-path.ts @@ -27,6 +27,44 @@ const segmentToPathname = (segment: Segment): string => { return segment[1] } +const segmentToSourcePagePathname = (segment: Segment): string => { + if (typeof segment === 'string') { + if (segment === 'children') return '' + if (segment.startsWith(PAGE_SEGMENT_KEY)) return 'page' + return segment + } + + const [paramName, , dynamicParamType] = segment + + switch (dynamicParamType) { + case 'c': + return `[...${paramName}]` + case 'ci(..)(..)': + return `(..)(..)[...${paramName}]` + case 'ci(.)': + return `(.)[...${paramName}]` + case 'ci(..)': + return `(..)[...${paramName}]` + case 'ci(...)': + return `(...)[...${paramName}]` + case 'oc': + return `[[...${paramName}]]` + case 'd': + return `[${paramName}]` + case 'di(..)(..)': + return `(..)(..)[${paramName}]` + case 'di(.)': + return `(.)[${paramName}]` + case 'di(..)': + return `(..)[${paramName}]` + case 'di(...)': + return `(...)[${paramName}]` + default: + dynamicParamType satisfies never + return `[${paramName}]` + } +} + function normalizeSegments(segments: string[]): string { return ( segments.reduce((acc, segment) => { @@ -79,6 +117,55 @@ export function extractPathFromFlightRouterState( return normalizeSegments(segments) } +function extractSourcePageSegmentsFromFlightRouterState( + flightRouterState: FlightRouterState +): string[] | undefined { + const segment = segmentToSourcePagePathname(flightRouterState[0]) + + if (segment === DEFAULT_SEGMENT_KEY) { + return undefined + } + + if (segment === 'page') { + return [segment] + } + + const parallelRoutes = flightRouterState[1] ?? {} + + const childrenPath = parallelRoutes.children + ? extractSourcePageSegmentsFromFlightRouterState(parallelRoutes.children) + : undefined + + if (childrenPath !== undefined) { + return segment === '' + ? childrenPath + : [removeLeadingSlash(segment), ...childrenPath] + } + + for (const [key, value] of Object.entries(parallelRoutes)) { + if (key === 'children') continue + + const childPath = extractSourcePageSegmentsFromFlightRouterState(value) + + if (childPath !== undefined) { + return segment === '' + ? childPath + : [removeLeadingSlash(segment), ...childPath] + } + } + + return undefined +} + +export function extractSourcePageFromFlightRouterState( + flightRouterState: FlightRouterState +): string | undefined { + const sourcePageSegments = + extractSourcePageSegmentsFromFlightRouterState(flightRouterState) + + return sourcePageSegments ? `/${sourcePageSegments.join('/')}` : undefined +} + function computeChangedPathImpl( treeA: FlightRouterState, treeB: FlightRouterState diff --git a/packages/next/src/server/lib/router-utils/router-server-context.ts b/packages/next/src/server/lib/router-utils/router-server-context.ts index 80a7365495e81..fef818d13f746 100644 --- a/packages/next/src/server/lib/router-utils/router-server-context.ts +++ b/packages/next/src/server/lib/router-utils/router-server-context.ts @@ -49,6 +49,8 @@ export type RouterServerContext = Record< errorsRscStream: ReadableStream, htmlRequestId: string ) => void + // indicates request handlers are already wrapped by next-server tracing + isWrappedByNextServer?: boolean } > diff --git a/packages/next/src/server/lib/trace/tracer.test.ts b/packages/next/src/server/lib/trace/tracer.test.ts new file mode 100644 index 0000000000000..8bb60e5eeda2c --- /dev/null +++ b/packages/next/src/server/lib/trace/tracer.test.ts @@ -0,0 +1,138 @@ +/** + * @jest-environment node + */ + +import type { + Context, + ContextManager, + TextMapGetter, + TextMapPropagator, +} from '@opentelemetry/api' +import { + ROOT_CONTEXT, + context, + createContextKey, + propagation, + trace, +} from '@opentelemetry/api' + +import { getTracer } from './tracer' + +const customContextKey = createContextKey('next.tracer.test.custom-context') + +const getter: TextMapGetter> = { + keys: (carrier) => Object.keys(carrier), + get: (carrier, key) => carrier[key], +} + +class TestContextManager implements ContextManager { + private currentContext: Context = ROOT_CONTEXT + + active(): Context { + return this.currentContext + } + + with ReturnType>( + newContext: Context, + fn: F, + thisArg?: ThisParameterType, + ...args: A + ): ReturnType { + const previousContext = this.currentContext + this.currentContext = newContext + try { + return fn.apply(thisArg, args) + } finally { + this.currentContext = previousContext + } + } + + bind(bindContext: Context, target: T): T { + if (typeof target !== 'function') { + return target + } + + return ((...args: unknown[]) => { + return this.with( + bindContext, + target as (...args: unknown[]) => unknown, + undefined, + ...args + ) + }) as T + } + + enable(): this { + return this + } + + disable(): this { + this.currentContext = ROOT_CONTEXT + return this + } +} + +class CustomPropagator implements TextMapPropagator { + fields(): string[] { + return ['x-custom'] + } + + inject(): void {} + + extract( + extractedContext: Context, + carrier: Record, + mapGetter: TextMapGetter> + ): Context { + const value = mapGetter.get(carrier, 'x-custom') + if (!value || Array.isArray(value)) { + return extractedContext + } + + return extractedContext.setValue(customContextKey, value) + } +} + +describe('withPropagatedContext', () => { + beforeEach(() => { + context.disable() + propagation.disable() + context.setGlobalContextManager(new TestContextManager()) + propagation.setGlobalPropagator(new CustomPropagator()) + }) + + afterEach(() => { + propagation.disable() + context.disable() + }) + + it('merges extracted context in force mode when no remote span exists', () => { + const activeSpan = trace.wrapSpanContext({ + traceId: '0123456789abcdef0123456789abcdef', + spanId: '0123456789abcdef', + traceFlags: 1, + isRemote: false, + }) + const activeContext = trace.setSpan(ROOT_CONTEXT, activeSpan) + + const result = context.with(activeContext, () => + getTracer().withPropagatedContext( + { 'x-custom': 'custom1' }, + () => { + const scopedContext = context.active() + return { + customValue: scopedContext.getValue(customContextKey), + activeSpanId: trace.getSpanContext(scopedContext)?.spanId, + } + }, + getter, + true + ) + ) + + expect(result).toEqual({ + customValue: 'custom1', + activeSpanId: '0123456789abcdef', + }) + }) +}) diff --git a/packages/next/src/server/lib/trace/tracer.ts b/packages/next/src/server/lib/trace/tracer.ts index e6e1ec933497e..cff723be0f41d 100644 --- a/packages/next/src/server/lib/trace/tracer.ts +++ b/packages/next/src/server/lib/trace/tracer.ts @@ -227,13 +227,29 @@ class NextTracerImpl implements NextTracer { public withPropagatedContext( carrier: C, fn: () => T, - getter?: TextMapGetter + getter?: TextMapGetter, + force = false ): T { const activeContext = context.active() + + if (force) { + const remoteContext = propagation.extract(ROOT_CONTEXT, carrier, getter) + + if (trace.getSpanContext(remoteContext)) { + return context.with(remoteContext, fn) + } + + // Preserve the current active span while still merging any extracted + // baggage/context values from the carrier. + const mergedContext = propagation.extract(activeContext, carrier, getter) + return context.with(mergedContext, fn) + } + if (trace.getSpanContext(activeContext)) { // Active span is already set, too late to propagate. return fn() } + const remoteContext = propagation.extract(activeContext, carrier, getter) return context.with(remoteContext, fn) } diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 26b8f609a305c..2493d1733d6b1 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -1060,6 +1060,9 @@ export default class NextNodeServer extends BaseServer< routerServerGlobal[RouterServerContextSymbol][ relativeProjectDir ].nextConfig = this.nextConfig + routerServerGlobal[RouterServerContextSymbol][ + relativeProjectDir + ].isWrappedByNextServer = true try { // next.js core assumes page path without trailing slash diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index efa496713e54b..6645a8691e80e 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index 32708d0716773..f0ee93a605e3e 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "16.2.0-canary.51", + "version": "16.2.0-canary.52", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "16.2.0-canary.51", + "next": "16.2.0-canary.52", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "5.9.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d75d62d3b311f..d667f9303b5cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1011,7 +1011,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 16.2.0-canary.51 + specifier: 16.2.0-canary.52 version: link:../eslint-plugin-next eslint: specifier: '>=9.0.0' @@ -1088,7 +1088,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 16.2.0-canary.51 + specifier: 16.2.0-canary.52 version: link:../next-env '@swc/helpers': specifier: 0.5.15 @@ -1216,19 +1216,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 16.2.0-canary.51 + specifier: 16.2.0-canary.52 version: link:../font '@next/polyfill-module': - specifier: 16.2.0-canary.51 + specifier: 16.2.0-canary.52 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 16.2.0-canary.51 + specifier: 16.2.0-canary.52 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 16.2.0-canary.51 + specifier: 16.2.0-canary.52 version: link:../react-refresh-utils '@next/swc': - specifier: 16.2.0-canary.51 + specifier: 16.2.0-canary.52 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1943,7 +1943,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 16.2.0-canary.51 + specifier: 16.2.0-canary.52 version: link:../next outdent: specifier: 0.8.0 diff --git a/test/e2e/app-dir/app/index.test.ts b/test/e2e/app-dir/app/index.test.ts index 85712b53b669c..b1460adcb32c3 100644 --- a/test/e2e/app-dir/app/index.test.ts +++ b/test/e2e/app-dir/app/index.test.ts @@ -145,6 +145,56 @@ describe('app dir - basic', () => { }) }) + it.each([ + { + path: '/dashboard', + srcPage: '/dashboard/page', + }, + { + path: '/dynamic/category-1/id-2', + srcPage: '/dynamic/[category]/[id]/page', + }, + { + path: '/dashboard/another', + srcPage: '/(newroot)/dashboard/another/page', + }, + ])( + 'should expose app source page on window.next.__internal_src_page for $path', + async ({ path, srcPage }) => { + const browser = await next.browser(path) + + await retry(async () => { + expect(await browser.eval('window.next.__internal_src_page')).toBe( + srcPage + ) + }) + } + ) + + it('should update window.next.__internal_src_page on app router transitions', async () => { + const browser = await next.browser('/dashboard') + + await retry(async () => { + expect(await browser.eval('window.next.__internal_src_page')).toBe( + '/dashboard/page' + ) + }) + + await browser.eval(`window.next.router.push('/dynamic/category-1/id-2')`) + await retry(async () => { + expect(await browser.eval('window.next.__internal_src_page')).toBe( + '/dynamic/[category]/[id]/page' + ) + }) + + await browser.eval(`window.next.router.push('/dashboard/another')`) + await retry(async () => { + expect(await browser.eval('window.next.__internal_src_page')).toBe( + '/(newroot)/dashboard/another/page' + ) + }) + }) + if (!isNextDev) { it('should successfully detect app route during prefetch', async () => { const browser = await next.browser('/') diff --git a/test/e2e/opentelemetry/instrumentation/custom-entrypoint-server.ts b/test/e2e/opentelemetry/instrumentation/custom-entrypoint-server.ts new file mode 100644 index 0000000000000..1fdb2c702d925 --- /dev/null +++ b/test/e2e/opentelemetry/instrumentation/custom-entrypoint-server.ts @@ -0,0 +1,62 @@ +import { createServer, type IncomingMessage, type ServerResponse } from 'http' +import path from 'path' +import getPort from 'get-port' +import { trace } from '@opentelemetry/api' + +import { register } from './instrumentation-custom-server' + +register() + +type EntrypointHandler = ( + req: IncomingMessage, + res: ServerResponse, + ctx: { + waitUntil?: (prom: Promise) => void + } +) => Promise + +async function main() { + const port = await getPort() + const hostname = 'localhost' + + require('next/dist/server/node-environment') + + const entrypointPath = path.join( + __dirname, + '.next', + 'server', + 'app', + 'app', + '[param]', + 'rsc-fetch', + 'page.js' + ) + const { handler } = require(entrypointPath) as { handler: EntrypointHandler } + + const tracer = trace.getTracer('custom-entrypoint-server', '1.0.0') + + createServer((req, res) => { + // Simulate a custom parent span around direct entrypoint invocation. + tracer.startActiveSpan('custom-entrypoint-request', async (span) => { + try { + await handler(req, res, { + waitUntil: () => {}, + }) + } catch (err) { + span.recordException(err as Error) + res.statusCode = 500 + res.end('Internal Server Error') + } finally { + span.end() + } + }) + }).listen(port, undefined, (err?: Error) => { + if (err) throw err + console.log(`- Local: http://${hostname}:${port}`) + }) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts b/test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts index db558ef799469..47538ae99ae8d 100644 --- a/test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts +++ b/test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts @@ -1418,6 +1418,66 @@ describe('opentelemetry with custom server', () => { }) }) +if (!isNextDev) { + describe('opentelemetry with direct entrypoint handler', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + dependencies: require('./package.json').dependencies, + startCommand: 'pnpm start-entrypoint', + packageJson: { + scripts: { + 'start-entrypoint': 'pnpm tsx custom-entrypoint-server.ts', + }, + }, + serverReadyPattern: /- Local:/, + env: { + TEST_OTEL_COLLECTOR_PORT: String(COLLECTOR_PORT), + NEXT_TELEMETRY_DISABLED: '1', + NODE_ENV: 'production', + }, + }) + + if (skipped) { + return + } + + let collector: Collector + + function getCollector(): Collector { + return collector + } + + beforeEach(async () => { + collector = await connectCollector({ port: COLLECTOR_PORT }) + }) + + afterEach(async () => { + await collector.shutdown() + }) + + it('should propagate incoming context without next-server wrapper', async () => { + await next.fetch('/app/param/rsc-fetch', { + headers: { + traceparent: `00-${EXTERNAL.traceId}-${EXTERNAL.spanId}-01`, + }, + }) + + await expectTrace(getCollector(), [ + { + name: 'GET /app/[param]/rsc-fetch/page', + traceId: EXTERNAL.traceId, + parentId: EXTERNAL.spanId, + attributes: { + 'http.target': '/app/param/rsc-fetch', + 'next.span_type': 'BaseServer.handleRequest', + }, + }, + ]) + }) + }) +} + type HierSavedSpan = SavedSpan & { spans?: HierSavedSpan[] } type SpanMatch = Omit, 'spans'> & { spans?: SpanMatch[] }