Skip to content

nextjs: Next 16 request yields a route-less http.server segment named only by method, plus the route-named one #25370

Open

Description

@rhbra

Environment

  • @sentry/nextjs 11.7.0 (default span streaming), @sentry/node 11.7.0
  • next 16.4.0, App Router, proxy.ts present, output: 'standalone', next build with Turbopack, run with node server.js
  • Sentry.init in instrumentation.ts with tracesSampleRate: 1, no ignoreSpans, no custom sampler

What happens

Each request produces three separate segment spans in the same trace, none with a parent:

name sentry.op http.route
middleware GET middleware middleware GET
GET http.server none
GET /login http.server /login

Captured by pointing the DSN at a local HTTP sink (one request each to /login and /api/users/me). For /login, the bare GET starts within a millisecond of middleware GET and ends when it ends; GET /login starts when the bare GET ends. The bare GET has url.path set but no http.route or next.route, and enhanceHandleRequestRootSpan only names a span from next.route/http.route, so it keeps the method as its name. Every route's bare GET looks the same.

Why it matters

  • http.server is counted twice per request, so transaction counts, failure rates and p95 over span.op:http.server are skewed.
  • When the named segment is dropped (for example by Sentry's "Filter out health check transactions" inbound filter, which matches GET /api/health), the bare GET for that request is still ingested, so a filtered route still shows up as GET.

What I tried

ignoreSpans: [{ name: /^(GET|POST)$/, op: 'http.server' }] drops a span created with that name and op in a unit test (startSpan), but removes nothing from the real Next server. I did not find out why. My guess is that the name or op is set after the filter runs, but I have not confirmed that.

Not tested

I did not run the same app without proxy.ts, so "the bare root is the proxy phase" is inferred from timing and the middleware GET sibling, not confirmed.

Expected

One http.server segment per request, named by route, with the proxy/middleware work as a child or as a clearly separate segment that is not also op http.server.

Activity

  1. linear-code commented on Oct 14, 2026

    @linear-code
  2. moved this to Waiting for: Product Owner in GitHub Issues with 👀 3on Oct 14, 2026
  3. self-assigned this
    on Oct 14, 2026
  4. Lms24 commented on Oct 14, 2026

    @Lms24
    Member

    Hey @rhbra thanks for reporting! I'm going to look into this. My current thinking is we should avoid creating the GET span in the first place and leave the two middleware GET and GET /login spans be separate segments. This is probably the thing closest to how NextJS intends to have its traces look like (our SDK mostly picks up the spans emitted from NextJS). Might be a bit tricky to implement but I'm working on this right now.

  5. moved this from Waiting for: Product Owner to No status in GitHub Issues with 👀 3on Oct 14, 2026
  6. inickua commented on Oct 15, 2026

    @inickua

    Thanks for #25386, it matches what we see. Here is some extra data in case it helps with test cases.

    Setup: @sentry/nextjs 11.6.0, next 16.4.0, App Router, proxy.ts, output: 'standalone', Turbopack, node server.js. The DSN pointed at a local sink and every request carried a sampled sentry-trace header. The same image was built twice, differing only in traceLifecycle.

    Requests: four pages, one 308 issued by proxy.ts, two 404s (an unknown path under a catch-all, and a segment whose layout calls notFound()), and one POST to the tunnelRoute.

    static stream
    route-less GET shell (next.bubble: true) 0 7: one per page request, including the 308 and both 404s
    render of a 404 (GET /[locale]/[...slug], GET /[locale]/crypto, status 404) 0 2
    POST o1.ingest.sentry.io (http.client) per tunnel request 0 1

    The third row looks like the ingest drop that the PR description says was silently disabled.

    Two cases that may be worth covering:

    1. Shells of a 308 and of a 404. In our capture these look the same as the shells of pages. It would be good to confirm that middlewareInvoke is set on those requests too.
    2. 404 renders. In static mode they produce no transaction, but in a stream each one becomes a full http.server segment with status 404. The PR description does not seem to touch these.

    One improvement with streaming, for completeness: a background ISR regeneration now arrives as children of its render. In static mode its late spans came as separate transactions marked sentry.parent_span_already_sent.

    We are keeping traceLifecycle: 'static' for now and will rerun the same capture on the release that includes the fix.

  7. moved this to Waiting for: Product Owner in GitHub Issues with 👀 3on Oct 15, 2026
  8. Lms24 commented on Oct 16, 2026

    @Lms24
    Member

    It seems like this is currently being reworked by NextJS:

    This was revealed because the our Next 16 canary tests failed on my fix PR. If Next ends up fixing this in a later version, we might just wait for that fixed version and recommend upgrading to it.

  9. moved this from Waiting for: Product Owner to No status in GitHub Issues with 👀 3on Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions