Skip to content

feat(nextjs): Emit the tunnel route handler - #25424

Draft
logaretm wants to merge 8 commits into
awad/runtime-tunnel-drop-framework-hooksfrom
awad/tunnel-nextjs
Draft

logaretm wants to merge 8 commits into
awad/runtime-tunnel-drop-framework-hooksfrom
awad/tunnel-nextjs

Conversation

@logaretm

@logaretm logaretm commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

This overhauls the tunnel implementation of Next.js by doing a few things:

  • Making the implementation use the same logic we have in the other frameworks/core, so it inherits:
    • The randomization is DSN based.
    • Proper forwarding for client IP and several headers.
    • Memory optimizations due to streaming the request/response.
  • We now spit a route file with the route, fully capitalizing on Sigrid's feat(nextjs): Add a tunnel route handler that sends only the event payload to Sentry #25456 this means:
    • No need to have omit for the transport options.

The way it works varies depeding on how the user deploys their application.

On Conventional Servers/Platforms

Our runtime patchers introduced in this stack handle the tunnel request, the user's Next.js app never handles any of these.

On Vercel

We utilize the two-hop idea we did in #25456 by writing a small idempotent app router file. The re-write rules move traffic to the new handler, and the file exports a POST handler for the route.

// Generated by @sentry/nextjs for the managed tunnel on Vercel.
export { POST } from '@sentry/nextjs/tunnel';

Vercel applies certain rules and responds with 404 for tunnel requests that aren't present in re-writes or app router, so our runtime patchers will never trigger. Which is why this workaround was needed.

For everyone else, the runtime patchers will work.

@logaretm
logaretm added this pull request to stack #25201 October 15, 2026 20:01
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Compared: bb7f6bf (PR test merge using develop at 8d098a1)

Against: dd9903c (develop) · base run #64617

⚠️ Baseline mismatch: 26 develop commit(s) in the PR build are missing from the baseline. Their size changes also affect this comparison. Compare commits. Re-run once develop's baseline build for 8d098a1 finishes.

Path Size % Change Change
@sentry/browser 30.39 kB - -
@sentry/browser - with treeshaking flags 28.54 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 28.42 kB - -
@sentry/browser (incl. Tracing) 52.71 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 52.72 kB - -
@sentry/browser (incl. Tracing, Profiling) 55.69 kB - -
@sentry/browser (incl. Tracing, Replay) 93.01 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 81.61 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 97.76 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 110.75 kB - -
@sentry/browser (incl. Feedback) 47.98 kB - -
@sentry/browser (incl. sendFeedback) 35.5 kB - -
@sentry/browser (incl. FeedbackAsync) 40.6 kB - -
@sentry/browser (incl. Metrics) 31.43 kB - -
@sentry/browser (incl. Logs) 31.72 kB - -
@sentry/browser (incl. Metrics & Logs) 32.44 kB - -
@sentry/react 32.28 kB - -
@sentry/react (incl. Tracing) 55.08 kB - -
@sentry/vue 38.66 kB - -
@sentry/vue (incl. Tracing) 55.73 kB - -
@sentry/svelte 30.42 kB - -
@sentry/remix (Remix 3 client bundle) 57.79 kB - -
CDN Bundle 32.18 kB - -
CDN Bundle (incl. Tracing) 53.23 kB - -
CDN Bundle (incl. Logs, Metrics) 34.46 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 55.17 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 75.87 kB - -
CDN Bundle (incl. Tracing, Replay) 91.45 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 93.38 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 97.55 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 99.55 kB - -
CDN Bundle - uncompressed 94.64 kB - -
CDN Bundle (incl. Tracing) - uncompressed 157.6 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 101.11 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 163.34 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 232.68 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 279.29 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 285.01 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 293 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 298.71 kB - -
@sentry/nextjs (client) 58.13 kB +0.19% +108 B 🔺
@sentry/sveltekit (client) 53.23 kB - -
@sentry/core/server 44.47 kB +0.52% +230 B 🔺
@sentry/core/browser 13.93 kB - -
@sentry/node 155.98 kB +0.19% +284 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 82.22 kB +0.01% +2 B 🔺
@sentry/node - without tracing 97.1 kB +0.3% +282 B 🔺
@sentry/node - without channel injection 136.3 kB +0.21% +277 B 🔺
@sentry/aws-serverless 105.36 kB +0.33% +342 B 🔺
@sentry/cloudflare (withSentry) - minified 202.02 kB +0.69% +1.38 kB 🔺
@sentry/cloudflare (withSentry) 501.94 kB +0.59% +2.94 kB 🔺
@sentry/nextjs/cloudflare (withSentry) - minified 222.9 kB +0.78% +1.73 kB 🔺

View base workflow run

Comment thread packages/nextjs/src/config/types.ts Resolved
@logaretm

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

@logaretm

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

@logaretm

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

@logaretm

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 1 potential issue.

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 5472088. Configure here.

basePath: userNextConfig.basePath ?? '',
...(path && { path }),
...(allowedDsns && { allowedDsns }),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tunnel ignores Next.js trailingSlash

Medium Severity

getManagedTunnelOptions copies basePath from the Next.js config but not trailingSlash, so the browser posts to the slashless DSN path. With trailingSlash: true, Next.js 308s that POST before beforeFiles rewrites, and the DSN rewrite source also omits the optional trailing slash that the fixed-path rewrite includes. Unload-time keepalive fetches often will not follow that redirect, so events never reach the tunnel.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5472088. Configure here.

logaretm added a commit that referenced this pull request Oct 23, 2026
…om the DSN (#25199)

### Overview

SDK Tunnel is a feature that allows client SDKs (e.g: browser) to send
their requests through an endpoint that lives on the user's own
application that forwards to Sentry. This bypasses adblockers and
similar extensions.

Tunnels could be configured as a fixed endpoint defined by the user
(defaulted to `/monitoring`) but that usually ends up in global
adblocker lists. Alternatively, users can tell the SDK to use a random
endpoint to bypass that.

Previously the tunnel logic mostly lived in client bundles and in server
frameworks build time, this had two issues:

- Both client and server bundles needed to share a single ENV during
build, or a single process. So both can agree on the random URL
generated.
- CI/CD workflows means assets can drift, so older client bundles might
be sending requests to older server bundles and vice versa, what happens
then is during the rollout/phase-out, the random endpoints won't match
and thus these envelopes will be getting 404'ed.

This stack of PRs re-works the idea by moving to more stable yet random
URL generation based on the client's DSN and moves the tunnel handling
into the runtime of the core SDK.

Meaning tunneling now is a core SDK capability rather than a meta
framework feature.

Here is how it looks for users:

```ts
// client, posts to a path hashed from the DSN, e.g. `/k3x9q2a`
Sentry.init({ dsn, tunnel: true });

// server, nothing to do in a meta-framework, the SDK answers that path from its request hook.
// You only need this if the browser uses a different DSN than the server.
Sentry.init({ dsn, tunnel: { allowedDsns: [browserDsn] } });
```

And a tunneled request goes like this: the framework hook calls
`handleTunnelRequestIfMatched`, if the path matches it checks the
envelope's DSN against the allowed ones and forwards it to Sentry,
otherwise the request continues as normal.

### This PR

This sets up the core for the new tunnel mechanism that the rest of the
stack will be implemented against, it does the following:

- Sets up the tunnel generation and matching logic.
- Sets up global span/breadcrumb ignore rules for tunneled requests.
- Adds request checking, header forwarding, and the proxying to the
Sentry SaaS DSN.

It also sets up a couple of key decisions that will carry over
throughout the stack:

- **Base paths are respected always:** if a user sets `/app` as a base
path our tunnels handlers will expect that prefix.
- **Servers always allow their own DSNs:** This avoids the footgun where
a user misconfigures `allowedDSNs` on the server, this correction only
applies if the client and server share the same DSN, if there is a
different DSN involved then it is on the user to configure that
correctly. Will work on documenting this.

### Reviewing the stack

This PR and #25200 are where the actual behavior lives so these need the
closest look, mainly `forwardEnvelope` and the base path handling in
`tunnel.ts`.

The framework PRs all follow the same pattern: find the earliest request
hook the framework gives us, call the core handler there, and pass the
framework's base path along. They are small, so ideally whoever knows
that framework best picks it up:

- TanStack Start #25408
- SvelteKit #25415
- Nuxt and Nitro #25418, this one has more going on since it has to run
before the user's own server middleware
- Astro #25419
- Remix #25420
- React Router #25422
- SolidStart #25423

#25414 is tests only, and Next.js #25424 is on hold, I will split it up
before it's ready.
@logaretm
logaretm removed this pull request from stack #25201 October 27, 2026 19:24
Base automatically changed from awad/tunnel-solidstart to develop October 27, 2026 19:48
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Optional E2E tests

All optional checks were successful. 10 successful. See the run.

Successful checks
Check Status
✅ CI: Optional E2E Tests / E2E nextjs-15 (turbo) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-16-tunnel (cloudflare) Test (optional) Successful in 37s Details
✅ CI: Optional E2E Tests / E2E supabase-nextjs Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-orpc Test (optional) Successful in 50s Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (deno) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (vinext, bun) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (cloudflare, latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (vinext) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-15 (latest) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-14 (latest) Test (optional) Successful in 1m Details

@github-actions

Copy link
Copy Markdown
Contributor

One or more gzipped bundles increased by more than 500 bytes. If this increase is intentional, add the Accept Bundlesize Increase label to this PR to rerun and accept the check.

…equest`

The tunnel route handler now leaves DSN normalization, the allowlist and
tracing suppression to core. As in every other SDK, `allowedDsns` adds to
the server's own DSN, and the default `POST` accepts the DSNs
`serveTunnelIntegration` allows.
…ePath`

`tunnel: true` in the browser now sends to `<basePath>/<hash>`, like the
SvelteKit and React Router SDKs do with their base paths. A `basePath` the
user set and a `tunnel` URL stay as written.
…the server

`withSentryConfig` now injects `basePath` into production server and edge
builds, not only in dev, and the server, edge and Cloudflare inits pass it
to `setManagedTunnelDefaults`. `serveTunnelIntegration` then matches
`/<basePath>/<hash>`, like the browser sends.
Vercel serves unknown paths from its CDN without running the app, so the runtime never sees managed tunnel
requests there. On Vercel builds without `tunnelRoute`, `withSentryConfig` writes the tunnel route handler and
rewrites envelope requests to the DSN-derived paths there. The handler answers only when `serveTunnelIntegration`
is set up, and on a cold start too. Pages Router apps get an `app` directory with only this route.
`tunnel: true` in the browser and `serveTunnelIntegration` on the server serve the tunnel on every Next.js
deployment, so `tunnelRoute` and the `@sentry/nextjs/tunnel` route handlers are deprecated. `withSentryConfig`
warns when `tunnelRoute` is set.

This branch has not been deployed

No deployments
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