Skip to content

feat(nextjs): Add option for auto-generated random tunnel route #16626

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down
23 changes: 21 additions & 2 deletions packages/nextjs/src/config/withSentryConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ export function withSentryConfig<C>(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}`;
}
Comment on lines +81 to +85
Copy link
Member

Choose a reason for hiding this comment

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

Purely out of interestet, because I don't have context on how the SDK works in this case - how do we make sure these routes don't show up as endpoints in the data? Is this done by setUpTunnelRewriteRules?

Copy link
Member Author

Choose a reason for hiding this comment

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

We have an event processor in place that will filter these out. That being said I still need to figure out some details regarding value injection and test this manually


// Modify the materialized object form of the user's next config by deleting the `sentry` property and wrapping the
// `webpack` property
function getFinalConfigObject(
Expand All @@ -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;
Comment on lines +110 to +111
Copy link
Member

Choose a reason for hiding this comment

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

Hm, is this the only way to pass the route around? Not a big fan of overwriting the passed in option but maybe this is bike-shedding.

Copy link
Member Author

Choose a reason for hiding this comment

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

Seemed to be the most KISS variant I could think of here, because we can prevent the case that we have two different values floating around for the route

Copy link
Member

Choose a reason for hiding this comment

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

Yea, I guess fair enough.

setUpTunnelRewriteRules(incomingUserNextConfigObject, resolvedTunnelRoute);
}
}

Expand Down Expand Up @@ -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;

Expand Down
37 changes: 37 additions & 0 deletions packages/nextjs/test/utils/tunnelRoute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Loading