-
Notifications
You must be signed in to change notification settings - Fork 29.9k
Description
Link to the code that reproduces this issue
https://github.com/eli0shin/next-js-custom-server-repro
To Reproduce
- build the server
- run npm run start to run nextjs with the custom server
- open the page in your browser
- the otel spans will be logged in the console. the handleRequest span is missing the http.route and next.route attributes
- stop the server
- start the regular server with npm run start:regular
- open the page in your browser
- the spans are logged to the console. the http.route and next.route attributes are on the handleRequest span
Current vs. Expected behavior
Current
When a custom server is used no span contains the http.route attribute for the matched nextjs route
Expected
When a custom server is used the handleRequest route contains the http.route attribute for the matched route
Provide environment information
This behavior is entirely independent of the environment.Which area(s) are affected? (Select all that apply)
Instrumentation
Which stage(s) are affected? (Select all that apply)
Other (Deployed)
Additional context
When Next.js is used with a custom server that creates its own root span, Next.js still creates its own handleRequest span, but it fails to propagate attributes (like next.route and http.route) to the span that it creates because the attribute propagation mechanism requires isRootSpan = true.
The impact of this issue is that no server span in the trace contains the http.route attribute breaking trace visibility and trace derived metrics. Next.js is the router in this case and there is no way to set the matched route on either the next.js server span or the custom server's server span.
The Actual Issue
Next.js creates the handleRequest span at packages/next/src/server/base-server.ts:869-871:
return tracer.withPropagatedContext(req.headers, () => {
return tracer.trace(BaseServerSpan.handleRequest, {...}, async (span) => {
// This span IS created, but...However, the rootSpanAttributesStore is only initialized when isRootSpan = true at packages/next/src/server/lib/trace/tracer.ts:337-347:
if (isRootSpan) {
rootSpanAttributesStore.set(spanId, new Map(...))
}With a custom server parent span:
isRootSpan = false(because there's a parent context)- No entry is added to
rootSpanAttributesStorefor this spanId - The spanId is never stored in the context via
rootSpanIdKey
Root Span Detection Logic
packages/next/src/server/lib/trace/tracer.ts:284-295:
let spanContext = this.getSpanContext(
options?.parentSpan ?? this.getActiveScopeSpan()
)
let isRootSpan = false
if (!spanContext) {
spanContext = context?.active() ?? ROOT_CONTEXT
isRootSpan = true
} else if (trace.getSpanContext(spanContext)?.isRemote) {
isRootSpan = true
}When a custom server propagates its span:
- Next.js extracts the remote context via
withPropagatedContext()atpackages/next/src/server/base-server.ts:867 spanContextis NOT falsy because there's a parent span from the custom server- The
isRemotecheck may not properly identify the extracted context, OR the span is not marked as remote isRootSpanremainsfalse
Why Attribute Propagation Fails
When child spans call setRootSpanAttribute('next.route', pathname) at these locations:
packages/next/src/server/base-server.ts:2458packages/next/src/server/render.tsx:1427packages/next/src/server/app-render/app-render.tsx:1710packages/next/src/server/route-modules/app-route/module.ts:781packages/next/src/server/api-utils/index.ts:77
They try to retrieve the root span ID from context at packages/next/src/server/lib/trace/tracer.ts:456-462:
public setRootSpanAttribute(key: AttributeNames, value: AttributeValue) {
const spanId = context.active().getValue(rootSpanIdKey) as number // undefined!
const attributes = rootSpanAttributesStore.get(spanId) // store has no entry
if (attributes && !attributes.has(key)) {
attributes.set(key, value)
}
}Since rootSpanIdKey was never set in the context (only happens at packages/next/src/server/lib/trace/tracer.ts:305 when isRootSpan = true), and the store has no entry for this span, the attributes can't be propagated to the Next.js span.
Failed Attribute Retrieval
When the request completes, packages/next/src/server/base-server.ts:898-899 tries to retrieve the attributes:
const rootSpanAttributes = tracer.getRootSpanAttributes()
if (!rootSpanAttributes) return // Returns early, route never gets setgetRootSpanAttributes() at packages/next/src/server/lib/trace/tracer.ts:451-454:
public getRootSpanAttributes() {
const spanId = context.active().getValue(rootSpanIdKey) as number // undefined!
return rootSpanAttributesStore.get(spanId) // returns undefined
}Root Cause
Next.js conflates "root span" (no parent) with "span where we should store propagated attributes". With a custom server:
- Next.js's
handleRequestspan has a parent (the custom server's span) - So it's not treated as a root span (
isRootSpan = false) - But it still needs the attribute store initialized to collect route information from child spans
- The attributes should be going to Next.js's
handleRequestspan, but the propagation mechanism is disabled because that span isn't flagged as a root span
Impact
The Next.js handleRequest span is created but lacks critical attributes:
next.routeis not sethttp.routeis not set- Span name remains generic (e.g.,
GETinstead ofGET /api/users) - Observability is significantly degraded and breaks trace metrics in providers like Datadog