From 8d8728bb54238f1e2b5492e4d552594bb06f9236 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:01:47 +0200 Subject: [PATCH 1/3] feat(nextjs): Send tunnel route requests with `credentials: 'omit'` --- CHANGELOG.md | 14 +++++++ .../tests/tunnel-route.test.ts | 35 ++++++++++++++++- packages/browser/src/transports/fetch.ts | 2 + packages/nextjs/src/client/tunnelRoute.ts | 6 +++ packages/nextjs/src/config/types.ts | 4 ++ .../nextjs/test/utils/tunnelRoute.test.ts | 38 +++++++++++++++++++ 6 files changed, 97 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eebec107e4de..de60d2d7e348 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +### Important Changes + +- **feat(nextjs): Send tunnel route requests with `credentials: 'omit'` ([#PR](https://github.com/getsentry/sentry-javascript/pull/PR))** + + With `tunnelRoute`, `@sentry/nextjs` now sends events from the browser with `credentials: 'omit'`, because the tunnel route does not need cookies. + If a layer in front of your app checks cookies before it lets requests through (for example Vercel Deployment Protection), you can send them again in your browser `Sentry.init` call: + + ```js + // instrumentation-client.ts + Sentry.init({ + transportOptions: { fetchOptions: { credentials: 'same-origin' } }, + }); + ``` + Work in this release was contributed by @adityareddy-dev, @jpdefo, and @Jxxunnn. Thank you for your contributions! ## 11.8.0 diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-route.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-route.test.ts index 3d44c8fc8108..8b9e8c3641b7 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-route.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-route.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -import { getSpanOp, waitForStreamedSpan, waitForStreamedSpans } from '@sentry-internal/test-utils'; -import type { SerializedStreamedSpan } from '@sentry/core'; +import { getSpanOp, waitForRequest, waitForStreamedSpan, waitForStreamedSpans } from '@sentry-internal/test-utils'; +import type { Event, SerializedStreamedSpan } from '@sentry/core'; function getStringAttribute(span: SerializedStreamedSpan, key: string): string | undefined { const attribute = span.attributes[key]; @@ -120,3 +120,34 @@ test('Tunnel requests should not create middleware or fetch spans', async ({ pag expect(sentryFetchSpans).toHaveLength(0); }); + +test('Tunnel requests are sent without the cookies of the browser', async ({ page, context, baseURL }) => { + await context.addCookies([{ name: 'session', value: '8f2c1d', url: baseURL }]); + const proxyRequestPromise = waitForRequest('nextjs-16-tunnel', ({ envelope }) => + envelope[1].some( + ([itemHeader, item]) => + itemHeader.type === 'event' && (item as Event).exception?.values?.[0]?.value === 'Tunnel cookie test', + ), + ); + + await page.goto('/'); + await page.waitForFunction(() => typeof (window as { __SENTRY__?: unknown }).__SENTRY__ === 'object'); + const tunnelRequestPromise = page.waitForRequest( + request => + request.method() === 'POST' && + new URL(request.url()).origin === baseURL && + (request.postData() ?? '').includes('Tunnel cookie test'), + ); + await page.evaluate(() => { + setTimeout(() => { + throw new Error('Tunnel cookie test'); + }); + }); + + const tunnelRequest = await tunnelRequestPromise; + const proxyRequest = await proxyRequestPromise; + + expect(await page.evaluate(() => document.cookie)).toBe('session=8f2c1d'); + expect((await tunnelRequest.allHeaders()).cookie).toBeUndefined(); + expect(proxyRequest.rawProxyRequestHeaders.cookie).toBeUndefined(); +}); diff --git a/packages/browser/src/transports/fetch.ts b/packages/browser/src/transports/fetch.ts index 83e3905de35c..23d499bb66e1 100644 --- a/packages/browser/src/transports/fetch.ts +++ b/packages/browser/src/transports/fetch.ts @@ -38,6 +38,8 @@ export function makeFetchTransport( // There is also a limit of requests that can be open at the same time, so we also limit this to 15 // See https://github.com/getsentry/sentry-javascript/pull/7553 for details keepalive: pendingBodySize <= 60_000 && pendingCount < 15, + // TODO(v12): Default to `credentials: 'omit'`. With the fetch default `'same-origin'`, requests to a same-origin + // `tunnel` include credentials (cookies, HTTP authentication), which the tunnel does not need. ...options.fetchOptions, }; diff --git a/packages/nextjs/src/client/tunnelRoute.ts b/packages/nextjs/src/client/tunnelRoute.ts index 64025fdd5f9d..f435c283eddb 100644 --- a/packages/nextjs/src/client/tunnelRoute.ts +++ b/packages/nextjs/src/client/tunnelRoute.ts @@ -25,6 +25,12 @@ export function applyTunnelRouteOption(options: BrowserOptions): void { tunnelPath += `&r=${regionCode}`; } options.tunnel = tunnelPath; + // The tunnel route is same-origin, so fetch would include credentials (cookies, HTTP authentication) that the + // tunnel does not need. + options.transportOptions = { + ...options.transportOptions, + fetchOptions: { credentials: 'omit', ...options.transportOptions?.fetchOptions }, + }; DEBUG_BUILD && debug.log(`Tunneling events to "${tunnelPath}"`); } else { DEBUG_BUILD && debug.warn('Provided DSN is not a Sentry SaaS DSN. Will not tunnel events.'); diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index f1d3a8441bcb..84d097746781 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -276,6 +276,10 @@ export type SentryBuildOptions = Omit< * middleware redirects or blocks unauthenticated requests, exclude the tunnel route in its `matcher` so events * can reach Sentry. Matchers have to be static, so use a fixed string route in that case rather than `true`. * + * The browser sends tunnel requests with `credentials: 'omit'`, so without cookies or HTTP authentication. If a + * layer in front of your app needs them (for example Vercel Deployment Protection or HTTP Basic authentication), set + * `transportOptions: { fetchOptions: { credentials: 'same-origin' } }` in `Sentry.init` of `instrumentation-client.ts`. + * * NOTE: This feature only works with Next.js 11+ */ tunnelRoute?: string | boolean; diff --git a/packages/nextjs/test/utils/tunnelRoute.test.ts b/packages/nextjs/test/utils/tunnelRoute.test.ts index 8382e66ca7d4..d6b4339ecb54 100644 --- a/packages/nextjs/test/utils/tunnelRoute.test.ts +++ b/packages/nextjs/test/utils/tunnelRoute.test.ts @@ -118,3 +118,41 @@ describe('Random tunnel route generation', () => { expect(options.tunnel).toBeUndefined(); }); }); + +describe('applyTunnelRouteOption() credentials', () => { + const SAAS_DSN = 'https://11111111111111111111111111111111@o2222222.ingest.sentry.io/3333333'; + + beforeEach(() => { + globalWithInjectedValues._sentryRewritesTunnelPath = '/my-error-monitoring-route'; + }); + + it("sends tunnel requests with `credentials: 'omit'`", () => { + const options: BrowserOptions = { dsn: SAAS_DSN }; + + applyTunnelRouteOption(options); + + expect(options.transportOptions).toEqual({ fetchOptions: { credentials: 'omit' } }); + }); + + it('keeps the fetch options of the user', () => { + const options: BrowserOptions = { + dsn: SAAS_DSN, + transportOptions: { bufferSize: 10, fetchOptions: { credentials: 'same-origin', priority: 'low' } }, + }; + + applyTunnelRouteOption(options); + + expect(options.transportOptions).toEqual({ + bufferSize: 10, + fetchOptions: { credentials: 'same-origin', priority: 'low' }, + }); + }); + + it('does not change the transport options when the events are not tunneled', () => { + const options: BrowserOptions = { dsn: 'https://11111111111111111111111111111111@example.com/3333333' }; + + applyTunnelRouteOption(options); + + expect(options.transportOptions).toBeUndefined(); + }); +}); From b672c6ba2f035c9723105821406e1aa1eee5a89d Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:07:36 +0200 Subject: [PATCH 2/3] add pr number to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de60d2d7e348..16140571c31e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ ### Important Changes -- **feat(nextjs): Send tunnel route requests with `credentials: 'omit'` ([#PR](https://github.com/getsentry/sentry-javascript/pull/PR))** +- **feat(nextjs): Send tunnel route requests with `credentials: 'omit'` ([#25509](https://github.com/getsentry/sentry-javascript/pull/25509))** With `tunnelRoute`, `@sentry/nextjs` now sends events from the browser with `credentials: 'omit'`, because the tunnel route does not need cookies. If a layer in front of your app checks cookies before it lets requests through (for example Vercel Deployment Protection), you can send them again in your browser `Sentry.init` call: From 42f512321e9777266eca801022b32b80a77a10e8 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Tue, 20 Oct 2026 12:27:29 +0200 Subject: [PATCH 3/3] remove brwoser todo --- packages/browser/src/transports/fetch.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/browser/src/transports/fetch.ts b/packages/browser/src/transports/fetch.ts index 23d499bb66e1..83e3905de35c 100644 --- a/packages/browser/src/transports/fetch.ts +++ b/packages/browser/src/transports/fetch.ts @@ -38,8 +38,6 @@ export function makeFetchTransport( // There is also a limit of requests that can be open at the same time, so we also limit this to 15 // See https://github.com/getsentry/sentry-javascript/pull/7553 for details keepalive: pendingBodySize <= 60_000 && pendingCount < 15, - // TODO(v12): Default to `credentials: 'omit'`. With the fetch default `'same-origin'`, requests to a same-origin - // `tunnel` include credentials (cookies, HTTP authentication), which the tunnel does not need. ...options.fetchOptions, };