Skip to content

fix(platform): Classify traffic via middleware metric instead of trace sampling#18775

Open
sergical wants to merge 1 commit into
masterfrom
sergical/fix/classify-traffic-in-middleware
Open

fix(platform): Classify traffic via middleware metric instead of trace sampling#18775
sergical wants to merge 1 commit into
masterfrom
sergical/fix/classify-traffic-in-middleware

Conversation

@sergical

Copy link
Copy Markdown
Member

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.execute OTEL span, created in a detached sandbox before any request data reaches Sentry — so tracesSampler fires with no headers and no user-agent. Result since the fix landed: ~100% of middleware spans classify as unknown and get sampled at 30% — including bots — ingesting ~264k/day spans that carry no information (the span name is collapsed to middleware GET, no URL).

Rather than fighting this with send-time filtering wrappers, this PR moves each signal to where the data actually lives:

  • middleware.ts counts every request. It already classifies each request to stamp x-traffic-type; now it classifies once and emits a docs.request.classified counter (traffic_type, device_type, plus the matched agent for 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.
  • tracesSampler drops middleware root spans (rate 0) and becomes a pure sampling function. The proven classification for downstream node routes is unchanged: x-traffic-type header first, user-agent fallback — agents 100%, bots 0%, users 30%. All metric emission is removed from the sampler.
  • Removes the temporary beforeSendTransaction hook in instrumentation.ts that 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.sampled emission, and per-request edge CPU goes down (one classification instead of two, sampler exits immediately for middleware spans).

Dashboard note: docs.trace.sampled stops being emitted. Queries should move to docs.request.classified, which counts requests rather than sampling decisions — a clean epoch, since 4.5 months of the old metric are all-unknown zombie data anyway.

Post-deploy verification (sentry/docs, then sentry/develop-docs):

  • metrics · docs.request.classified · group by traffic_type → real ai_agent / bot / user split; group by agent → which AI agents consume the docs
  • spans · span.op:http.server.middleware · release:<new sha> → should drop to zero
  • spans · span.op:http.server release:<new sha> · group by ... → node route spans keep flowing with header-based classification

IS YOUR CHANGE URGENT?

Help us prioritize incoming PRs by letting us know when the change needs to go live.

  • Urgent deadline (GA date, etc.):
  • Other deadline:
  • None: Not urgent, can wait up to 1 week+

SLA

  • Teamwork makes the dream work, so please add a reviewer to your PRs.
  • Please give the docs team up to 1 week to review your PR unless you've added an urgent due date to it.
    Thanks in advance for your help!

PRE-MERGE CHECKLIST

Make sure you've checked the following before merging your changes:

  • Checked Vercel preview for correctness, including links
  • PR was reviewed and approved by any necessary SMEs (subject matter experts)
  • PR was reviewed and approved by a member of the Sentry docs team

🤖 Generated with Claude Code

…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>
@vercel

vercel Bot commented Jul 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
develop-docs Ready Ready Preview Jul 20, 2026 11:25pm
sentry-docs Ready Ready Preview Jul 20, 2026 11:25pm

Request Review

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ 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.

Comment thread src/tracesSampler.ts
samplingContext.attributes?.['sentry.op'] === 'http.server.middleware' ||
samplingContext.name === 'middleware' ||
Boolean(samplingContext.name?.startsWith('middleware '))
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b1e62a6. Configure here.

Comment thread middleware.ts
/**
* Handles redirection to markdown versions for AI/LLM clients
*/
const handleAIClientRedirect = (request: NextRequest) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b1e62a6. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant