diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index e36e88802fa5..e43061eb59a5 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -205,7 +205,7 @@ function createReleaseConfig( vcsRemote: sentryBuildOptions.release?.vcsRemote, setCommits: sentryBuildOptions.release?.setCommits, deploy: sentryBuildOptions.release?.deploy, - ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.release, + ...sentryBuildOptions.webpack?.unstable_sentryWebpackPluginOptions?.release, }; } @@ -272,8 +272,8 @@ export function getBuildPluginOptions({ reactComponentAnnotation: buildTool.startsWith('after-production-compile') ? undefined : { - ...sentryBuildOptions.reactComponentAnnotation, - ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.reactComponentAnnotation, + ...sentryBuildOptions.webpack?.reactComponentAnnotation, + ...sentryBuildOptions.webpack?.unstable_sentryWebpackPluginOptions?.reactComponentAnnotation, }, silent: sentryBuildOptions.silent, url: sentryBuildOptions.sentryUrl, @@ -283,7 +283,7 @@ export function getBuildPluginOptions({ assets: sentryBuildOptions.sourcemaps?.assets ?? sourcemapUploadAssets, ignore: sentryBuildOptions.sourcemaps?.ignore ?? sourcemapUploadIgnore, filesToDeleteAfterUpload, - ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.sourcemaps, + ...sentryBuildOptions.webpack?.unstable_sentryWebpackPluginOptions?.sourcemaps, }, release: createReleaseConfig(releaseName, sentryBuildOptions), bundleSizeOptimizations: { @@ -295,6 +295,6 @@ export function getBuildPluginOptions({ metaFramework: 'nextjs', }, }, - ...sentryBuildOptions.unstable_sentryWebpackPluginOptions, + ...sentryBuildOptions.webpack?.unstable_sentryWebpackPluginOptions, }; } diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 28e038b6d0f2..c7472c08fc20 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -57,6 +57,91 @@ export type NextConfigObject = { }; }; +export type SentryBuildWebpackOptions = { + /** + * Automatically instrument Next.js data fetching methods and Next.js API routes with error and performance monitoring. + * Defaults to `true`. + */ + autoInstrumentServerFunctions?: boolean; + + /** + * Automatically instrument Next.js middleware with error and performance monitoring. Defaults to `true`. + */ + autoInstrumentMiddleware?: boolean; + + /** + * Automatically instrument components in the `app` directory with error monitoring. Defaults to `true`. + */ + autoInstrumentAppDirectory?: boolean; + + /** + * Automatically create cron monitors in Sentry for your Vercel Cron Jobs if configured via `vercel.json`. + * + * Defaults to `false`. + */ + automaticVercelMonitors?: boolean; + + /** + * Exclude certain serverside API routes or pages from being instrumented with Sentry during build-time. This option + * takes an array of strings or regular expressions. This options also affects pages in the `app` directory. + * + * NOTE: Pages should be specified as routes (`/animals` or `/api/animals/[animalType]/habitat`), not filepaths + * (`pages/animals/index.js` or `.\src\pages\api\animals\[animalType]\habitat.tsx`), and strings must be be a full, + * exact match. + * + * Notice: If you build Next.js with turbopack, the Sentry SDK will no longer apply build-time instrumentation and + * purely rely on Next.js telemetry features, meaning that this option will effectively no-op. + */ + excludeServerRoutes?: Array; + + /** + * Disables automatic injection of Sentry's Webpack configuration. + * + * By default, the Sentry Next.js SDK injects its own Webpack configuration to enable features such as + * source map upload and automatic instrumentation. Set this option to `true` if you want to prevent + * the SDK from modifying your Webpack config (for example, if you want to handle Sentry integration manually + * or if you are on an older version of Next.js while using Turbopack). + */ + disableSentryConfig?: boolean; + + /** + * Tree-shaking options to help reduce the size of the Sentry SDK bundle. + */ + treeshake?: { + /** + * Removes Sentry SDK logger statements from the bundle. Note that this doesn't affect Sentry Logs. + */ + removeDebugLogging?: boolean; + }; + + /** + * Options to be passed directly to the Sentry Webpack Plugin (`@sentry/webpack-plugin`) that ships with the Sentry SDK. + * You can use this option to override any options the SDK passes to the Webpack plugin. + * + * Please note that this option is unstable and may change in a breaking way in any release. + */ + unstable_sentryWebpackPluginOptions?: SentryWebpackPluginOptions; + + /** + * Options related to react component name annotations. + * Disabled by default, unless a value is set for this option. + * When enabled, your app's DOM will automatically be annotated during build-time with their respective component names. + * This will unlock the capability to search for Replays in Sentry by component name, as well as see component names in breadcrumbs and performance monitoring. + * Please note that this feature is not currently supported by the esbuild bundler plugins, and will only annotate React components + */ + reactComponentAnnotation?: { + /** + * Whether the component name annotate plugin should be enabled or not. + */ + enabled?: boolean; + + /** + * 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[]; + }; +}; + export type SentryBuildOptions = { /** * The slug of the Sentry organization associated with the app. @@ -363,6 +448,8 @@ export type SentryBuildOptions = { * When enabled, your app's DOM will automatically be annotated during build-time with their respective component names. * This will unlock the capability to search for Replays in Sentry by component name, as well as see component names in breadcrumbs and performance monitoring. * Please note that this feature is not currently supported by the esbuild bundler plugins, and will only annotate React components + * + * @deprecated Use `webpack.reactComponentAnnotation` instead. */ reactComponentAnnotation?: { /** @@ -381,6 +468,7 @@ export type SentryBuildOptions = { * You can use this option to override any options the SDK passes to the webpack plugin. * * 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; @@ -391,6 +479,8 @@ export type SentryBuildOptions = { * Disabling this option will leave you without readable stacktraces for dependencies and Next.js-internal code. * * Defaults to `false`. + * + * This option applies to both webpack and turbopack builds. */ // Enabling this option may upload a lot of source maps and since the sourcemap upload endpoint in Sentry is super // slow we don't enable it by default so that we don't opaquely increase build times for users. @@ -400,16 +490,19 @@ export type SentryBuildOptions = { /** * Automatically instrument Next.js data fetching methods and Next.js API routes with error and performance monitoring. * Defaults to `true`. + * @deprecated Use `webpack.autoInstrumentServerFunctions` instead. */ autoInstrumentServerFunctions?: boolean; /** * Automatically instrument Next.js middleware with error and performance monitoring. Defaults to `true`. + * @deprecated Use `webpack.autoInstrumentMiddleware` instead. */ autoInstrumentMiddleware?: boolean; /** * Automatically instrument components in the `app` directory with error monitoring. Defaults to `true`. + * @deprecated Use `webpack.autoInstrumentAppDirectory` instead. */ autoInstrumentAppDirectory?: boolean; @@ -423,6 +516,8 @@ export type SentryBuildOptions = { * * Notice: If you build Next.js with turbopack, the Sentry SDK will no longer apply build-time instrumentation and * purely rely on Next.js telemetry features, meaning that this option will effectively no-op. + * + * @deprecated Use `webpack.excludeServerRoutes` instead. */ excludeServerRoutes?: Array; @@ -439,6 +534,8 @@ export type SentryBuildOptions = { /** * Tree shakes Sentry SDK logger statements from the bundle. + * + * @deprecated Use `webpack.treeshake.removeDebugLogging` instead. */ disableLogger?: boolean; @@ -446,6 +543,8 @@ export type SentryBuildOptions = { * Automatically create cron monitors in Sentry for your Vercel Cron Jobs if configured via `vercel.json`. * * Defaults to `false`. + * + * @deprecated Use `webpack.automaticVercelMonitors` instead. */ automaticVercelMonitors?: boolean; @@ -497,6 +596,8 @@ export type SentryBuildOptions = { * the SDK from modifying your Webpack config (for example, if you want to handle Sentry integration manually * or if you are on an older version of Next.js while using Turbopack). * + * @deprecated Use `webpack.disableSentryConfig` instead. + * * @default false */ disableSentryWebpackConfig?: boolean; @@ -519,6 +620,11 @@ export type SentryBuildOptions = { _experimental?: Partial<{ thirdPartyOriginStackFrames?: boolean; }>; + + /** + * Options related to webpack builds, has no effect if you are using Turbopack. + */ + webpack?: SentryBuildWebpackOptions; }; export type NextConfigFunction = ( diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index df32c31f392e..6e38a3f08440 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -145,7 +145,7 @@ export function constructWebpackConfigFunction({ appDir: appDirPath, pagesDir: pagesDirPath, pageExtensionRegex, - excludeServerRoutes: userSentryOptions.excludeServerRoutes, + excludeServerRoutes: userSentryOptions.webpack?.excludeServerRoutes, nextjsRequestAsyncStorageModulePath: getRequestAsyncStorageModuleLocation( projectDir, rawNewConfig.resolve?.modules, @@ -220,7 +220,7 @@ export function constructWebpackConfigFunction({ ); }; - if (isServer && userSentryOptions.autoInstrumentServerFunctions !== false) { + if (isServer && userSentryOptions.webpack?.autoInstrumentServerFunctions !== false) { // It is very important that we insert our loaders at the beginning of the array because we expect any sort of transformations/transpilations (e.g. TS -> JS) to already have happened. // Wrap pages @@ -239,7 +239,7 @@ export function constructWebpackConfigFunction({ let vercelCronsConfig: VercelCronsConfig = undefined; try { - if (process.env.VERCEL && userSentryOptions.automaticVercelMonitors) { + if (process.env.VERCEL && userSentryOptions.webpack?.automaticVercelMonitors) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access vercelCronsConfig = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'vercel.json'), 'utf8')).crons; if (vercelCronsConfig) { @@ -277,7 +277,7 @@ export function constructWebpackConfigFunction({ // Wrap middleware const canWrapStandaloneMiddleware = userNextConfig.output !== 'standalone' || !major || major < 16; - if ((userSentryOptions.autoInstrumentMiddleware ?? true) && canWrapStandaloneMiddleware) { + if ((userSentryOptions.webpack?.autoInstrumentMiddleware ?? true) && canWrapStandaloneMiddleware) { newConfig.module.rules.unshift({ test: isMiddlewareResource, use: [ @@ -293,7 +293,7 @@ export function constructWebpackConfigFunction({ } } - if (isServer && userSentryOptions.autoInstrumentAppDirectory !== false) { + if (isServer && userSentryOptions.webpack?.autoInstrumentAppDirectory !== false) { // Wrap server components newConfig.module.rules.unshift({ test: isServerComponentResource, @@ -431,7 +431,7 @@ export function constructWebpackConfigFunction({ } } - if (userSentryOptions.disableLogger) { + if (userSentryOptions.webpack?.treeshake?.removeDebugLogging) { newConfig.plugins = newConfig.plugins || []; newConfig.plugins.push( new buildContext.webpack.DefinePlugin({ diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 892f4d6745fa..835ef6dc68a4 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -98,12 +98,102 @@ function generateRandomTunnelRoute(): string { return `/${randomString}`; } +/** + * Migrates deprecated top-level webpack options to the new `webpack.*` path for backward compatibility. + * The new path takes precedence over deprecated options. This mutates the userSentryOptions object. + */ +function migrateDeprecatedWebpackOptions(userSentryOptions: SentryBuildOptions): void { + // Initialize webpack options if not present + userSentryOptions.webpack = userSentryOptions.webpack || {}; + + const webpack = userSentryOptions.webpack; + + const withDeprecatedFallback = ( + newValue: T | undefined, + deprecatedValue: T | undefined, + message: string, + ): T | undefined => { + if (deprecatedValue !== undefined) { + // eslint-disable-next-line no-console + console.warn(message); + } + + return newValue ?? deprecatedValue; + }; + + const deprecatedMessage = (deprecatedPath: string, newPath: string): string => + `[@sentry/nextjs] DEPRECATION WARNING: ${deprecatedPath} is deprecated and will be removed in a future version. Use ${newPath} instead.`; + + /* eslint-disable deprecation/deprecation */ + // Migrate each deprecated option to the new path, but only if the new path isn't already set + webpack.autoInstrumentServerFunctions = withDeprecatedFallback( + webpack.autoInstrumentServerFunctions, + userSentryOptions.autoInstrumentServerFunctions, + deprecatedMessage('autoInstrumentServerFunctions', 'webpack.autoInstrumentServerFunctions'), + ); + + webpack.autoInstrumentMiddleware = withDeprecatedFallback( + webpack.autoInstrumentMiddleware, + userSentryOptions.autoInstrumentMiddleware, + deprecatedMessage('autoInstrumentMiddleware', 'webpack.autoInstrumentMiddleware'), + ); + + webpack.autoInstrumentAppDirectory = withDeprecatedFallback( + webpack.autoInstrumentAppDirectory, + userSentryOptions.autoInstrumentAppDirectory, + deprecatedMessage('autoInstrumentAppDirectory', 'webpack.autoInstrumentAppDirectory'), + ); + + webpack.excludeServerRoutes = withDeprecatedFallback( + webpack.excludeServerRoutes, + userSentryOptions.excludeServerRoutes, + deprecatedMessage('excludeServerRoutes', 'webpack.excludeServerRoutes'), + ); + + webpack.unstable_sentryWebpackPluginOptions = withDeprecatedFallback( + webpack.unstable_sentryWebpackPluginOptions, + userSentryOptions.unstable_sentryWebpackPluginOptions, + deprecatedMessage('unstable_sentryWebpackPluginOptions', 'webpack.unstable_sentryWebpackPluginOptions'), + ); + + webpack.disableSentryConfig = withDeprecatedFallback( + webpack.disableSentryConfig, + userSentryOptions.disableSentryWebpackConfig, + deprecatedMessage('disableSentryWebpackConfig', 'webpack.disableSentryConfig'), + ); + + // Handle treeshake.removeDebugLogging specially since it's nested + if (userSentryOptions.disableLogger !== undefined) { + webpack.treeshake = webpack.treeshake || {}; + webpack.treeshake.removeDebugLogging = withDeprecatedFallback( + webpack.treeshake.removeDebugLogging, + userSentryOptions.disableLogger, + deprecatedMessage('disableLogger', 'webpack.treeshake.removeDebugLogging'), + ); + } + + webpack.automaticVercelMonitors = withDeprecatedFallback( + webpack.automaticVercelMonitors, + userSentryOptions.automaticVercelMonitors, + deprecatedMessage('automaticVercelMonitors', 'webpack.automaticVercelMonitors'), + ); + + webpack.reactComponentAnnotation = withDeprecatedFallback( + webpack.reactComponentAnnotation, + userSentryOptions.reactComponentAnnotation, + deprecatedMessage('reactComponentAnnotation', 'webpack.reactComponentAnnotation'), + ); +} + // Modify the materialized object form of the user's next config by deleting the `sentry` property and wrapping the // `webpack` property function getFinalConfigObject( incomingUserNextConfigObject: NextConfigObject, userSentryOptions: SentryBuildOptions, ): NextConfigObject { + // Migrate deprecated webpack options to new webpack path for backward compatibility + migrateDeprecatedWebpackOptions(userSentryOptions); + // Only determine a release name if release creation is not explicitly disabled // This prevents injection of Git commit hashes that break build determinism const shouldCreateRelease = userSentryOptions.release?.create !== false; @@ -363,7 +453,7 @@ function getFinalConfigObject( ], }, }), - ...(isWebpack && !userSentryOptions.disableSentryWebpackConfig + ...(isWebpack && !userSentryOptions.webpack?.disableSentryConfig ? { webpack: constructWebpackConfigFunction({ userNextConfig: incomingUserNextConfigObject, diff --git a/packages/nextjs/test/config/getBuildPluginOptions.test.ts b/packages/nextjs/test/config/getBuildPluginOptions.test.ts index 609183d198bb..3e95eadafc96 100644 --- a/packages/nextjs/test/config/getBuildPluginOptions.test.ts +++ b/packages/nextjs/test/config/getBuildPluginOptions.test.ts @@ -573,13 +573,15 @@ describe('getBuildPluginOptions', () => { create: true, vcsRemote: 'origin', }, - unstable_sentryWebpackPluginOptions: { - release: { - setCommits: { - auto: true, - }, - deploy: { - env: 'production', + webpack: { + unstable_sentryWebpackPluginOptions: { + release: { + setCommits: { + auto: true, + }, + deploy: { + env: 'production', + }, }, }, }, @@ -592,7 +594,7 @@ describe('getBuildPluginOptions', () => { buildTool: 'webpack-client', }); - // The unstable_sentryWebpackPluginOptions.release is spread at the end and may override base properties + // The webpack.unstable_sentryWebpackPluginOptions.release is spread at the end and may override base properties expect(result.release).toHaveProperty('setCommits.auto', true); expect(result.release).toHaveProperty('deploy.env', 'production'); }); @@ -603,12 +605,14 @@ describe('getBuildPluginOptions', () => { const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', project: 'test-project', - reactComponentAnnotation: { - enabled: true, - }, - unstable_sentryWebpackPluginOptions: { + webpack: { reactComponentAnnotation: { - enabled: false, // This will override the base setting + enabled: true, + }, + unstable_sentryWebpackPluginOptions: { + reactComponentAnnotation: { + enabled: false, // This will override the base setting + }, }, }, }; @@ -695,10 +699,12 @@ describe('getBuildPluginOptions', () => { const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', project: 'test-project', - unstable_sentryWebpackPluginOptions: { - applicationKey: 'test-app-key', - sourcemaps: { - disable: false, + webpack: { + unstable_sentryWebpackPluginOptions: { + applicationKey: 'test-app-key', + sourcemaps: { + disable: false, + }, }, }, }; diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index b67a05845a7e..ed4b96a78125 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -267,6 +267,189 @@ describe('withSentryConfig', () => { expect(finalConfig.turbopack).toBeUndefined(); }); + + describe('webpack configuration options path', () => { + afterEach(() => { + delete process.env.TURBOPACK; + vi.restoreAllMocks(); + }); + + it('uses new webpack.disableSentryConfig option', () => { + delete process.env.TURBOPACK; + + const originalWebpackFunction = vi.fn(); + const configWithWebpack = { + ...exportedNextConfig, + webpack: originalWebpackFunction, + }; + + const sentryOptions = { + webpack: { + disableSentryConfig: true, + }, + }; + + const finalConfig = materializeFinalNextConfig(configWithWebpack, undefined, sentryOptions); + expect(finalConfig.webpack).toBe(originalWebpackFunction); + }); + + it('new webpack path takes precedence over deprecated top-level options', () => { + delete process.env.TURBOPACK; + + const originalWebpackFunction = vi.fn(); + const configWithWebpack = { + ...exportedNextConfig, + webpack: originalWebpackFunction, + }; + + // Both old and new paths set, new should win + const sentryOptions = { + disableSentryWebpackConfig: false, // deprecated - says enable + webpack: { + disableSentryConfig: true, // new - says disable + }, + }; + + const finalConfig = materializeFinalNextConfig(configWithWebpack, undefined, sentryOptions); + // Should preserve original webpack because new path disables it + expect(finalConfig.webpack).toBe(originalWebpackFunction); + }); + + it('falls back to deprecated option when new path is not set', () => { + delete process.env.TURBOPACK; + + const originalWebpackFunction = vi.fn(); + const configWithWebpack = { + ...exportedNextConfig, + webpack: originalWebpackFunction, + }; + + // Only deprecated path set + const sentryOptions = { + disableSentryWebpackConfig: true, + }; + + const finalConfig = materializeFinalNextConfig(configWithWebpack, undefined, sentryOptions); + // Should preserve original webpack because deprecated option disables it + expect(finalConfig.webpack).toBe(originalWebpackFunction); + }); + + it('merges webpack.treeshake.removeDebugLogging with deprecated disableLogger', () => { + delete process.env.TURBOPACK; + + // New webpack.treeshake.removeDebugLogging should map to disableLogger internally + const sentryOptionsNew = { + webpack: { + treeshake: { + removeDebugLogging: true, + }, + }, + }; + + const sentryOptionsOld = { + disableLogger: true, + }; + + // Both should work the same way internally (though we can't easily test the actual effect here) + const finalConfigNew = materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptionsNew); + const finalConfigOld = materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptionsOld); + + // Both should have webpack functions (not disabled) + expect(finalConfigNew.webpack).toBeInstanceOf(Function); + expect(finalConfigOld.webpack).toBeInstanceOf(Function); + }); + }); + + describe('deprecation warnings', () => { + let consoleWarnSpy: ReturnType; + + beforeEach(() => { + consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleWarnSpy.mockRestore(); + delete process.env.TURBOPACK; + vi.restoreAllMocks(); + }); + + it('warns when using deprecated top-level options', () => { + delete process.env.TURBOPACK; + + const sentryOptions = { + disableLogger: true, + }; + + materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining('[@sentry/nextjs] DEPRECATION WARNING: disableLogger is deprecated'), + ); + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining('Use webpack.treeshake.removeDebugLogging instead'), + ); + }); + + it('does not warn when using new webpack path', () => { + delete process.env.TURBOPACK; + + const sentryOptions = { + webpack: { + treeshake: { + removeDebugLogging: true, + }, + }, + }; + + materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + expect(consoleWarnSpy).not.toHaveBeenCalled(); + }); + + it('warns even when new path is also set', () => { + delete process.env.TURBOPACK; + + const sentryOptions = { + disableLogger: true, // deprecated + webpack: { + treeshake: { + removeDebugLogging: false, // new path takes precedence + }, + }, + }; + + materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + // Should warn because deprecated value is present + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining('[@sentry/nextjs] DEPRECATION WARNING: disableLogger is deprecated'), + ); + }); + + it('warns for multiple deprecated options at once', () => { + delete process.env.TURBOPACK; + + const sentryOptions = { + disableLogger: true, + automaticVercelMonitors: false, + excludeServerRoutes: ['/api/test'], + }; + + materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + // Should warn for all three deprecated options + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining('[@sentry/nextjs] DEPRECATION WARNING: disableLogger is deprecated'), + ); + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining('[@sentry/nextjs] DEPRECATION WARNING: automaticVercelMonitors is deprecated'), + ); + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining('[@sentry/nextjs] DEPRECATION WARNING: excludeServerRoutes is deprecated'), + ); + expect(consoleWarnSpy).toHaveBeenCalledTimes(3); + }); + }); }); describe('bundler detection', () => {