Skip to content

feat: Accept tunnel: true in init to enable the managed tunnel - #25200

Open
logaretm wants to merge 19 commits into
developfrom
awad/tunnel-browser-managed
Open

logaretm wants to merge 19 commits into
developfrom
awad/tunnel-browser-managed

Conversation

@logaretm

@logaretm logaretm commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

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
logaretm added this pull request to stack #25201 October 8, 2026 15:45
@logaretm
logaretm force-pushed the awad/tunnel-browser-managed branch from ec2da8d to 2933944 Compare October 8, 2026 16:05
@logaretm logaretm changed the title feat(browser): Add applyManagedTunnel to point the tunnel at the DSN-derived path feat: Accept tunnel: true in init to enable the managed tunnel Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Compared: 3a5171f (PR test merge using develop at 8c4491d)

Against: ca4fc32 (develop) · base run #62950

⚠️ Baseline mismatch: 34 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 8c4491d finishes.

Path Size % Change Change
@sentry/browser 30.56 kB +2.18% +651 B 🔺
@sentry/browser - with treeshaking flags 28.54 kB +1.76% +491 B 🔺
@sentry/browser - with treeshaking flags tracing without tracing 28.42 kB +1.69% +470 B 🔺
@sentry/browser (incl. Tracing) 52.8 kB +1.35% +702 B 🔺
@sentry/browser (incl. Tracing + Span Streaming) 52.8 kB +1.35% +699 B 🔺
@sentry/browser (incl. Tracing, Profiling) 55.77 kB +1.31% +720 B 🔺
@sentry/browser (incl. Tracing, Replay) 92.47 kB +0.68% +621 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 81.21 kB +0.61% +492 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 97.21 kB +0.66% +633 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 110.2 kB +0.63% +687 B 🔺
@sentry/browser (incl. Feedback) 48.12 kB +1.49% +703 B 🔺
@sentry/browser (incl. sendFeedback) 35.63 kB +1.9% +664 B 🔺
@sentry/browser (incl. FeedbackAsync) 40.76 kB +1.71% +684 B 🔺
@sentry/browser (incl. Metrics) 31.57 kB +2.05% +634 B 🔺
@sentry/browser (incl. Logs) 31.84 kB +2.02% +629 B 🔺
@sentry/browser (incl. Metrics & Logs) 32.52 kB +2.12% +673 B 🔺
@sentry/react 32.37 kB +1.99% +630 B 🔺
@sentry/react (incl. Tracing) 55.05 kB +1.17% +635 B 🔺
@sentry/vue 38.71 kB +1.7% +646 B 🔺
@sentry/vue (incl. Tracing) 55.75 kB +1.3% +715 B 🔺
@sentry/svelte 30.59 kB +2.2% +656 B 🔺
@sentry/remix (Remix 3 client bundle) 57.84 kB +1.27% +722 B 🔺
CDN Bundle 32.14 kB +1.5% +474 B 🔺
CDN Bundle (incl. Tracing) 53.09 kB +0.92% +480 B 🔺
CDN Bundle (incl. Logs, Metrics) 34.34 kB +1.4% +471 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) 55.04 kB +0.89% +484 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 75.29 kB +0.67% +499 B 🔺
CDN Bundle (incl. Tracing, Replay) 90.75 kB +0.53% +476 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 92.69 kB +0.52% +479 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 96.89 kB +0.49% +470 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 98.86 kB +0.5% +486 B 🔺
CDN Bundle - uncompressed 94.44 kB +1.24% +1.15 kB 🔺
CDN Bundle (incl. Tracing) - uncompressed 157.29 kB +0.74% +1.15 kB 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 100.99 kB +1.16% +1.15 kB 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 163.26 kB +0.72% +1.15 kB 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 231.03 kB +0.51% +1.15 kB 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed 277.46 kB +0.42% +1.15 kB 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 283.42 kB +0.41% +1.15 kB 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 291.16 kB +0.4% +1.15 kB 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 297.11 kB +0.39% +1.15 kB 🔺
@sentry/nextjs (client) 57.91 kB +1.05% +601 B 🔺
@sentry/sveltekit (client) 53.11 kB +1.21% +634 B 🔺
@sentry/core/server 43.52 kB +4.13% +1.73 kB 🔺
@sentry/core/browser 13.81 kB - -
@sentry/node 153.28 kB +0.44% +659 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.58 kB - -
@sentry/node - without tracing 94.9 kB +0.69% +649 B 🔺
@sentry/node - without channel injection 133.92 kB +0.47% +616 B 🔺
@sentry/aws-serverless 103.15 kB +0.7% +710 B 🔺
@sentry/cloudflare (withSentry) - minified 213.13 kB +0.96% +2.02 kB 🔺
@sentry/cloudflare (withSentry) 528.73 kB +0.94% +4.9 kB 🔺
@sentry/nextjs/cloudflare (withSentry) - minified 232.75 kB +0.88% +2.01 kB 🔺

View base workflow run

@logaretm
logaretm force-pushed the awad/tunnel-browser-managed branch from 2933944 to 4837dc0 Compare October 15, 2026 14:38
@logaretm
logaretm force-pushed the awad/tunnel-browser-managed branch 2 times, most recently from d5c110f to 4ab6297 Compare October 15, 2026 14:44
@logaretm
logaretm force-pushed the awad/tunnel-browser-managed branch from 4ab6297 to bf1d62c Compare October 15, 2026 14:47
@logaretm
logaretm force-pushed the awad/tunnel-browser-managed branch 3 times, most recently from 75a299e to b1553a8 Compare October 15, 2026 15:36
@logaretm
logaretm force-pushed the awad/tunnel-browser-managed branch from b1553a8 to 0e573ca Compare October 15, 2026 15:51
@logaretm logaretm changed the title feat: Accept tunnel: true in init to enable the managed tunnel feat: Accept tunnel: true in the SDKs that serve the managed tunnel Oct 15, 2026
@logaretm
logaretm force-pushed the awad/tunnel-browser-managed branch from 1d77f74 to 80f1700 Compare October 15, 2026 17:15
@logaretm logaretm changed the title feat: Accept tunnel: true in the SDKs that serve the managed tunnel feat: Accept tunnel: true in init to enable the managed tunnel Oct 15, 2026
@logaretm
logaretm force-pushed the awad/tunnel-browser-managed branch 4 times, most recently from a769b10 to d79df66 Compare October 15, 2026 18:28
@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/client.ts Outdated
Comment thread packages/browser/src/utils/managedTunnel.ts
@logaretm
logaretm force-pushed the awad/tunnel-browser-managed branch from d79df66 to 8717465 Compare October 16, 2026 01:51
@logaretm

Copy link
Copy Markdown
Member Author

bugbot run

1 similar comment
@logaretm

Copy link
Copy Markdown
Member Author

bugbot run

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Optional E2E tests

Some optional checks were not successful. They do not block the merge. 1 not successful, 64 successful. See the run.

Check Status
❌ CI: Optional E2E Tests / E2E node-anthropic-send-to-sentry (latest) Test (optional) Failing after 1m Details
Successful checks
Check Status
✅ CI: Optional E2E Tests / E2E astro-7-static Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-think (ai v6) Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E nextjs-14 (canary) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-express-mcp-v1 Test (optional) Successful in 25s Details
✅ CI: Optional E2E Tests / E2E gen-ai-libraries (cloudflare) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-15 (turbo) Test (optional) Successful in 4m Details
✅ CI: Optional E2E Tests / E2E node-anthropic-send-to-sentry Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E node-pi-durable Test (optional) Successful in 33s Details
✅ CI: Optional E2E Tests / E2E node-eve Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-openai-decisions-send-to-sentry Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E angular (canary) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-15 (latest) Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E gen-ai-libraries Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (canary, webpack) Test (optional) Successful in 7m Details
✅ CI: Optional E2E Tests / E2E cloudflare-workers-send-to-sentry Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E node-eve (orchestrion) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-14 (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E deno (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-openai-decisions-send-to-sentry (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-openai-send-to-sentry Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E react-router-7-framework-instrumentation Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nuxt-4-static (canary) Test (optional) Successful in 4m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (canary, turbopack) Test (optional) Successful in 4m Details
✅ CI: Optional E2E Tests / E2E cloudflare-flue-send-to-sentry Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-openai-send-to-sentry (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-eve (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E astro-7 Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (deno) Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E cloudflare-mastra Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-flue Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-express-send-to-sentry-static Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (vinext) Test (optional) Successful in 4m Details
✅ CI: Optional E2E Tests / E2E node-mastra Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-express-send-to-sentry Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-pi-durable Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (vinext, bun) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nuxt-5 Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E tanstackstart-react-cloudflare Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-flue (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-think Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-16-standalone (canary, turbopack) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-16-tunnel (cloudflare) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-pi-durable (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E react-send-to-sentry Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E vue-3 (canary) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-fastify-6 Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E angular (canary) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-orpc Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nuxt-3 (canary) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-express-mcp-v2 Test (optional) Successful in 26s Details
✅ CI: Optional E2E Tests / E2E debug-id-sourcemaps Test (optional) Successful in 51s Details
✅ CI: Optional E2E Tests / E2E node-pi-durable (latest) Test (optional) Successful in 37s Details
✅ CI: Optional E2E Tests / E2E node-mastra (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (cloudflare, latest) Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps (client source maps kept) Test (optional) Successful in 34s Details
✅ CI: Optional E2E Tests / E2E cloudflare-agent Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nuxt-4 (canary) Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E node-fastify-6 (tracer provider) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E supabase-nextjs Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps (static / nuxt generate) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps Test (optional) Successful in 44s Details
✅ CI: Optional E2E Tests / E2E solidstart-2 Test (optional) Successful in 50s Details
✅ CI: Optional E2E Tests / E2E tanstackstart-react-sourcemaps Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E vue-3 (no Options API) Test (optional) Successful in 1m Details

@logaretm
logaretm force-pushed the awad/tunnel-browser-managed branch from aee0a31 to 3d0e344 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.

Stale Bugbot comment from a previous run.

Comment thread packages/browser/src/sdk.ts Outdated

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

Had some questions mostly. I don't love the 0.5KB bundle size hit on the browser but I guess there's not much we can do here. Not sure if you tried it but would moving the managedTunnelTransport logic into the fetchTransport help reducing a bit of size footprint?

*
* @default undefined
*/
tunnel?: TunnelInit;

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.

A relative path, e.g. /monitor, enables the managed tunnel at that fixed path. The browser sends envelopes to it and the server SDK serves it, so set the same path in both inits.

dumb q and I probably missed something in the first PR: What happens if a user sets tunnel: '/my-endpoint' and expects to fully manage the tunnel endpoint themselves? Just wondering since the JSDoc says it enables the managed tunnel if this is still possible

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.

could it be that this doc is outdated? Given we don't set _managedTunnel when users pass a string…

@logaretm logaretm Oct 23, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's a good point, there is a bit of a fork here, if the user does:

tunnel: true // new behavior
tunnel: { path } // new behavior
tunnel: `/watever` // old behavior

Which I don't like 🤔 since you brought it up its better to resolve it here.

Ideally I want all to behave the same, and from a browser standpoint, that only matters in the resend fallback. The fallback doesn't kick in for non managed cases right now, but what i am saying is the browser shouldn't care about the tunnel being managed or not.

So my current idea is:

  • 200s are fine, no fallback what so ever there, i verified most static page providers, they never respond with a 200 for a post request for a page that doesn't exist, always 404 or 405. so that concern was invalid.
  • 4xx
    • if Sentry tunnel header tag present, no fallback, it came from Sentry.
    • If header not present, it came from the app, fallback activates.

So the breaking change case is the last one, I'm thinking we only add the fallback for 404/405 and keep everything else fallback-less. That makes it less aggressive and won't trigger for that use case as much.

* Falls back to sending directly when nothing on this origin serves the managed tunnel. An envelope is resent only
* when the response proves it never reached Sentry.
*/
export function makeManagedTunnelTransport(

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.

I was a bit hesitant first if sending directly to Sentry is a good fallback decision. But then again, this would only apply for a managed tunnel, which is new behaviour users have to opt into. That's fine I think. My concern was primarily about users explicitly not wanting to send directly, but every use case I could come up with (custom Relay, scrubbing, cleaning headers, etc) would require a custom tunnel implementation anyway. So I think this is fine. Wanted to leave here as food for thought if you agree with this or if you could see other cases where the fallback behaviour might not be ideal.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

See my comment up there. I will make changes here.

Comment on lines +80 to +86
// Not the tunnel: a 4xx never reached Sentry so it's resent, a 2xx may have, so only later envelopes go direct.
const rejected = statusCode >= 400 && statusCode < 500;
const { dsn, sdk } = envelope[0];
const components = (rejected || (statusCode >= 200 && statusCode < 300)) && dsn && makeDsn(dsn);
if (!components) {
return response;
}

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.

related to my comment above: What's the real world use case how this can happen? I assume it would be someone manually not configuring the server managed tunnel, correct? Also, can we be sure that 4xx responses never come from Sentry? I primarily want to make sure we don't resend when we already got rate limited.

@logaretm logaretm Oct 23, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Also, can we be sure that 4xx responses never come from Sentry

It depends, if they use a server SDK or our utils to handle the tunnel request then we will know because our utils tag the response with a header. if the header is present then we know it is from sentry and no fallback is triggered.

I'm making a couple of changes here:

  • Fallback will be limited to 404/405 responses because those would be misconfigured tunnels, unless it was from Sentry.
  • Everything else works the same without fallback.

@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/browser/src/transports/fetch.ts Outdated

@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 2d2f6a9. Configure here.

Comment thread packages/browser/src/transports/fetch.ts
Setting `tunnel: true` (or `{ allowedDsns, basePath, trailingSlash }`) in both
the browser and the server `init` enables the managed tunnel. The browser
resolves it to the DSN-derived same-origin path and sends envelopes as
`application/x-sentry-envelope`, because SvelteKit and Astro CSRF checks reject
a same-origin `text/plain` POST. Server SDKs keep their own envelopes going
directly to Sentry and set `_managedTunnel` for their request handlers.
With `debug: true`, the client warns once when a managed tunnel request gets a
403, 404 or 405, pointing at the server side: a meta-framework SDK, an endpoint
calling `handleTunnelRequest`, or `allowedDsns`.
…port, and import server helpers from core/server

Only the browser sends to a managed tunnel, so the warning moves from the shared Client into the browser's tunnel transport. Server SDKs import resolveServerTunnelOption from @sentry/core/server.
The reason strings were built in production bundles too, while only the debug warning that printed them was stripped.
…nel tests

`isTunnelRequest` only reads the active server client now, and the paths are already checked against `getTunnelPath`.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Managed tunnel in core and browser

4 participants