From 942a138535e95cc3f12d4d79f7070a75478cdbe3 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 18 Jun 2025 00:17:43 +0200 Subject: [PATCH 1/3] add tunnel route randomization --- packages/nextjs/src/config/types.ts | 5 +++- .../nextjs/src/config/withSentryConfig.ts | 27 +++++++++++++++---- .../nextjs/test/utils/tunnelRoute.test.ts | 25 +++++++++++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index ffce8a8b0641..6ade625a19c5 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -426,9 +426,12 @@ export type SentryBuildOptions = { * Tunnel Sentry requests through this route on the Next.js server, to circumvent ad-blockers blocking Sentry events * from being sent. This option should be a path (for example: '/error-monitoring'). * + * - Pass `true` to auto-generate a random, ad-blocker-resistant route + * - Pass a string path (e.g., '/monitoring') to use a custom route + * * NOTE: This feature only works with Next.js 11+ */ - tunnelRoute?: string; + tunnelRoute?: string | boolean; /** * Tree shakes Sentry SDK logger statements from the bundle. diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index b94ce6187f97..66f996bdb2e5 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -75,6 +75,15 @@ export function withSentryConfig(nextConfig?: C, sentryBuildOptions: SentryBu } } +/** + * Generates a random tunnel route path that's less likely to be blocked by ad-blockers + */ +function generateRandomTunnelRoute(): string { + // Generate a random 8-character alphanumeric string + const randomString = Math.random().toString(36).substring(2, 10); + return `/${randomString}`; +} + // Modify the materialized object form of the user's next config by deleting the `sentry` property and wrapping the // `webpack` property function getFinalConfigObject( @@ -93,7 +102,12 @@ function getFinalConfigObject( ); } } else { - setUpTunnelRewriteRules(incomingUserNextConfigObject, userSentryOptions.tunnelRoute); + const resolvedTunnelRoute = + typeof userSentryOptions.tunnelRoute === 'boolean' + ? generateRandomTunnelRoute() + : userSentryOptions.tunnelRoute; + + setUpTunnelRewriteRules(incomingUserNextConfigObject, resolvedTunnelRoute); } } @@ -363,10 +377,13 @@ function setUpBuildTimeVariables( ): void { const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || ''; const basePath = userNextConfig.basePath ?? ''; - const rewritesTunnelPath = - userSentryOptions.tunnelRoute !== undefined && userNextConfig.output !== 'export' - ? `${basePath}${userSentryOptions.tunnelRoute}` - : undefined; + + let rewritesTunnelPath: string | undefined; + if (userSentryOptions.tunnelRoute !== undefined && userNextConfig.output !== 'export') { + const resolvedTunnelRoute = + typeof userSentryOptions.tunnelRoute === 'boolean' ? generateRandomTunnelRoute() : userSentryOptions.tunnelRoute; + rewritesTunnelPath = `${basePath}${resolvedTunnelRoute}`; + } const buildTimeVariables: Record = { // Make sure that if we have a windows path, the backslashes are interpreted as such (rather than as escape diff --git a/packages/nextjs/test/utils/tunnelRoute.test.ts b/packages/nextjs/test/utils/tunnelRoute.test.ts index fb228375f1e0..04c8febdb398 100644 --- a/packages/nextjs/test/utils/tunnelRoute.test.ts +++ b/packages/nextjs/test/utils/tunnelRoute.test.ts @@ -81,3 +81,28 @@ describe('applyTunnelRouteOption()', () => { expect(options.tunnel).toBe('/my-error-monitoring-route?o=2222222&p=3333333&r=us'); }); }); + +describe('Random tunnel route generation', () => { + it('Generates random tunnel routes when tunnelRoute is true', () => { + globalWithInjectedValues._sentryRewritesTunnelPath = '/abc123def'; // Simulated random path + const options: any = { + dsn: 'https://11111111111111111111111111111111@o2222222.ingest.sentry.io/3333333', + } as BrowserOptions; + + applyTunnelRouteOption(options); + + expect(options.tunnel).toBe('/abc123def?o=2222222&p=3333333'); + expect(options.tunnel).toMatch(/^\/[a-z0-9]+\?o=2222222&p=3333333$/); + }); + + it('Does not apply tunnel route when tunnelRoute is false', () => { + globalWithInjectedValues._sentryRewritesTunnelPath = undefined; + const options: any = { + dsn: 'https://11111111111111111111111111111111@o2222222.ingest.sentry.io/3333333', + } as BrowserOptions; + + applyTunnelRouteOption(options); + + expect(options.tunnel).toBeUndefined(); + }); +}); From c81a5c73b8a9c348c6095797047176aed6c881ab Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 18 Jun 2025 00:25:25 +0200 Subject: [PATCH 2/3] extend description --- packages/nextjs/src/config/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 6ade625a19c5..fe05624ba15e 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -426,7 +426,7 @@ export type SentryBuildOptions = { * Tunnel Sentry requests through this route on the Next.js server, to circumvent ad-blockers blocking Sentry events * from being sent. This option should be a path (for example: '/error-monitoring'). * - * - Pass `true` to auto-generate a random, ad-blocker-resistant route + * - Pass `true` to auto-generate a random, ad-blocker-resistant route for each build * - Pass a string path (e.g., '/monitoring') to use a custom route * * NOTE: This feature only works with Next.js 11+ From e1d364e0197d57ddf318253d03d0b00e6e2a4e69 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 18 Jun 2025 10:48:00 +0200 Subject: [PATCH 3/3] consistent path usage --- packages/nextjs/src/config/webpack.ts | 4 +++- packages/nextjs/src/config/withSentryConfig.ts | 14 ++++++++------ packages/nextjs/test/utils/tunnelRoute.test.ts | 16 ++++++++++++++-- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 8898b3495ba9..aeef52d4e309 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -692,7 +692,9 @@ function addValueInjectionLoader( const isomorphicValues = { // `rewritesTunnel` set by the user in Next.js config _sentryRewritesTunnelPath: - userSentryOptions.tunnelRoute !== undefined && userNextConfig.output !== 'export' + userSentryOptions.tunnelRoute !== undefined && + userNextConfig.output !== 'export' && + typeof userSentryOptions.tunnelRoute === 'string' ? `${userNextConfig.basePath ?? ''}${userSentryOptions.tunnelRoute}` : undefined, diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 66f996bdb2e5..4c6a4fb42ed5 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -107,6 +107,8 @@ function getFinalConfigObject( ? generateRandomTunnelRoute() : userSentryOptions.tunnelRoute; + // Update the global options object to use the resolved value everywhere + userSentryOptions.tunnelRoute = resolvedTunnelRoute; setUpTunnelRewriteRules(incomingUserNextConfigObject, resolvedTunnelRoute); } } @@ -378,12 +380,12 @@ function setUpBuildTimeVariables( const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || ''; const basePath = userNextConfig.basePath ?? ''; - let rewritesTunnelPath: string | undefined; - if (userSentryOptions.tunnelRoute !== undefined && userNextConfig.output !== 'export') { - const resolvedTunnelRoute = - typeof userSentryOptions.tunnelRoute === 'boolean' ? generateRandomTunnelRoute() : userSentryOptions.tunnelRoute; - rewritesTunnelPath = `${basePath}${resolvedTunnelRoute}`; - } + const rewritesTunnelPath = + userSentryOptions.tunnelRoute !== undefined && + userNextConfig.output !== 'export' && + typeof userSentryOptions.tunnelRoute === 'string' + ? `${basePath}${userSentryOptions.tunnelRoute}` + : undefined; const buildTimeVariables: Record = { // Make sure that if we have a windows path, the backslashes are interpreted as such (rather than as escape diff --git a/packages/nextjs/test/utils/tunnelRoute.test.ts b/packages/nextjs/test/utils/tunnelRoute.test.ts index 04c8febdb398..8382e66ca7d4 100644 --- a/packages/nextjs/test/utils/tunnelRoute.test.ts +++ b/packages/nextjs/test/utils/tunnelRoute.test.ts @@ -83,7 +83,7 @@ describe('applyTunnelRouteOption()', () => { }); describe('Random tunnel route generation', () => { - it('Generates random tunnel routes when tunnelRoute is true', () => { + it('Works when tunnelRoute is true and generates random-looking paths', () => { globalWithInjectedValues._sentryRewritesTunnelPath = '/abc123def'; // Simulated random path const options: any = { dsn: 'https://11111111111111111111111111111111@o2222222.ingest.sentry.io/3333333', @@ -95,7 +95,19 @@ describe('Random tunnel route generation', () => { expect(options.tunnel).toMatch(/^\/[a-z0-9]+\?o=2222222&p=3333333$/); }); - it('Does not apply tunnel route when tunnelRoute is false', () => { + it('Works with region DSNs when tunnelRoute is true', () => { + globalWithInjectedValues._sentryRewritesTunnelPath = '/x7h9k2m'; // Simulated random path + const options: any = { + dsn: 'https://11111111111111111111111111111111@o2222222.ingest.eu.sentry.io/3333333', + } as BrowserOptions; + + applyTunnelRouteOption(options); + + expect(options.tunnel).toBe('/x7h9k2m?o=2222222&p=3333333&r=eu'); + expect(options.tunnel).toMatch(/^\/[a-z0-9]+\?o=2222222&p=3333333&r=eu$/); + }); + + it('Does not apply tunnel when tunnelRoute is false', () => { globalWithInjectedValues._sentryRewritesTunnelPath = undefined; const options: any = { dsn: 'https://11111111111111111111111111111111@o2222222.ingest.sentry.io/3333333',