From 8e515a8981974fa0157df3baba861442e472cf04 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 13 Jan 2026 16:31:13 +0200 Subject: [PATCH 1/6] feat: added routeManifestInjection exclude options to disable manifest for specific pages --- packages/nextjs/src/config/types.ts | 55 +++++- .../getFinalConfigObjectUtils.ts | 36 +++- .../excludeRoutesFromManifest.test.ts | 175 ++++++++++++++++++ 3 files changed, 257 insertions(+), 9 deletions(-) create mode 100644 packages/nextjs/test/config/manifest/excludeRoutesFromManifest.test.ts diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 085e5c874184..e7c70ac44267 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -603,20 +603,59 @@ export type SentryBuildOptions = { /** * Disables automatic injection of the route manifest into the client bundle. * + * @deprecated Use `routeManifestInjection: false` instead. + * + * @default false + */ + disableManifestInjection?: boolean; + + /** + * Options for the route manifest injection feature. + * * The route manifest is a build-time generated mapping of your Next.js App Router * routes that enables Sentry to group transactions by parameterized route names * (e.g., `/users/:id` instead of `/users/123`, `/users/456`, etc.). * - * **Disable this option if:** - * - You want to minimize client bundle size - * - You're experiencing build issues related to route scanning - * - You're using custom routing that the scanner can't detect - * - You prefer raw URLs in transaction names - * - You're only using Pages Router (this feature is only supported in the App Router) + * Set to `false` to disable route manifest injection entirely. * - * @default false + * @example + * ```js + * // Disable route manifest injection + * routeManifestInjection: false + * + * // Exclude specific routes + * routeManifestInjection: { + * exclude: [ + * '/admin', // Exact match + * /^\/internal\//, // Regex: all routes starting with /internal/ + * /\/secret-/, // Regex: any route containing /secret- + * ] + * } + * + * // Exclude using a function + * routeManifestInjection: { + * exclude: (route) => route.includes('hidden') + * } + * ``` */ - disableManifestInjection?: boolean; + routeManifestInjection?: + | false + | { + /** + * Exclude specific routes from the route manifest. + * + * Use this option to prevent certain routes from being included in the client bundle's + * route manifest. This is useful for: + * - Hiding confidential or unreleased feature routes + * - Excluding internal/admin routes you don't want exposed + * - Reducing bundle size by omitting rarely-used routes + * + * Can be specified as: + * - An array of strings (exact match) or RegExp patterns + * - A function that receives a route path and returns `true` to exclude it + */ + exclude?: Array | ((route: string) => boolean); + }; /** * Disables automatic injection of Sentry's Webpack configuration. diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts index 469d3e02cc4f..f60da00ecac2 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts @@ -89,13 +89,47 @@ export function maybeCreateRouteManifest( incomingUserNextConfigObject: NextConfigObject, userSentryOptions: SentryBuildOptions, ): RouteManifest | undefined { + // Handle deprecated option with warning if (userSentryOptions.disableManifestInjection) { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/nextjs] The `disableManifestInjection` option is deprecated. Use `routeManifestInjection: false` instead.', + ); + } + + // Check if manifest injection is disabled (new option takes precedence) + if (userSentryOptions.routeManifestInjection === false || userSentryOptions.disableManifestInjection) { return undefined; } - return createRouteManifest({ + const manifest = createRouteManifest({ basePath: incomingUserNextConfigObject.basePath, }); + + // Apply route exclusion filter if configured + const excludeFilter = userSentryOptions.routeManifestInjection?.exclude; + if (!excludeFilter) { + return manifest; + } + + const shouldExclude = (route: string): boolean => { + if (typeof excludeFilter === 'function') { + return excludeFilter(route); + } + + return excludeFilter.some(pattern => { + if (typeof pattern === 'string') { + return route === pattern; + } + return pattern.test(route); + }); + }; + + return { + staticRoutes: manifest.staticRoutes.filter(r => !shouldExclude(r.path)), + dynamicRoutes: manifest.dynamicRoutes.filter(r => !shouldExclude(r.path)), + isrRoutes: manifest.isrRoutes.filter(r => !shouldExclude(r)), + }; } /** diff --git a/packages/nextjs/test/config/manifest/excludeRoutesFromManifest.test.ts b/packages/nextjs/test/config/manifest/excludeRoutesFromManifest.test.ts new file mode 100644 index 000000000000..bf7a031619f5 --- /dev/null +++ b/packages/nextjs/test/config/manifest/excludeRoutesFromManifest.test.ts @@ -0,0 +1,175 @@ +import { describe, expect, it } from 'vitest'; +import type { RouteManifest } from '../../../src/config/manifest/types'; +import type { SentryBuildOptions } from '../../../src/config/types'; + +type RouteManifestInjectionOptions = Exclude; +type ExcludeFilter = RouteManifestInjectionOptions['exclude']; + +// Inline the filtering logic for unit testing +// This mirrors what maybeCreateRouteManifest does internally +function filterManifest(manifest: RouteManifest, excludeFilter: ExcludeFilter): RouteManifest { + if (!excludeFilter) { + return manifest; + } + + const shouldExclude = (route: string): boolean => { + if (typeof excludeFilter === 'function') { + return excludeFilter(route); + } + + return excludeFilter.some((pattern: string | RegExp) => { + if (typeof pattern === 'string') { + return route === pattern; + } + return pattern.test(route); + }); + }; + + return { + staticRoutes: manifest.staticRoutes.filter(r => !shouldExclude(r.path)), + dynamicRoutes: manifest.dynamicRoutes.filter(r => !shouldExclude(r.path)), + isrRoutes: manifest.isrRoutes.filter(r => !shouldExclude(r)), + }; +} + +describe('routeManifestInjection.exclude', () => { + const mockManifest: RouteManifest = { + staticRoutes: [ + { path: '/' }, + { path: '/about' }, + { path: '/admin' }, + { path: '/admin/dashboard' }, + { path: '/internal/secret' }, + { path: '/public/page' }, + ], + dynamicRoutes: [ + { path: '/users/:id', regex: '^/users/([^/]+)$', paramNames: ['id'] }, + { path: '/admin/users/:id', regex: '^/admin/users/([^/]+)$', paramNames: ['id'] }, + { path: '/secret-feature/:id', regex: '^/secret-feature/([^/]+)$', paramNames: ['id'] }, + ], + isrRoutes: ['/blog', '/admin/reports', '/internal/stats'], + }; + + describe('with no filter', () => { + it('should return manifest unchanged', () => { + const result = filterManifest(mockManifest, undefined); + expect(result).toEqual(mockManifest); + }); + }); + + describe('with string patterns', () => { + it('should exclude exact string matches', () => { + const result = filterManifest(mockManifest, ['/admin']); + + expect(result.staticRoutes.map(r => r.path)).toEqual([ + '/', + '/about', + '/admin/dashboard', // Not excluded - not exact match + '/internal/secret', + '/public/page', + ]); + }); + + it('should exclude multiple exact matches', () => { + const result = filterManifest(mockManifest, ['/admin', '/about', '/blog']); + + expect(result.staticRoutes.map(r => r.path)).toEqual([ + '/', + '/admin/dashboard', + '/internal/secret', + '/public/page', + ]); + expect(result.isrRoutes).toEqual(['/admin/reports', '/internal/stats']); + }); + }); + + describe('with regex patterns', () => { + it('should exclude routes matching regex', () => { + const result = filterManifest(mockManifest, [/^\/admin/]); + + expect(result.staticRoutes.map(r => r.path)).toEqual(['/', '/about', '/internal/secret', '/public/page']); + expect(result.dynamicRoutes.map(r => r.path)).toEqual(['/users/:id', '/secret-feature/:id']); + expect(result.isrRoutes).toEqual(['/blog', '/internal/stats']); + }); + + it('should support multiple regex patterns', () => { + const result = filterManifest(mockManifest, [/^\/admin/, /^\/internal/]); + + expect(result.staticRoutes.map(r => r.path)).toEqual(['/', '/about', '/public/page']); + expect(result.isrRoutes).toEqual(['/blog']); + }); + + it('should support partial regex matches', () => { + const result = filterManifest(mockManifest, [/secret/]); + + expect(result.staticRoutes.map(r => r.path)).toEqual([ + '/', + '/about', + '/admin', + '/admin/dashboard', + '/public/page', + ]); + expect(result.dynamicRoutes.map(r => r.path)).toEqual(['/users/:id', '/admin/users/:id']); + }); + }); + + describe('with mixed patterns', () => { + it('should support both strings and regex', () => { + const result = filterManifest(mockManifest, ['/about', /^\/admin/]); + + expect(result.staticRoutes.map(r => r.path)).toEqual(['/', '/internal/secret', '/public/page']); + }); + }); + + describe('with function filter', () => { + it('should exclude routes where function returns true', () => { + const result = filterManifest(mockManifest, (route: string) => route.includes('admin')); + + expect(result.staticRoutes.map(r => r.path)).toEqual(['/', '/about', '/internal/secret', '/public/page']); + expect(result.dynamicRoutes.map(r => r.path)).toEqual(['/users/:id', '/secret-feature/:id']); + expect(result.isrRoutes).toEqual(['/blog', '/internal/stats']); + }); + + it('should support complex filter logic', () => { + const result = filterManifest(mockManifest, (route: string) => { + // Exclude anything with "secret" or "internal" or admin routes + return route.includes('secret') || route.includes('internal') || route.startsWith('/admin'); + }); + + expect(result.staticRoutes.map(r => r.path)).toEqual(['/', '/about', '/public/page']); + expect(result.dynamicRoutes.map(r => r.path)).toEqual(['/users/:id']); + expect(result.isrRoutes).toEqual(['/blog']); + }); + }); + + describe('edge cases', () => { + it('should handle empty manifest', () => { + const emptyManifest: RouteManifest = { + staticRoutes: [], + dynamicRoutes: [], + isrRoutes: [], + }; + + const result = filterManifest(emptyManifest, [/admin/]); + expect(result).toEqual(emptyManifest); + }); + + it('should handle filter that excludes everything', () => { + const result = filterManifest(mockManifest, () => true); + + expect(result.staticRoutes).toEqual([]); + expect(result.dynamicRoutes).toEqual([]); + expect(result.isrRoutes).toEqual([]); + }); + + it('should handle filter that excludes nothing', () => { + const result = filterManifest(mockManifest, () => false); + expect(result).toEqual(mockManifest); + }); + + it('should handle empty filter array', () => { + const result = filterManifest(mockManifest, []); + expect(result).toEqual(mockManifest); + }); + }); +}); From f3389836139455e2609871cfcafc794c86fc1ef1 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 13 Jan 2026 23:44:05 +0200 Subject: [PATCH 2/6] fix: lint --- .../src/config/withSentryConfig/getFinalConfigObjectUtils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts index f60da00ecac2..79ef3c84f7ec 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts @@ -90,6 +90,7 @@ export function maybeCreateRouteManifest( userSentryOptions: SentryBuildOptions, ): RouteManifest | undefined { // Handle deprecated option with warning + // eslint-disable-next-line deprecation/deprecation if (userSentryOptions.disableManifestInjection) { // eslint-disable-next-line no-console console.warn( @@ -98,6 +99,7 @@ export function maybeCreateRouteManifest( } // Check if manifest injection is disabled (new option takes precedence) + // eslint-disable-next-line deprecation/deprecation if (userSentryOptions.routeManifestInjection === false || userSentryOptions.disableManifestInjection) { return undefined; } From 8919cc7b8a46630786656da3eb0a10e9b9b78c68 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 13 Jan 2026 23:54:21 +0200 Subject: [PATCH 3/6] fix: correctly priortize the new option --- .../config/withSentryConfig/getFinalConfigObjectUtils.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts index 79ef3c84f7ec..c458459ec4a4 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts @@ -98,9 +98,14 @@ export function maybeCreateRouteManifest( ); } - // Check if manifest injection is disabled (new option takes precedence) + // If explicitly disabled, skip + if (userSentryOptions.routeManifestInjection === false) { + return undefined; + } + + // Still check the deprecated option if the new option is not set // eslint-disable-next-line deprecation/deprecation - if (userSentryOptions.routeManifestInjection === false || userSentryOptions.disableManifestInjection) { + if (userSentryOptions.routeManifestInjection === undefined && userSentryOptions.disableManifestInjection) { return undefined; } From 7acc2c8e7e5fee8c7deb211c322b163508b99a58 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 14 Jan 2026 00:01:48 +0200 Subject: [PATCH 4/6] fix: use match instead and export fn for testing --- .../getFinalConfigObjectUtils.ts | 13 +++- .../excludeRoutesFromManifest.test.ts | 76 ++++++++----------- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts index c458459ec4a4..d84c2144a8ee 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts @@ -115,6 +115,16 @@ export function maybeCreateRouteManifest( // Apply route exclusion filter if configured const excludeFilter = userSentryOptions.routeManifestInjection?.exclude; + return filterRouteManifest(manifest, excludeFilter); +} + +type ExcludeFilter = ((route: string) => boolean) | (string | RegExp)[] | undefined; + +/** + * Filters routes from the manifest based on the exclude filter. + * (Exported only for testing) + */ +export function filterRouteManifest(manifest: RouteManifest, excludeFilter: ExcludeFilter): RouteManifest { if (!excludeFilter) { return manifest; } @@ -128,7 +138,8 @@ export function maybeCreateRouteManifest( if (typeof pattern === 'string') { return route === pattern; } - return pattern.test(route); + + return !!route.match(pattern); }); }; diff --git a/packages/nextjs/test/config/manifest/excludeRoutesFromManifest.test.ts b/packages/nextjs/test/config/manifest/excludeRoutesFromManifest.test.ts index bf7a031619f5..6cdf968516c4 100644 --- a/packages/nextjs/test/config/manifest/excludeRoutesFromManifest.test.ts +++ b/packages/nextjs/test/config/manifest/excludeRoutesFromManifest.test.ts @@ -1,36 +1,6 @@ import { describe, expect, it } from 'vitest'; import type { RouteManifest } from '../../../src/config/manifest/types'; -import type { SentryBuildOptions } from '../../../src/config/types'; - -type RouteManifestInjectionOptions = Exclude; -type ExcludeFilter = RouteManifestInjectionOptions['exclude']; - -// Inline the filtering logic for unit testing -// This mirrors what maybeCreateRouteManifest does internally -function filterManifest(manifest: RouteManifest, excludeFilter: ExcludeFilter): RouteManifest { - if (!excludeFilter) { - return manifest; - } - - const shouldExclude = (route: string): boolean => { - if (typeof excludeFilter === 'function') { - return excludeFilter(route); - } - - return excludeFilter.some((pattern: string | RegExp) => { - if (typeof pattern === 'string') { - return route === pattern; - } - return pattern.test(route); - }); - }; - - return { - staticRoutes: manifest.staticRoutes.filter(r => !shouldExclude(r.path)), - dynamicRoutes: manifest.dynamicRoutes.filter(r => !shouldExclude(r.path)), - isrRoutes: manifest.isrRoutes.filter(r => !shouldExclude(r)), - }; -} +import { filterRouteManifest } from '../../../src/config/withSentryConfig/getFinalConfigObjectUtils'; describe('routeManifestInjection.exclude', () => { const mockManifest: RouteManifest = { @@ -52,14 +22,14 @@ describe('routeManifestInjection.exclude', () => { describe('with no filter', () => { it('should return manifest unchanged', () => { - const result = filterManifest(mockManifest, undefined); + const result = filterRouteManifest(mockManifest, undefined); expect(result).toEqual(mockManifest); }); }); describe('with string patterns', () => { it('should exclude exact string matches', () => { - const result = filterManifest(mockManifest, ['/admin']); + const result = filterRouteManifest(mockManifest, ['/admin']); expect(result.staticRoutes.map(r => r.path)).toEqual([ '/', @@ -71,7 +41,7 @@ describe('routeManifestInjection.exclude', () => { }); it('should exclude multiple exact matches', () => { - const result = filterManifest(mockManifest, ['/admin', '/about', '/blog']); + const result = filterRouteManifest(mockManifest, ['/admin', '/about', '/blog']); expect(result.staticRoutes.map(r => r.path)).toEqual([ '/', @@ -85,7 +55,7 @@ describe('routeManifestInjection.exclude', () => { describe('with regex patterns', () => { it('should exclude routes matching regex', () => { - const result = filterManifest(mockManifest, [/^\/admin/]); + const result = filterRouteManifest(mockManifest, [/^\/admin/]); expect(result.staticRoutes.map(r => r.path)).toEqual(['/', '/about', '/internal/secret', '/public/page']); expect(result.dynamicRoutes.map(r => r.path)).toEqual(['/users/:id', '/secret-feature/:id']); @@ -93,14 +63,14 @@ describe('routeManifestInjection.exclude', () => { }); it('should support multiple regex patterns', () => { - const result = filterManifest(mockManifest, [/^\/admin/, /^\/internal/]); + const result = filterRouteManifest(mockManifest, [/^\/admin/, /^\/internal/]); expect(result.staticRoutes.map(r => r.path)).toEqual(['/', '/about', '/public/page']); expect(result.isrRoutes).toEqual(['/blog']); }); it('should support partial regex matches', () => { - const result = filterManifest(mockManifest, [/secret/]); + const result = filterRouteManifest(mockManifest, [/secret/]); expect(result.staticRoutes.map(r => r.path)).toEqual([ '/', @@ -111,11 +81,29 @@ describe('routeManifestInjection.exclude', () => { ]); expect(result.dynamicRoutes.map(r => r.path)).toEqual(['/users/:id', '/admin/users/:id']); }); + + it('should handle regex with global flag consistently across multiple routes', () => { + // Regex with `g` flag has stateful lastIndex - ensure it works correctly + const globalRegex = /admin/g; + const result = filterRouteManifest(mockManifest, [globalRegex]); + + // All admin routes should be excluded, not just every other one + expect(result.staticRoutes.map(r => r.path)).toEqual(['/', '/about', '/internal/secret', '/public/page']); + expect(result.dynamicRoutes.map(r => r.path)).toEqual(['/users/:id', '/secret-feature/:id']); + expect(result.isrRoutes).toEqual(['/blog', '/internal/stats']); + }); + + it('should handle regex with global and case-insensitive flags', () => { + const result = filterRouteManifest(mockManifest, [/ADMIN/gi]); + + expect(result.staticRoutes.map(r => r.path)).toEqual(['/', '/about', '/internal/secret', '/public/page']); + expect(result.dynamicRoutes.map(r => r.path)).toEqual(['/users/:id', '/secret-feature/:id']); + }); }); describe('with mixed patterns', () => { it('should support both strings and regex', () => { - const result = filterManifest(mockManifest, ['/about', /^\/admin/]); + const result = filterRouteManifest(mockManifest, ['/about', /^\/admin/]); expect(result.staticRoutes.map(r => r.path)).toEqual(['/', '/internal/secret', '/public/page']); }); @@ -123,7 +111,7 @@ describe('routeManifestInjection.exclude', () => { describe('with function filter', () => { it('should exclude routes where function returns true', () => { - const result = filterManifest(mockManifest, (route: string) => route.includes('admin')); + const result = filterRouteManifest(mockManifest, (route: string) => route.includes('admin')); expect(result.staticRoutes.map(r => r.path)).toEqual(['/', '/about', '/internal/secret', '/public/page']); expect(result.dynamicRoutes.map(r => r.path)).toEqual(['/users/:id', '/secret-feature/:id']); @@ -131,7 +119,7 @@ describe('routeManifestInjection.exclude', () => { }); it('should support complex filter logic', () => { - const result = filterManifest(mockManifest, (route: string) => { + const result = filterRouteManifest(mockManifest, (route: string) => { // Exclude anything with "secret" or "internal" or admin routes return route.includes('secret') || route.includes('internal') || route.startsWith('/admin'); }); @@ -150,12 +138,12 @@ describe('routeManifestInjection.exclude', () => { isrRoutes: [], }; - const result = filterManifest(emptyManifest, [/admin/]); + const result = filterRouteManifest(emptyManifest, [/admin/]); expect(result).toEqual(emptyManifest); }); it('should handle filter that excludes everything', () => { - const result = filterManifest(mockManifest, () => true); + const result = filterRouteManifest(mockManifest, () => true); expect(result.staticRoutes).toEqual([]); expect(result.dynamicRoutes).toEqual([]); @@ -163,12 +151,12 @@ describe('routeManifestInjection.exclude', () => { }); it('should handle filter that excludes nothing', () => { - const result = filterManifest(mockManifest, () => false); + const result = filterRouteManifest(mockManifest, () => false); expect(result).toEqual(mockManifest); }); it('should handle empty filter array', () => { - const result = filterManifest(mockManifest, []); + const result = filterRouteManifest(mockManifest, []); expect(result).toEqual(mockManifest); }); }); From 9fb1f8a40e0e583161df56d3c48b6b55f48fb013 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 14 Jan 2026 13:19:14 +0200 Subject: [PATCH 5/6] chore: added deprecation notices for v11 --- packages/nextjs/src/config/types.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index e7c70ac44267..cdc6e68f053d 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -491,7 +491,7 @@ export type SentryBuildOptions = { * A list of strings representing the names of components to ignore. The plugin will not apply `data-sentry` annotations on the DOM element for these components. */ ignoredComponents?: string[]; - }; + }; // TODO(v11): remove this option /** * Options to be passed directly to the Sentry Webpack Plugin (`@sentry/webpack-plugin`) that ships with the Sentry Next.js SDK. @@ -500,7 +500,7 @@ export type SentryBuildOptions = { * Please note that this option is unstable and may change in a breaking way in any release. * @deprecated Use `webpack.unstable_sentryWebpackPluginOptions` instead. */ - unstable_sentryWebpackPluginOptions?: SentryWebpackPluginOptions; + unstable_sentryWebpackPluginOptions?: SentryWebpackPluginOptions; // TODO(v11): remove this option /** * Include Next.js-internal code and code from dependencies when uploading source maps. @@ -522,19 +522,19 @@ export type SentryBuildOptions = { * Defaults to `true`. * @deprecated Use `webpack.autoInstrumentServerFunctions` instead. */ - autoInstrumentServerFunctions?: boolean; + autoInstrumentServerFunctions?: boolean; // TODO(v11): remove this option /** * Automatically instrument Next.js middleware with error and performance monitoring. Defaults to `true`. * @deprecated Use `webpack.autoInstrumentMiddleware` instead. */ - autoInstrumentMiddleware?: boolean; + autoInstrumentMiddleware?: boolean; // TODO(v11): remove this option /** * Automatically instrument components in the `app` directory with error monitoring. Defaults to `true`. * @deprecated Use `webpack.autoInstrumentAppDirectory` instead. */ - autoInstrumentAppDirectory?: boolean; + autoInstrumentAppDirectory?: boolean; // TODO(v11): remove this option /** * Exclude certain serverside API routes or pages from being instrumented with Sentry during build-time. This option @@ -567,7 +567,7 @@ export type SentryBuildOptions = { * * @deprecated Use `webpack.treeshake.removeDebugLogging` instead. */ - disableLogger?: boolean; + disableLogger?: boolean; // TODO(v11): remove this option /** * Automatically create cron monitors in Sentry for your Vercel Cron Jobs if configured via `vercel.json`. @@ -576,7 +576,7 @@ export type SentryBuildOptions = { * * @deprecated Use `webpack.automaticVercelMonitors` instead. */ - automaticVercelMonitors?: boolean; + automaticVercelMonitors?: boolean; // TODO(v11): remove this option /** * When an error occurs during release creation or sourcemaps upload, the plugin will call this function. @@ -607,7 +607,7 @@ export type SentryBuildOptions = { * * @default false */ - disableManifestInjection?: boolean; + disableManifestInjection?: boolean; // TODO(v11): remove this option /** * Options for the route manifest injection feature. @@ -669,7 +669,7 @@ export type SentryBuildOptions = { * * @default false */ - disableSentryWebpackConfig?: boolean; + disableSentryWebpackConfig?: boolean; // TODO(v11): remove this option /** * When true (and Next.js >= 15), use the runAfterProductionCompile hook to consolidate sourcemap uploads From fba9871bdb20b6a70b6af10eb785c0b662915efd Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 14 Jan 2026 13:30:46 +0200 Subject: [PATCH 6/6] refactor: use isMatchingPattern util for consistent SDK behavior --- .../getFinalConfigObjectUtils.ts | 10 +---- .../excludeRoutesFromManifest.test.ts | 41 ++++++++++--------- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts index d84c2144a8ee..b56fa1894362 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts @@ -1,4 +1,4 @@ -import { parseSemver } from '@sentry/core'; +import { isMatchingPattern, parseSemver } from '@sentry/core'; import { getSentryRelease } from '@sentry/node'; import { createRouteManifest } from '../manifest/createRouteManifest'; import type { RouteManifest } from '../manifest/types'; @@ -134,13 +134,7 @@ export function filterRouteManifest(manifest: RouteManifest, excludeFilter: Excl return excludeFilter(route); } - return excludeFilter.some(pattern => { - if (typeof pattern === 'string') { - return route === pattern; - } - - return !!route.match(pattern); - }); + return excludeFilter.some(pattern => isMatchingPattern(route, pattern)); }; return { diff --git a/packages/nextjs/test/config/manifest/excludeRoutesFromManifest.test.ts b/packages/nextjs/test/config/manifest/excludeRoutesFromManifest.test.ts index 6cdf968516c4..a22af530b332 100644 --- a/packages/nextjs/test/config/manifest/excludeRoutesFromManifest.test.ts +++ b/packages/nextjs/test/config/manifest/excludeRoutesFromManifest.test.ts @@ -28,28 +28,40 @@ describe('routeManifestInjection.exclude', () => { }); describe('with string patterns', () => { - it('should exclude exact string matches', () => { + it('should exclude routes containing the string pattern (substring match)', () => { const result = filterRouteManifest(mockManifest, ['/admin']); + // All routes containing '/admin' are excluded + expect(result.staticRoutes.map(r => r.path)).toEqual(['/', '/about', '/internal/secret', '/public/page']); + expect(result.dynamicRoutes.map(r => r.path)).toEqual(['/users/:id', '/secret-feature/:id']); + expect(result.isrRoutes).toEqual(['/blog', '/internal/stats']); + }); + + it('should exclude routes matching multiple string patterns', () => { + const result = filterRouteManifest(mockManifest, ['/about', '/blog']); + expect(result.staticRoutes.map(r => r.path)).toEqual([ '/', - '/about', - '/admin/dashboard', // Not excluded - not exact match + '/admin', + '/admin/dashboard', '/internal/secret', '/public/page', ]); + expect(result.isrRoutes).toEqual(['/admin/reports', '/internal/stats']); }); - it('should exclude multiple exact matches', () => { - const result = filterRouteManifest(mockManifest, ['/admin', '/about', '/blog']); + it('should match substrings anywhere in the route', () => { + // 'secret' matches '/internal/secret' and '/secret-feature/:id' + const result = filterRouteManifest(mockManifest, ['secret']); expect(result.staticRoutes.map(r => r.path)).toEqual([ '/', + '/about', + '/admin', '/admin/dashboard', - '/internal/secret', '/public/page', ]); - expect(result.isrRoutes).toEqual(['/admin/reports', '/internal/stats']); + expect(result.dynamicRoutes.map(r => r.path)).toEqual(['/users/:id', '/admin/users/:id']); }); }); @@ -82,19 +94,8 @@ describe('routeManifestInjection.exclude', () => { expect(result.dynamicRoutes.map(r => r.path)).toEqual(['/users/:id', '/admin/users/:id']); }); - it('should handle regex with global flag consistently across multiple routes', () => { - // Regex with `g` flag has stateful lastIndex - ensure it works correctly - const globalRegex = /admin/g; - const result = filterRouteManifest(mockManifest, [globalRegex]); - - // All admin routes should be excluded, not just every other one - expect(result.staticRoutes.map(r => r.path)).toEqual(['/', '/about', '/internal/secret', '/public/page']); - expect(result.dynamicRoutes.map(r => r.path)).toEqual(['/users/:id', '/secret-feature/:id']); - expect(result.isrRoutes).toEqual(['/blog', '/internal/stats']); - }); - - it('should handle regex with global and case-insensitive flags', () => { - const result = filterRouteManifest(mockManifest, [/ADMIN/gi]); + it('should handle case-insensitive regex', () => { + const result = filterRouteManifest(mockManifest, [/ADMIN/i]); expect(result.staticRoutes.map(r => r.path)).toEqual(['/', '/about', '/internal/secret', '/public/page']); expect(result.dynamicRoutes.map(r => r.path)).toEqual(['/users/:id', '/secret-feature/:id']);