diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index ffce8a8b0641..fe05624ba15e 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 for each build + * - 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/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 b94ce6187f97..4c6a4fb42ed5 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,14 @@ function getFinalConfigObject( ); } } else { - setUpTunnelRewriteRules(incomingUserNextConfigObject, userSentryOptions.tunnelRoute); + const resolvedTunnelRoute = + typeof userSentryOptions.tunnelRoute === 'boolean' + ? generateRandomTunnelRoute() + : userSentryOptions.tunnelRoute; + + // Update the global options object to use the resolved value everywhere + userSentryOptions.tunnelRoute = resolvedTunnelRoute; + setUpTunnelRewriteRules(incomingUserNextConfigObject, resolvedTunnelRoute); } } @@ -363,8 +379,11 @@ function setUpBuildTimeVariables( ): void { const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || ''; const basePath = userNextConfig.basePath ?? ''; + const rewritesTunnelPath = - userSentryOptions.tunnelRoute !== undefined && userNextConfig.output !== 'export' + userSentryOptions.tunnelRoute !== undefined && + userNextConfig.output !== 'export' && + typeof userSentryOptions.tunnelRoute === 'string' ? `${basePath}${userSentryOptions.tunnelRoute}` : undefined; diff --git a/packages/nextjs/test/utils/tunnelRoute.test.ts b/packages/nextjs/test/utils/tunnelRoute.test.ts index fb228375f1e0..8382e66ca7d4 100644 --- a/packages/nextjs/test/utils/tunnelRoute.test.ts +++ b/packages/nextjs/test/utils/tunnelRoute.test.ts @@ -81,3 +81,40 @@ describe('applyTunnelRouteOption()', () => { expect(options.tunnel).toBe('/my-error-monitoring-route?o=2222222&p=3333333&r=us'); }); }); + +describe('Random tunnel route generation', () => { + 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', + } 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('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', + } as BrowserOptions; + + applyTunnelRouteOption(options); + + expect(options.tunnel).toBeUndefined(); + }); +});