fix(platform): Classify traffic via middleware metric instead of trace sampling#18775
fix(platform): Classify traffic via middleware metric instead of trace sampling#18775sergical wants to merge 1 commit into
Conversation
…e sampling Middleware root spans are created by Next.js itself (Middleware.execute, in a detached sandbox) before any request data reaches Sentry, so tracesSampler can never see headers or a user-agent for them. Since the instrumentation fix landed, ~100% of middleware spans classified as unknown and were sampled at 30% — including bots — ingesting ~264k information-free spans per day (the span name is collapsed to "middleware GET" with no URL). Stop trying to classify at sampling time and count where the data lives: - middleware.ts classifies each request once and emits a docs.request.classified counter (traffic_type, device_type, and the matched agent for AI traffic) for every request it sees, including redirects, which were previously not counted. The classification is threaded through to the x-traffic-type request-header stamping instead of classifying twice. - tracesSampler drops middleware root spans (sample rate 0) and becomes a pure sampling function: it keeps the proven x-traffic-type / user-agent classification for downstream node routes (agents 100%, bots 0%, users 30%) and no longer emits metrics. - Remove the temporary beforeSendTransaction hook in instrumentation.ts that renamed middleware transactions; it is dead once middleware spans are no longer sampled. The docs.trace.sampled metric stops being emitted; dashboards should switch to docs.request.classified, which counts requests rather than sampling decisions. Co-Authored-By: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b1e62a6. Configure here.
| samplingContext.attributes?.['sentry.op'] === 'http.server.middleware' || | ||
| samplingContext.name === 'middleware' || | ||
| Boolean(samplingContext.name?.startsWith('middleware ')) | ||
| ); |
There was a problem hiding this comment.
Middleware name fallbacks never match
Medium Severity
isMiddlewareRootSpan falls back on name === 'middleware' and name.startsWith('middleware '), but tracesSampler receives the span's initial name. For Next.js that is typically Middleware.execute, before any later rename to middleware GET. Those name checks therefore never match, so detection depends entirely on next.span_type / sentry.op. If those attributes are missing, middleware root spans keep getting sampled.
Reviewed by Cursor Bugbot for commit b1e62a6. Configure here.
| /** | ||
| * Handles redirection to markdown versions for AI/LLM clients | ||
| */ | ||
| const handleAIClientRedirect = (request: NextRequest) => { |
There was a problem hiding this comment.
Unused device-type header remains
Low Severity
createClassifiedRequestHeaders still sets x-device-type on every forwarded request, but this commit removed the only reader in tracesSampler. Device type is now only used for the local docs.request.classified metric, so the header is a dead store.
Reviewed by Cursor Bugbot for commit b1e62a6. Configure here.


DESCRIBE YOUR PR
Follow-up to #18771. That PR restored server-side Sentry, which exposed a structural problem: middleware root spans can never be classified at sampling time. The root span for a middleware request is Next.js's own
Middleware.executeOTEL span, created in a detached sandbox before any request data reaches Sentry — sotracesSamplerfires with no headers and no user-agent. Result since the fix landed: ~100% of middleware spans classify asunknownand get sampled at 30% — including bots — ingesting ~264k/day spans that carry no information (the span name is collapsed tomiddleware GET, no URL).Rather than fighting this with send-time filtering wrappers, this PR moves each signal to where the data actually lives:
middleware.tscounts every request. It already classifies each request to stampx-traffic-type; now it classifies once and emits adocs.request.classifiedcounter (traffic_type,device_type, plus the matchedagentfor AI traffic) for every request — no sampling, 100% accurate, and redirect requests are now counted too (previously they were skipped). This metric is the system of record for agent/bot/user traffic on the docs.tracesSamplerdrops middleware root spans (rate 0) and becomes a pure sampling function. The proven classification for downstream node routes is unchanged:x-traffic-typeheader first, user-agent fallback — agents 100%, bots 0%, users 30%. All metric emission is removed from the sampler.beforeSendTransactionhook ininstrumentation.tsthat renamed middleware transactions — dead code once middleware spans stop flowing.Net effect on ingestion: middleware span volume drops from ~264k/day to zero (they carried no insight), metric volume stays roughly the same as the old
docs.trace.sampledemission, and per-request edge CPU goes down (one classification instead of two, sampler exits immediately for middleware spans).Dashboard note:
docs.trace.sampledstops being emitted. Queries should move todocs.request.classified, which counts requests rather than sampling decisions — a clean epoch, since 4.5 months of the old metric are all-unknownzombie data anyway.Post-deploy verification (
sentry/docs, thensentry/develop-docs):metrics · docs.request.classified · group by traffic_type→ realai_agent/bot/usersplit;group by agent→ which AI agents consume the docsspans · span.op:http.server.middleware · release:<new sha>→ should drop to zerospans · span.op:http.server release:<new sha> · group by ...→ node route spans keep flowing with header-based classificationIS YOUR CHANGE URGENT?
Help us prioritize incoming PRs by letting us know when the change needs to go live.
SLA
Thanks in advance for your help!
PRE-MERGE CHECKLIST
Make sure you've checked the following before merging your changes:
🤖 Generated with Claude Code