Skip to content

feat(core): Add managed tunnel helpers that derive the tunnel path from the DSN - #25199

Merged
logaretm merged 40 commits into
developfrom
awad/tunnel-core-dsn-path
Oct 23, 2026
Merged

logaretm merged 40 commits into
developfrom
awad/tunnel-core-dsn-path

Conversation

@logaretm

@logaretm logaretm commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

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:

// 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:

#25414 is tests only, and Next.js #25424 is on hold, I will split it up before it's ready.

@logaretm
logaretm added this pull request to stack #25201 October 8, 2026 15:45
@logaretm
logaretm force-pushed the awad/tunnel-core-dsn-path branch from af27973 to a2af1e1 Compare October 8, 2026 16:05
@logaretm logaretm changed the title feat(core): Derive the tunnel path from the DSN and match tunnel requests feat(core): Add managed tunnel helpers that derive the tunnel path from the DSN Oct 8, 2026
@logaretm
logaretm force-pushed the awad/tunnel-core-dsn-path branch 4 times, most recently from c544ac0 to 89a5b2c Compare October 15, 2026 15:36
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Compared: e7eedd7 (PR test merge using develop at d4a4e81)

Against: ca4fc32 (develop) · base run #62950

⚠️ Baseline mismatch: 1 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 d4a4e81 finishes.

Path Size % Change Change
@sentry/browser 29.91 kB - -
@sentry/browser - with treeshaking flags 28.05 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.95 kB - -
@sentry/browser (incl. Tracing) 52.1 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 52.1 kB - -
@sentry/browser (incl. Tracing, Profiling) 55.05 kB - -
@sentry/browser (incl. Tracing, Replay) 91.85 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 80.72 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 96.58 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 109.52 kB - -
@sentry/browser (incl. Feedback) 47.42 kB - -
@sentry/browser (incl. sendFeedback) 34.97 kB - -
@sentry/browser (incl. FeedbackAsync) 40.07 kB - -
@sentry/browser (incl. Metrics) 30.93 kB - -
@sentry/browser (incl. Logs) 31.21 kB - -
@sentry/browser (incl. Metrics & Logs) 31.84 kB - -
@sentry/react 31.74 kB - -
@sentry/react (incl. Tracing) 54.42 kB - -
@sentry/vue 38.07 kB - -
@sentry/vue (incl. Tracing) 55.03 kB - -
@sentry/svelte 29.93 kB - -
@sentry/remix (Remix 3 client bundle) 57.11 kB - -
CDN Bundle 31.67 kB - -
CDN Bundle (incl. Tracing) 52.61 kB - -
CDN Bundle (incl. Logs, Metrics) 33.87 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 54.55 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 74.79 kB - -
CDN Bundle (incl. Tracing, Replay) 90.28 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 92.21 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 96.42 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 98.37 kB - -
CDN Bundle - uncompressed 93.29 kB - -
CDN Bundle (incl. Tracing) - uncompressed 156.14 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 99.84 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 162.11 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 229.88 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 276.31 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 282.26 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 290.01 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 295.96 kB - -
@sentry/nextjs (client) 57.31 kB - -
@sentry/sveltekit (client) 52.48 kB - -
@sentry/core/server 43.52 kB +4.13% +1.73 kB 🔺
@sentry/core/browser 13.81 kB - -
@sentry/node 153.18 kB +0.37% +553 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.58 kB - -
@sentry/node - without tracing 94.8 kB +0.58% +546 B 🔺
@sentry/node - without channel injection 133.85 kB +0.42% +547 B 🔺
@sentry/aws-serverless 102.98 kB +0.53% +542 B 🔺
@sentry/cloudflare (withSentry) - minified 212.91 kB +0.86% +1.8 kB 🔺
@sentry/cloudflare (withSentry) 528.31 kB +0.86% +4.47 kB 🔺
@sentry/nextjs/cloudflare (withSentry) - minified 232.53 kB +0.78% +1.8 kB 🔺

View base workflow run

@logaretm
logaretm force-pushed the awad/tunnel-core-dsn-path branch from 3bba67e to 4eea4c9 Compare October 15, 2026 17:36
@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.

Comment thread packages/core/src/utils/tunnel.ts Outdated
Comment thread packages/core/test/lib/utils/tunnel.test.ts
@logaretm

Copy link
Copy Markdown
Member Author

bugbot run

1 similar comment
@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.

@linear-code

linear-code Bot commented Oct 16, 2026

Copy link
Copy Markdown

JS-3942

Comment thread packages/core/src/utils/tunnel.ts Fixed
@logaretm
logaretm force-pushed the awad/tunnel-core-dsn-path branch 6 times, most recently from 8e34dd6 to f97d8a2 Compare October 19, 2026 19:12
@logaretm
logaretm marked this pull request as ready for review October 19, 2026 19:36
@logaretm
logaretm requested a review from a team as a code owner October 19, 2026 19:36
@logaretm
logaretm requested review from chargome and removed request for a team October 19, 2026 19:36
@logaretm
logaretm force-pushed the awad/tunnel-core-dsn-path branch from feae0ed to 98ff881 Compare October 22, 2026 15:07

@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 98ff881. Configure here.

Comment thread packages/core/src/integrations/fetch.ts

@Lms24 Lms24 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Given @JPeer264 thorough review, I mainly focused on the forwarding logic itself and had some suggestions for the max size check and forwarding more headers.

I'm +1 for making the browser- and server-specific options browser/server client specific to avoid confusion.

Comment thread packages/core/src/utils/tunnel.ts Outdated
Comment thread packages/core/src/utils/tunnel.ts
Comment thread packages/core/src/utils/tunnel.ts
@logaretm
logaretm requested review from JPeer264 and Lms24 October 22, 2026 15:48

@Lms24 Lms24 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

thanks for making the changes!

@logaretm
logaretm merged commit 9d42fea into develop Oct 23, 2026
690 of 695 checks passed
@logaretm
logaretm deleted the awad/tunnel-core-dsn-path branch October 23, 2026 17:36
logaretm added a commit that referenced this pull request Oct 27, 2026
…25200)

Previously the client side only accepted a `string` tunnel option so it
sent the requests to that fixed endpoint, this PR effectively adds
"managed" tunnel support that is generated automatically from the
provided DSN.

This PR adds the tunnel options to the SDKs's `init` options and
implements the client transport if the tunnel is provided.

On the server side the runtime SDKs only translate `tunnel: true` into
the internal `_managedTunnel` option, answering the requests is left to
the meta-frameworks in the rest of the stack.

For reviewers: if you need an overview of what this is, refer to #25199.

closes #25460
logaretm added a commit that referenced this pull request Oct 27, 2026
…ntry (#25408)

TanStack Start picked a random tunnel path per build and patched the
generated route tree to serve it.

Because the new approach relies less on build-time, the logic was
changed to wrap the server entrypoint instead and only route the
matching tunnel requests to our handler. The tradeoff is that every app
using `sentryTanstackStart()` gets its server entry wrapped, tunnel or
not, since `serveTunnelIntegration()` turns the tunnel on at runtime.
The original entry then loads through a `?sentry-tunnel-original` id,
and without a tunnel each request only pays one lookup.

This isn't a breaking change, previously we injected a single endpoint
handler, now we inject a middleware since the DSN is only known at
runtime.

The base path comes from `router.basepath` (or Vite's `base`), and the
`tunnel-basepath` e2e variants cover it in both dev and prod.

For reviewers: if you need an overview of what this is, refer to #25199.

closes #25461
logaretm added a commit that referenced this pull request Oct 27, 2026
…uests (#25414)

Runtimes only translate `tunnel: true` while meta-frameworks answer the
requests, so nothing pinned down that their server spans get dropped.

This PR asserts that runtime SDKs level (Cloudflare, Node, Bun and Deno)
ignores the tunneled span so we don't have to chase it around. This is
one benefit of having tunnels as a core SDK capability, this PR just
tests that.

Node, Deno and Cloudflare live in their integration test suites, and Bun
is a unit test in its package.

For reviewers: if you need an overview of what this is, refer to #25199.

closes #25462
logaretm added a commit that referenced this pull request Oct 27, 2026
…he app's base path (#25415)

Implements sveltekit tunnel wiring, the way it works is:

- Our Vite plugin injects the base path (`kit.paths.base`) if it exists
to both server/client bundles.
- Our entry handler (`sentryHandle`) will check for any incoming request
if it is a tunneled one, and if it matches it handles it.

_(I made the decision to respect base paths rather than ignoring them,
this is something to review in #25199)_

This also adds a couple of shared e2e helpers to
`@sentry-internal/test-utils` that the rest of the stack uses. The
`sveltekit-3` tunnel variants cover dev and prod, and there's one for
Cloudflare Workers too.

For reviewers: if you need an overview of what this is, refer to #25199.

closes #20256
logaretm added a commit that referenced this pull request Oct 27, 2026
…iddleware (#25418)

A Nitro app's own `server/middleware`, like an auth check, ran before
anything Sentry registered and would block tunnel requests, fixed by
putting the tunnel check first in h3's middleware on Nitro v3 and
answering from the `request` hook on Nitro v2.

This is the one framework PR where the ordering matters, so worth a
closer look. Nuxt picks the right plugin based on the Nitro version, and
the base path comes from Nitro's `baseURL`.

The e2e tunnel variants for Nuxt 4, Nuxt 5 and Nitro 3 check that the
tunnel answers while an auth middleware rejects every other POST.

For reviewers: if you need an overview of what this is, refer to #25199.

closes #20254
logaretm added a commit that referenced this pull request Oct 27, 2026
)

This PR adds the tunnel wiring to the astro SDK. We already had a global
middleware active, so it works by adding logic to check if the incoming
request is a tunnel request and if it is, then it will be handled.

The server init now also runs from the integration's middleware entry,
so the first request is instrumented even when it hits a prerendered
page first, which also fixes server islands after a prerendered page.

This also respects the decision made earlier to follow the base path
configured by the user, which comes from Astro's `base` config.

The `astro-7` and `astro-6-cf-workers` tunnel variants cover node and
Cloudflare.

For reviewers: if you need an overview of what this is, refer to #25199.

closes #25463
logaretm added a commit that referenced this pull request Oct 27, 2026
…are (#25420)

Adds the tunnel wiring to the remix SDK. We already had a middleware so
it works by adding the request matching logic there.

The `remix-v3` tunnel variant covers it.

For reviewers: if you need an overview of what this is, refer to #25199.

closes #25464
logaretm added a commit that referenced this pull request Oct 27, 2026
…er build (#25422)

React Router has no request hook, so to wire the tunnel logic, we add a
resource route to the server build whose path comes from the DSN at
runtime.

The base path is the app's `basename`, which the server build already
exports. The `tunnel-basepath` e2e variants cover dev, prod and
Cloudflare.

For reviewers: if you need an overview of what this is, refer to #25199.

closes #20255
logaretm added a commit that referenced this pull request Oct 27, 2026
This PR wires SolidStart's SDK tunnel by adding a nitro 2 plugin that
adds a request hook to handle tunneled requests.

The base path comes from `server.baseURL`, and the `tunnel-basepath` e2e
variants cover dev and prod.

For reviewers: if you need an overview of what this is, refer to #25199.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants