Repository navigation
Emit instrumentation lifecycle spans in the request that registered it - #100350
Merged
mischnic merged 7 commits intoOct 21, 2026
Conversation
Contributor
Tests PassedCommit: c8bff0e |
sokra
force-pushed
the
fleet/otel-instrumentation-spans-under-request
branch
from
October 19, 2026 11:26
f566475 to
10d5128
Compare
mischnic
force-pushed
the
fleet/otel-instrumentation-spans-under-request
branch
from
October 19, 2026 15:21
10d5128 to
6754551
Compare
sokra
force-pushed
the
fleet/otel-instrumentation-spans-under-request
branch
from
October 20, 2026 10:51
6754551 to
211f4eb
Compare
sokra
force-pushed
the
fleet/otel-instrumentation-spans-under-request
branch
from
October 20, 2026 11:20
211f4eb to
c03855f
Compare
sokra
commented
Oct 20, 2026
sokra
force-pushed
the
fleet/otel-instrumentation-spans-under-request
branch
from
October 20, 2026 13:35
c03855f to
1cca382
Compare
sokra
force-pushed
the
fleet/otel-instrumentation-spans-under-request
branch
from
October 20, 2026 14:04
1cca382 to
bb48172
Compare
sokra
commented
Oct 20, 2026
sokra
force-pushed
the
fleet/otel-instrumentation-spans-under-request
branch
from
October 20, 2026 16:12
bb48172 to
8121501
Compare
sokra
force-pushed
the
fleet/otel-instrumentation-spans-under-request
branch
from
October 20, 2026 16:36
8121501 to
b7a797e
Compare
sokra
force-pushed
the
fleet/otel-instrumentation-spans-under-request
branch
from
October 20, 2026 22:41
b7a797e to
cf1a8f9
Compare
sokra
force-pushed
the
fleet/otel-instrumentation-spans-under-request
branch
from
October 21, 2026 13:33
4abe217 to
9df28d3
Compare
Member
Author
|
Reviewed 9df28d3 |
sokra
marked this pull request as ready for review
October 21, 2026 14:40
Direct entrypoints register instrumentation.ts lazily in the first request. Its backdated lifecycle spans (load instrumentation module, register instrumentation) were separate root traces. Defer them to the request that triggered registration and emit them as children of its request span, or, for Node.js middleware, in the shared parent / inserted Middleware.request root. Startup registration, errors and Edge are unchanged. <!-- NEXT_JS_LLM --> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
Assert the recorded lifecycle spans and their ancestors as a span tree, and include them in the first request's tree. Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
sokra
force-pushed
the
fleet/otel-instrumentation-spans-under-request
branch
from
October 21, 2026 15:10
9df28d3 to
44566d8
Compare
When a Node.js middleware entrypoint registers instrumentation.ts, emit the lifecycle spans inside the middleware span and backdate it to cover registration. The middleware and render stay siblings. Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
mischnic
reviewed
Oct 21, 2026
mischnic
reviewed
Oct 21, 2026
mischnic
reviewed
Oct 21, 2026
…tionRegisteredWithDeferredSpans Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
mischnic
approved these changes
Oct 21, 2026
mischnic
added a commit
that referenced
this pull request
Oct 21, 2026
## Summary Bottom of a stack: this PR → #100294 → #100324 → #100350 → #100392. In direct entrypoints (Vercel, adapters, standalone handlers, i.e. without next-server), `prepare route module` ran before the request span started. Since 16.4 every request therefore produced an extra parentless `prepare route module` trace next to the request trace. Now it is part of the request span: ``` [P →] GET /page (BaseServer.handleRequest) └─ prepare route module → render … ``` ### How - New `RouteModule.traceRequest` (`@internal`, kept out of the published declaration types) starts the request span **before** `prepare`. It uses the caller's tracer. When the handler is wrapped by next-server, which already started the request span, it runs inside that active span instead. - Callers first await the new `RouteModule.ensureInstrumentation` (`@internal`), split out of `prepareImpl`: it registers `instrumentation.ts`, which usually installs the tracer provider, so a span started earlier would not be recorded. `prepareImpl` still calls it, and patches `res.setHeader` as before. - The app-page, app-route, pages and pages-api templates/handlers call `ensureInstrumentation` and then `traceRequest`, and handle the response through one code path: `traceRequest` passes the request span and the request's root span (the span that was active when the handler was invoked — next-server's request span or a platform/host span — or the request span itself), and the route name is propagated to the root span when it isn't the request span. - Parenting of the request span itself (incoming `traceparent`, host span, root) is unchanged. ## Verification - Collector suite `test/e2e/opentelemetry/instrumentation`: direct entrypoint cold and warm requests for all four handler types, with and without an incoming `traceparent` and with a host span, assert `RouteModule.prepare` is inside the request's trace, and route names on the request and parent spans. - New deploy-capable e2e suite `test/e2e/app-dir/otel-middleware-trace` (moved down from #100294): `proxy.ts` and Edge `middleware.ts` variants, plus a custom server with a host span. The app records its spans with a SpanProcessor; the middleware and the render report the spans they can see, and the test asserts the stitched span tree (with `http.target`, `http.route`, `next.route` and `http.status_code` where set at report time) in inline snapshots for pages, route handlers, rewrites, redirects, direct responses, matcher misses, incoming `traceparent`, header overrides and a middleware-set `traceparent`, in dev, start, Cache Components and deploy. Here it records the current behavior (middleware and render in separate traces); #100294 shows the change. - Unit tests for the tracer changes (`tracer.test.ts`). <!-- NEXT_JS_LLM --> <!-- fleet ce0ecb23-b14c-4679-92a9-e676130f618e --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
mischnic
pushed a commit
that referenced
this pull request
Oct 21, 2026
## Summary Bottom of a stack: this PR → #100294 → #100324 → #100350 → #100392. In direct entrypoints (Vercel, adapters, standalone handlers, i.e. without next-server), `prepare route module` ran before the request span started. Since 16.4 every request therefore produced an extra parentless `prepare route module` trace next to the request trace. Now it is part of the request span: ``` [P →] GET /page (BaseServer.handleRequest) └─ prepare route module → render … ``` ### How - New `RouteModule.traceRequest` (`@internal`, kept out of the published declaration types) starts the request span **before** `prepare`. It uses the caller's tracer. When the handler is wrapped by next-server, which already started the request span, it runs inside that active span instead. - Callers first await the new `RouteModule.ensureInstrumentation` (`@internal`), split out of `prepareImpl`: it registers `instrumentation.ts`, which usually installs the tracer provider, so a span started earlier would not be recorded. `prepareImpl` still calls it, and patches `res.setHeader` as before. - The app-page, app-route, pages and pages-api templates/handlers call `ensureInstrumentation` and then `traceRequest`, and handle the response through one code path: `traceRequest` passes the request span and the request's root span (the span that was active when the handler was invoked — next-server's request span or a platform/host span — or the request span itself), and the route name is propagated to the root span when it isn't the request span. - Parenting of the request span itself (incoming `traceparent`, host span, root) is unchanged. ## Verification - Collector suite `test/e2e/opentelemetry/instrumentation`: direct entrypoint cold and warm requests for all four handler types, with and without an incoming `traceparent` and with a host span, assert `RouteModule.prepare` is inside the request's trace, and route names on the request and parent spans. - New deploy-capable e2e suite `test/e2e/app-dir/otel-middleware-trace` (moved down from #100294): `proxy.ts` and Edge `middleware.ts` variants, plus a custom server with a host span. The app records its spans with a SpanProcessor; the middleware and the render report the spans they can see, and the test asserts the stitched span tree (with `http.target`, `http.route`, `next.route` and `http.status_code` where set at report time) in inline snapshots for pages, route handlers, rewrites, redirects, direct responses, matcher misses, incoming `traceparent`, header overrides and a middleware-set `traceparent`, in dev, start, Cache Components and deploy. Here it records the current behavior (middleware and render in separate traces); #100294 shows the change. - Unit tests for the tracer changes (`tracer.test.ts`). <!-- NEXT_JS_LLM --> <!-- fleet ce0ecb23-b14c-4679-92a9-e676130f618e --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> (cherry picked from commit e3c2f63) Co-authored-by: Tobias Koppers <tobias.koppers@googlemail.com>
mischnic
pushed a commit
that referenced
this pull request
Oct 21, 2026
#100350) ## Summary Stacked on #100324. Includes the commits from #100392, which were folded into this PR. Next.js emits two lifecycle spans for `instrumentation.ts` in the Node.js runtime: `load instrumentation module` and `register instrumentation`. They're backdated, because they're emitted after `register()` returns (`register()` usually installs the tracer provider). Until now they were always separate root traces. Direct entrypoints (Vercel, adapters, standalone handlers) register instrumentation lazily, in the first request. That split the first request's work across extra traces. Now the spans join the trace of the request that triggered registration: ``` route / page entrypoint: [P →] GET /page (BaseServer.handleRequest, backdated to cover registration) ├─ load instrumentation module ├─ register instrumentation └─ prepare route module → render … Node.js middleware entrypoint, no parent: GET (Next.request, backdated) ├─ middleware GET (backdated to cover registration) │ ├─ load instrumentation module │ └─ register instrumentation └─ GET /page Node.js middleware entrypoint with host span / incoming traceparent P: P ├─ middleware GET (backdated to cover registration) │ ├─ load instrumentation module │ └─ register instrumentation └─ GET /page ``` ### How - `ensureInstrumentationRegisteredWithDeferredSpans`: when this call starts registration and registration succeeds, the lifecycle spans are recorded instead of emitted. The function returns an idempotent emitter to that caller only. Concurrent callers, later callers and startup callers get nothing. - `RouteModule.ensureInstrumentation(req, { deferSpans: true })` is called by the templates before `traceRequest`. It records when the request started and returns that time, together with the emitter, as `instrumentation`. `traceRequest` takes it as an option, backdates the request span to that time, and emits the lifecycle spans as the span's first children. - The Node.js middleware template passes the emitter and the start time to the adapter. The adapter calls the emitter first thing inside the `Middleware.execute` span and backdates that span to the request start. - The shared parent (incoming `traceparent`, host span, or the backdated inserted `Next.request` root) is still captured before entering the middleware span. The middleware and the render stay siblings, and `traceparent` handling and forwarding are unchanged. - Unchanged: - `next start` / `next dev` startup registration (separate roots). - Registration errors: emitted immediately with the same statuses, including non-`Error` throws. - Edge (no lifecycle spans). - No `instrumentation.ts`, or no `register` export. - User-created spans in `instrumentation.ts` are out of scope (see #100324). ## Verification - Collector suite, cold direct entrypoints: - Route/page requests, with and without an incoming `traceparent`, and with a host span. - Node.js middleware requests: no parent, incoming `traceparent`, host span. - Each test asserts exactly one load span and one register span in the request's trace, parented as shown above and covered by the backdated parent. For middleware, the parent is the middleware span, and it starts no later than the lifecycle spans and no earlier than its own parent. - Direct-entrypoint single-trace assertions no longer exclude lifecycle spans as startup work. Under `next start`, startup spans stay roots. Fail-first verified. - Deploy-capable `test/e2e/app-dir/otel-middleware-trace`: span tree snapshots of the lifecycle spans with their ancestors, plus the first request's tree including them. - Deployed: children of the request span that triggered registration, or of `middleware GET` for Node.js middleware. - `next dev` / `next start`: startup roots. - No deploy gate. - Unit tests for the deferral helper: once-only, concurrency, startup ownership, no `register`, `register()` errors (including `throw undefined`), and module-load failure. <!-- NEXT_JS_LLM --> <!-- fleet ce0ecb23-b14c-4679-92a9-e676130f618e --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> (cherry picked from commit 65675c3) Co-authored-by: Tobias Koppers <tobias.koppers@googlemail.com>
mischnic
added a commit
that referenced
this pull request
Oct 22, 2026
…hrows (#100552) ### What? Direct Node.js middleware/proxy entrypoints now emit the deferred `instrumentation.ts` lifecycle spans (`load instrumentation module`, `register instrumentation`) even when the adapter rejects the request before it runs the middleware. ### Why? Follow-up to #100350. When a request to the Node.js middleware entrypoint triggers `instrumentation.ts` registration, the lifecycle spans are deferred and emitted inside the `Middleware.execute` span. If adapter setup throws first (e.g. the configured cache handler throws while the adapter builds the request's incremental cache), the emitter is never called. Registration has already completed, so no later request receives it either, and those spans are lost for the lifetime of the instance. This only affects direct entrypoints (adapters, deployments). `next start` / `next dev` register instrumentation at startup. ### How? - The middleware entrypoint template, which owns the deferred emitter, now awaits the adapter in a `try/catch`. On error it calls the emitter, then rethrows the original error unchanged. The adapter and its options are unchanged. - The emitter already emits at most once, so the fallback does nothing when the spans were already emitted inside `Middleware.execute` (e.g. the user's middleware threw). Successful requests keep their existing span parentage. In the fallback case there is no middleware span, so the spans are emitted in the caller's active context (or as roots). - The fallback call is guarded so that a failure while tracing can't replace the adapter's error. **Test:** this reuses the existing direct-entrypoint OpenTelemetry harness (`test/e2e/opentelemetry/instrumentation`, `next start` only). The harness calls the built middleware entrypoint's `handler` with a real `Request`, like the Vercel adapter does. - The fixture's custom cache handler throws from its constructor when a test-only request header is set. The adapter constructs it per request, before running the middleware. - A new suite, in a fresh process, sends that request through the normal middleware flow and asserts that: - the original error propagates; - both lifecycle spans are collected exactly once, with no `Middleware.execute` span; - a later valid request succeeds without emitting them again. - The test fails without the fix. ### Verification - New test plus the existing "should trace instrumentation registration in the first middleware request" variants: `pnpm test-start-turbo` and `pnpm test-start-webpack` on `test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts` - `pnpm jest packages/next/src/server/lib/router-utils/instrumentation-globals.test.ts` - `pnpm --filter=next types`, `pnpm typescript` <!-- NEXT_JS_LLM --> <!-- fleet bb16cbee-5cda-4702-b442-ea49bf648fe0 --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
mischnic
pushed a commit
that referenced
this pull request
Oct 22, 2026
…hrows (#100552) ### What? Direct Node.js middleware/proxy entrypoints now emit the deferred `instrumentation.ts` lifecycle spans (`load instrumentation module`, `register instrumentation`) even when the adapter rejects the request before it runs the middleware. ### Why? Follow-up to #100350. When a request to the Node.js middleware entrypoint triggers `instrumentation.ts` registration, the lifecycle spans are deferred and emitted inside the `Middleware.execute` span. If adapter setup throws first (e.g. the configured cache handler throws while the adapter builds the request's incremental cache), the emitter is never called. Registration has already completed, so no later request receives it either, and those spans are lost for the lifetime of the instance. This only affects direct entrypoints (adapters, deployments). `next start` / `next dev` register instrumentation at startup. ### How? - The middleware entrypoint template, which owns the deferred emitter, now awaits the adapter in a `try/catch`. On error it calls the emitter, then rethrows the original error unchanged. The adapter and its options are unchanged. - The emitter already emits at most once, so the fallback does nothing when the spans were already emitted inside `Middleware.execute` (e.g. the user's middleware threw). Successful requests keep their existing span parentage. In the fallback case there is no middleware span, so the spans are emitted in the caller's active context (or as roots). - The fallback call is guarded so that a failure while tracing can't replace the adapter's error. **Test:** this reuses the existing direct-entrypoint OpenTelemetry harness (`test/e2e/opentelemetry/instrumentation`, `next start` only). The harness calls the built middleware entrypoint's `handler` with a real `Request`, like the Vercel adapter does. - The fixture's custom cache handler throws from its constructor when a test-only request header is set. The adapter constructs it per request, before running the middleware. - A new suite, in a fresh process, sends that request through the normal middleware flow and asserts that: - the original error propagates; - both lifecycle spans are collected exactly once, with no `Middleware.execute` span; - a later valid request succeeds without emitting them again. - The test fails without the fix. ### Verification - New test plus the existing "should trace instrumentation registration in the first middleware request" variants: `pnpm test-start-turbo` and `pnpm test-start-webpack` on `test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts` - `pnpm jest packages/next/src/server/lib/router-utils/instrumentation-globals.test.ts` - `pnpm --filter=next types`, `pnpm typescript` <!-- NEXT_JS_LLM --> <!-- fleet bb16cbee-5cda-4702-b442-ea49bf648fe0 --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> (cherry picked from commit b0d0150) Co-authored-by: Tobias Koppers <tobias.koppers@googlemail.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
mischnic
pushed a commit
that referenced
this pull request
Oct 22, 2026
…0294, #100324, #100350, #100552, #100583) (#100542) Backports the OpenTelemetry request-trace stack to `releases/lts/active`: - #100349 Start the request span before route module preparation - #100294 Keep middleware and render in one OpenTelemetry trace as siblings (re-lands #100136, whose LTS backport #100226 was reverted in #100269) - #100324 docs: note that instrumentation-time spans don't join request traces - #100350 Emit instrumentation lifecycle spans in the request that registered it - #100552 Preserve deferred middleware instrumentation spans when the adapter throws - #100583 test: add non-adapter deploy snapshots to otel-middleware-trace. Without it, `otel-middleware-trace` fails the release deploy tests that deploy with `NEXT_ENABLE_ADAPTER=0`, as on v16.5.0-canary.23. The six canary merge commits are cherry-picked in order with `-x`, and all applied cleanly. The patch content is identical to canary's; only blob IDs and line offsets differ. ## Verification - `test/e2e/opentelemetry/instrumentation/`: `test-start-turbo` (122 passed) and `test-dev-turbo` (67 passed). The #100552 regression test fails without its `middleware.ts` change. - `test/e2e/app-dir/otel-middleware-trace`: `test-start-turbo`, `test-dev-turbo` and `test-start-webpack` (30 passed each, inline snapshots unchanged). The #100583 commit leaves the test file byte-identical to canary's. - Unit tests for `instrumentation-globals`, `tracer` and `forward-trace-context` (40 passed), plus `pnpm --filter=next types` - Built with `CI=true pnpm build-all` against the published `@next/swc` 16.4.2 binary. The backport touches no Rust. - PR CI deploys only through the adapter, so the non-adapter snapshots from #100583 are confirmed by the next release deploy run. <!-- NEXT_JS_LLM --> <!-- fleet ce0ecb23-b14c-4679-92a9-e676130f618e --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacked on #100324. Includes the commits from #100392, which were folded into this PR.
Next.js emits two lifecycle spans for
instrumentation.tsin the Node.js runtime:load instrumentation moduleandregister instrumentation. They're backdated, because they're emitted afterregister()returns (register()usually installs the tracer provider). Until now they were always separate root traces.Direct entrypoints (Vercel, adapters, standalone handlers) register instrumentation lazily, in the first request. That split the first request's work across extra traces. Now the spans join the trace of the request that triggered registration:
How
ensureInstrumentationRegisteredWithDeferredSpans: when this call starts registration and registration succeeds, the lifecycle spans are recorded instead of emitted. The function returns an idempotent emitter to that caller only. Concurrent callers, later callers and startup callers get nothing.RouteModule.ensureInstrumentation(req, { deferSpans: true })is called by the templates beforetraceRequest. It records when the request started and returns that time, together with the emitter, asinstrumentation.traceRequesttakes it as an option, backdates the request span to that time, and emits the lifecycle spans as the span's first children.Middleware.executespan and backdates that span to the request start.traceparent, host span, or the backdated insertedNext.requestroot) is still captured before entering the middleware span. The middleware and the render stay siblings, andtraceparenthandling and forwarding are unchanged.next start/next devstartup registration (separate roots).Errorthrows.instrumentation.ts, or noregisterexport.instrumentation.tsare out of scope (see docs: note that instrumentation-time spans don't join request traces #100324).Verification
traceparent, and with a host span.traceparent, host span.next start, startup spans stay roots. Fail-first verified.test/e2e/app-dir/otel-middleware-trace: span tree snapshots of the lifecycle spans with their ancestors, plus the first request's tree including them.middleware GETfor Node.js middleware.next dev/next start: startup roots.register,register()errors (includingthrow undefined), and module-load failure.