You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
nextjs: Next 16 request yields a route-less http.server segment named only by method, plus the route-named one #25370
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.
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.
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:
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.
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.
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.
Activity
linear-code commentedon Oct 14, 2026
JS-3937
Lms24 commentedon Oct 14, 2026
Hey @rhbra thanks for reporting! I'm going to look into this. My current thinking is we should avoid creating the
GETspan in the first place and leave the twomiddleware GETandGET /loginspans 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.inickua commentedon Oct 15, 2026
Thanks for #25386, it matches what we see. Here is some extra data in case it helps with test cases.
Setup:
@sentry/nextjs11.6.0,next16.4.0, App Router,proxy.ts,output: 'standalone', Turbopack,node server.js. The DSN pointed at a local sink and every request carried a sampledsentry-traceheader. The same image was built twice, differing only intraceLifecycle.Requests: four pages, one 308 issued by
proxy.ts, two 404s (an unknown path under a catch-all, and a segment whose layout callsnotFound()), and onePOSTto thetunnelRoute.staticstreamGETshell (next.bubble: true)GET /[locale]/[...slug],GET /[locale]/crypto, status 404)POST o1.ingest.sentry.io(http.client) per tunnel requestThe third row looks like the ingest drop that the PR description says was silently disabled.
Two cases that may be worth covering:
middlewareInvokeis set on those requests too.http.serversegment 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.ref(nextjs): Drop the proxy-only request span with ignoreSpans
test(nextjs): Expect one http.server segment per request behind the p…
fix(nextjs): Drop the request span that only runs the proxy
ref(nextjs): Drop the proxy-only request span with ignoreSpans
Lms24 commentedon Oct 16, 2026
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.