From 4846226d53c25f6aeb8939c2114f36f713ded0cc Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Tue, 27 Oct 2026 17:42:01 +0100 Subject: [PATCH 1/3] feat(nextjs): Add `applicationKeyExcludeDependencies` option --- .../turbopack/constructTurbopackConfig.ts | 19 +++--- packages/nextjs/src/config/types.ts | 15 +++++ .../constructTurbopackConfig.test.ts | 59 +++++++++++++------ 3 files changed, 65 insertions(+), 28 deletions(-) diff --git a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts index 391dee0e39f1..eae3cf5c7d95 100644 --- a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts +++ b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts @@ -65,22 +65,21 @@ export function constructTurbopackConfig({ newConfig.rules = safelyAddTurbopackRule(newConfig.rules, { matcher, rule }); } - // Add module metadata injection loader for thirdPartyErrorFilterIntegration support. - // This loader tags modules with `_sentryModuleMetadata` so the integration can tell - // first-party code from third-party code. We intentionally do NOT use - // `condition: { not: 'foreign' }` so that node_modules are also tagged as first-party, - // matching the webpack plugin's BannerPlugin behavior (which injects into all chunks - // without excluding node_modules). The injected code is wrapped in a try-catch IIFE - // so it is safe even for node_modules with strict initialization order. - // We only exclude Next.js build polyfills which contain non-standard syntax that causes - // parse errors when any code is prepended (Turbopack re-parses the loader output). + // Dependencies are tagged by default to match webpack's per-chunk metadata injection. + // Next.js build polyfills contain non-standard syntax that fails when Turbopack re-parses loader output. // Server and edge modules are skipped because only the browser reads these tags. const applicationKey = userSentryOptions?.applicationKey; if (applicationKey && nextJsVersion && supportsTurbopackRuleCondition(nextJsVersion)) { newConfig.rules = safelyAddTurbopackRule(newConfig.rules, { matcher: '*.{ts,tsx,js,jsx,mjs,cjs}', rule: { - condition: { all: ['browser', { not: { path: /next\/dist\/build\/polyfills/ } }] }, + condition: { + all: [ + 'browser', + { not: { path: /next\/dist\/build\/polyfills/ } }, + ...(userSentryOptions?.applicationKeyExcludeDependencies ? [{ not: 'foreign' as const }] : []), + ], + }, loaders: [ { loader: path.resolve(__dirname, '..', 'loaders', 'moduleMetadataInjectionLoader.js'), diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 4e1e6bc2d9e3..7c990a480e51 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -225,6 +225,21 @@ export type SentryBuildOptions = Omit< */ applicationKey?: string; + /** + * Skip application key metadata injection for dependencies in Turbopack builds with Next.js 16+. + * Uses Turbopack's `foreign` condition, which includes `node_modules` and some Next.js internals. + * Requires `applicationKey`. Has no effect on webpack builds. + * + * This reduces per-module injection overhead, but chunks containing only dependencies may no + * longer be tagged as first-party. Only use with `*-exclusively-contains-third-party-frames` + * behaviours of `thirdPartyErrorFilterIntegration`, and accept that errors whose stacks are + * entirely in untagged chunks can be dropped or tagged as third-party. With `*-contains-*` + * behaviours, even one frame from an untagged chunk can cause this misclassification. + * + * @default false + */ + applicationKeyExcludeDependencies?: boolean; + /** * Metadata that should be associated with the built application. * diff --git a/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts b/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts index 3670ff6244e7..39aa27f8760a 100644 --- a/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts +++ b/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts @@ -990,20 +990,40 @@ describe('moduleMetadataInjection with applicationKey', () => { expect(rule.loaders[0]!.options.applicationKey).toBe('my-top-level-key'); }); - it('should only tag browser modules and exclude Next.js polyfills, not all foreign modules', () => { - const userNextConfig: NextConfigObject = {}; + it.each([undefined, false])( + 'includes dependencies when applicationKeyExcludeDependencies is %s', + applicationKeyExcludeDependencies => { + const userNextConfig: NextConfigObject = {}; + + const result = constructTurbopackConfig({ + userNextConfig, + userSentryOptions: { applicationKey: 'my-app', applicationKeyExcludeDependencies }, + nextJsVersion: '16.0.0', + }); + + const rule = result.rules!['*.{ts,tsx,js,jsx,mjs,cjs}'] as { condition?: unknown; loaders: unknown[] }; + expect(rule.condition).toEqual({ all: ['browser', { not: { path: /next\/dist\/build\/polyfills/ } }] }); + }, + ); + it('excludes dependencies from browser metadata injection when opted in', () => { const result = constructTurbopackConfig({ - userNextConfig, - userSentryOptions: { applicationKey: 'my-app' }, + userNextConfig: {}, + userSentryOptions: { applicationKey: 'my-app', applicationKeyExcludeDependencies: true }, nextJsVersion: '16.0.0', }); - const rule = result.rules!['*.{ts,tsx,js,jsx,mjs,cjs}'] as { condition?: unknown; loaders: unknown[] }; - // Unlike component annotation (which uses { not: 'foreign' }), metadata injection - // must cover node_modules to match the webpack plugin's BannerPlugin behavior. - // Only Next.js build polyfills are excluded because they have non-standard syntax. - expect(rule.condition).toEqual({ all: ['browser', { not: { path: /next\/dist\/build\/polyfills/ } }] }); + expect(result.rules!['*.{ts,tsx,js,jsx,mjs,cjs}']).toEqual({ + condition: { + all: ['browser', { not: { path: /next\/dist\/build\/polyfills/ } }, { not: 'foreign' }], + }, + loaders: [ + { + loader: '/mocked/path/to/moduleMetadataInjectionLoader.js', + options: { applicationKey: 'my-app' }, + }, + ], + }); }); it('should NOT add metadata loader rule when Next.js < 16', () => { @@ -1018,17 +1038,20 @@ describe('moduleMetadataInjection with applicationKey', () => { expect(result.rules!['*.{ts,tsx,js,jsx,mjs,cjs}']).toBeUndefined(); }); - it('should NOT add metadata loader rule when applicationKey is not set', () => { - const userNextConfig: NextConfigObject = {}; + it.each([undefined, true])( + 'does not inject metadata without applicationKey when exclusion is %s', + applicationKeyExcludeDependencies => { + const userNextConfig: NextConfigObject = {}; - const result = constructTurbopackConfig({ - userNextConfig, - userSentryOptions: {}, - nextJsVersion: '16.0.0', - }); + const result = constructTurbopackConfig({ + userNextConfig, + userSentryOptions: { applicationKeyExcludeDependencies }, + nextJsVersion: '16.0.0', + }); - expect(result.rules!['*.{ts,tsx,js,jsx,mjs,cjs}']).toBeUndefined(); - }); + expect(result.rules!['*.{ts,tsx,js,jsx,mjs,cjs}']).toBeUndefined(); + }, + ); it('should NOT add metadata loader rule when nextJsVersion is undefined', () => { const userNextConfig: NextConfigObject = {}; From 92a1d930b033e4e554a0f46d05ea59d9a5d78314 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Wed, 28 Oct 2026 14:52:23 +0100 Subject: [PATCH 2/3] test(nextjs): Cover application key dependency exclusion end to end Co-Authored-By: Codex --- .../nextjs-16/app/third-party-filter/page.tsx | 35 ++++++++++++------- .../fixtures/metadata-dependency/index.d.ts | 1 + .../fixtures/metadata-dependency/index.js | 3 ++ .../fixtures/metadata-dependency/package.json | 7 ++++ .../nextjs-16/instrumentation-client.ts | 5 ++- .../nextjs-16/next.config.ts | 1 + .../test-applications/nextjs-16/package.json | 6 ++++ .../nextjs-16/tests/build-output.test.ts | 21 +++++++++++ .../tests/third-party-filter.test.ts | 23 +++++++++++- 9 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/index.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/index.js create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/package.json diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/third-party-filter/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16/app/third-party-filter/page.tsx index b6b4bea80def..6fbaa229f1d3 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/app/third-party-filter/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/third-party-filter/page.tsx @@ -8,17 +8,28 @@ function throwFirstPartyError(): void { export default function Page() { return ( - + <> + + + ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/index.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/index.d.ts new file mode 100644 index 000000000000..ca7acab421df --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/index.d.ts @@ -0,0 +1 @@ +export function createDependencyError(): Error; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/index.js b/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/index.js new file mode 100644 index 000000000000..0ab52860144f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/index.js @@ -0,0 +1,3 @@ +export function createDependencyError() { + return new Error('application-key-dependency-error'); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/package.json new file mode 100644 index 000000000000..0811417bd955 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/package.json @@ -0,0 +1,7 @@ +{ + "name": "sentry-metadata-test-dependency", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "index.js" +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/instrumentation-client.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/instrumentation-client.ts index d0f5b13bb555..f1ad74d2ea85 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/instrumentation-client.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/instrumentation-client.ts @@ -9,7 +9,10 @@ Sentry.init({ integrations: [ Sentry.thirdPartyErrorFilterIntegration({ filterKeys: ['nextjs-16-e2e'], - behaviour: 'apply-tag-if-contains-third-party-frames', + behaviour: + process.env.NEXT_PUBLIC_EXCLUDE_APPLICATION_KEY_DEPENDENCIES === '1' + ? 'apply-tag-if-exclusively-contains-third-party-frames' + : 'apply-tag-if-contains-third-party-frames', }), ], // Verify Log type is available diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts index 6893bd77fdf7..aee951174d24 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts @@ -19,6 +19,7 @@ const nextConfig: NextConfig = { export default withSentryConfig(nextConfig, { silent: true, applicationKey: 'nextjs-16-e2e', + applicationKeyExcludeDependencies: process.env.NEXT_PUBLIC_EXCLUDE_APPLICATION_KEY_DEPENDENCIES === '1', reactComponentAnnotation: { enabled: true, }, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json index 4629e84a780d..330998a1e357 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json @@ -44,6 +44,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "require-in-the-middle": "^8", + "sentry-metadata-test-dependency": "file:../../test-applications/nextjs-16/fixtures/metadata-dependency", "zod": "^3.22.4" }, "devDependencies": { @@ -66,6 +67,11 @@ "sentryTest": { "runner": "namespace-profile-sentry-javascript", "variants": [ + { + "build-command": "NEXT_PUBLIC_EXCLUDE_APPLICATION_KEY_DEPENDENCIES=1 pnpm test:build", + "label": "nextjs-16 (exclude application key dependencies)", + "assert-command": "NEXT_PUBLIC_EXCLUDE_APPLICATION_KEY_DEPENDENCIES=1 pnpm test:assert" + }, { "build-command": "pnpm test:build-webpack", "label": "nextjs-16 (webpack)", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/build-output.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/build-output.test.ts index cc3dfde118af..613590b114ba 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/build-output.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/build-output.test.ts @@ -7,6 +7,27 @@ import { isVinext } from './isVinext'; test.skip(isVinext, 'Checks the build output of Next.js'); +test('excludes dependency metadata while preserving application metadata', () => { + test.skip(isDevMode || process.env.NEXT_PUBLIC_EXCLUDE_APPLICATION_KEY_DEPENDENCIES !== '1'); + + const staticDir = path.join(process.cwd(), '.next', 'static'); + const chunks = fs + .readdirSync(staticDir, { recursive: true }) + .filter(file => String(file).endsWith('.js')) + .map(file => fs.readFileSync(path.join(staticDir, String(file)), 'utf8')); + const dependencyChunks = chunks.filter(chunk => chunk.includes('application-key-dependency-error')); + const applicationChunks = chunks.filter(chunk => chunk.includes('first-party-error')); + + expect(dependencyChunks.length).toBeGreaterThan(0); + expect(applicationChunks.length).toBeGreaterThan(0); + for (const chunk of dependencyChunks) { + expect(chunk).not.toContain('_sentryBundlerPluginAppKey:nextjs-16-e2e'); + } + for (const chunk of applicationChunks) { + expect(chunk).toContain('_sentryBundlerPluginAppKey:nextjs-16-e2e'); + } +}); + test('emits no absolute-path imports into the server output', () => { const leaks = findAbsolutePathImports({ outputDir: path.join(process.cwd(), '.next', 'server') }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/third-party-filter.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/third-party-filter.test.ts index a89a8cf6645d..618b8712ecdd 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/third-party-filter.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/third-party-filter.test.ts @@ -13,6 +13,7 @@ test('First-party error with React frames should not be tagged as third-party co }); await page.goto('/third-party-filter'); + await page.waitForFunction(() => '__SENTRY__' in window); await page.locator('#first-party-error-btn').click(); const errorEvent = await errorPromise; @@ -22,7 +23,7 @@ test('First-party error with React frames should not be tagged as third-party co // In production, TEST_ENV=production is shared by both turbopack and webpack variants. // Only assert when the build is actually turbopack. if (errorEvent.tags?.turbopack) { - // The integration uses `apply-tag-if-contains-third-party-frames` which tags errors + // By default, the integration uses `apply-tag-if-contains-third-party-frames` which tags errors // if ANY frame is third-party. This error is thrown inside a React onClick handler, // so the stack trace contains React frames from node_modules. These must NOT be // treated as third-party — the module metadata injection must cover node_modules too @@ -30,3 +31,23 @@ test('First-party error with React frames should not be tagged as third-party co expect(errorEvent.tags?.third_party_code).toBeUndefined(); } }); + +test('retains application errors with dependency frames when dependency metadata is excluded', async ({ page }) => { + test.skip(process.env.NEXT_PUBLIC_EXCLUDE_APPLICATION_KEY_DEPENDENCIES !== '1'); + + const errorPromise = waitForError('nextjs-16', event => { + return event.exception?.values?.some(value => value.value === 'application-key-dependency-error') ?? false; + }); + + await page.goto('/third-party-filter'); + await page.waitForFunction(() => '__SENTRY__' in window); + await page.locator('#dependency-error-btn').click(); + + const event = await errorPromise; + const exception = event.exception?.values?.[0]; + expect(exception?.value).toBe('application-key-dependency-error'); + expect(exception?.mechanism).toEqual({ handled: true, type: 'generic' }); + expect(event.tags?.third_party_code).toBeUndefined(); + expect(event.tags?.turbopack).toBe(true); + expect(new Set(exception?.stacktrace?.frames?.map(frame => frame.filename)).size).toBeGreaterThan(1); +}); From b00119ccc4934695162740269d67ddb6ff4483dd Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Fri, 30 Oct 2026 15:12:35 +0100 Subject: [PATCH 3/3] fix(nextjs): Warn about ineffective application key exclusion Cover dependency-only error stacks and preserve metadata injection context. Co-Authored-By: GPT-6 --- .../nextjs-16/app/third-party-filter/page.tsx | 9 +++++ .../fixtures/metadata-dependency/index.d.ts | 1 + .../fixtures/metadata-dependency/index.js | 7 ++++ .../tests/third-party-filter.test.ts | 24 +++++++++++++ .../turbopack/constructTurbopackConfig.ts | 8 +++-- .../withSentryConfig/getFinalConfigObject.ts | 23 ++++++++++++ .../test/config/withSentryConfig.test.ts | 35 +++++++++++++++++++ 7 files changed, 105 insertions(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/third-party-filter/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16/app/third-party-filter/page.tsx index 6fbaa229f1d3..434d4ad3cada 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/app/third-party-filter/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/third-party-filter/page.tsx @@ -30,6 +30,15 @@ export default function Page() { > Capture Dependency Error + ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/index.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/index.d.ts index ca7acab421df..0071fa0b3ec2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/index.d.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/index.d.ts @@ -1 +1,2 @@ export function createDependencyError(): Error; +export function createAsyncDependencyError(): Promise; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/index.js b/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/index.js index 0ab52860144f..be62d583d1d3 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/index.js +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/fixtures/metadata-dependency/index.js @@ -1,3 +1,10 @@ export function createDependencyError() { return new Error('application-key-dependency-error'); } + +export function createAsyncDependencyError() { + return new Promise(resolve => { + // Wait until the application's call stack has unwound so the error stack contains only a dependency frame. + queueMicrotask(() => resolve(new Error('application-key-dependency-only-error'))); + }); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/third-party-filter.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/third-party-filter.test.ts index 618b8712ecdd..2c7104a2864d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/third-party-filter.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/third-party-filter.test.ts @@ -1,5 +1,6 @@ import test, { expect } from '@playwright/test'; import { waitForError } from '@sentry-internal/test-utils'; +import { isDevMode } from './isDevMode'; import { isVinext } from './isVinext'; const isWebpackDev = process.env.TEST_ENV === 'development-webpack'; @@ -51,3 +52,26 @@ test('retains application errors with dependency frames when dependency metadata expect(event.tags?.turbopack).toBe(true); expect(new Set(exception?.stacktrace?.frames?.map(frame => frame.filename)).size).toBeGreaterThan(1); }); + +test('classifies dependency-only errors according to dependency metadata inclusion', async ({ page }) => { + test.skip(isVinext || isDevMode, 'Checks dependency-only chunks in production Next.js builds'); + + const errorPromise = waitForError('nextjs-16', event => { + return event.exception?.values?.some(value => value.value === 'application-key-dependency-only-error') ?? false; + }); + + await page.goto('/third-party-filter'); + await page.waitForFunction(() => '__SENTRY__' in window); + await page.locator('#dependency-only-error-btn').click(); + + const event = await errorPromise; + const frames = event.exception?.values?.[0]?.stacktrace?.frames; + expect(frames).toHaveLength(1); + + const filename = frames![0]!.filename; + const chunkResponse = await page.request.get(filename!.replace('app://', '')); + expect(await chunkResponse.text()).toContain('application-key-dependency-only-error'); + expect(event.tags?.third_party_code).toBe( + process.env.NEXT_PUBLIC_EXCLUDE_APPLICATION_KEY_DEPENDENCIES === '1' ? true : undefined, + ); +}); diff --git a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts index eae3cf5c7d95..da0ca895c246 100644 --- a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts +++ b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts @@ -65,8 +65,12 @@ export function constructTurbopackConfig({ newConfig.rules = safelyAddTurbopackRule(newConfig.rules, { matcher, rule }); } - // Dependencies are tagged by default to match webpack's per-chunk metadata injection. - // Next.js build polyfills contain non-standard syntax that fails when Turbopack re-parses loader output. + // Tag modules so thirdPartyErrorFilterIntegration can identify first-party code. Dependencies + // are tagged by default to match webpack's BannerPlugin, which injects metadata into all chunks. + // Opting out with `applicationKeyExcludeDependencies` can leave dependency-only chunks untagged. + // The injected code is wrapped in a try-catch IIFE, so it is safe even for dependencies with + // strict initialization order. Next.js build polyfills are excluded because they contain + // non-standard syntax that causes parse errors when Turbopack re-parses the loader output. // Server and edge modules are skipped because only the browser reads these tags. const applicationKey = userSentryOptions?.applicationKey; if (applicationKey && nextJsVersion && supportsTurbopackRuleCondition(nextJsVersion)) { diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts index e855041202e1..6a2e4b164bb9 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts @@ -1,6 +1,8 @@ +import { getBuildLogger } from '../buildLogger'; import type { NextConfigObject, SentryBuildOptions } from '../types'; import { getNextjsVersion } from '../util'; import { setUpBuildTimeVariables } from './buildTime'; +import type { BundlerInfo } from './getFinalConfigObjectBundlerUtils'; import { getBundlerInfo, getServerExternalPackagesPatch, @@ -61,6 +63,7 @@ export function getFinalConfigObject( const bundlerInfo = getBundlerInfo(nextJsVersion); maybeWarnAboutUnsupportedTurbopack(nextJsVersion, bundlerInfo, userSentryOptions.silent); maybeWarnAboutTurbopackModuleMetadata(userSentryOptions, bundlerInfo); + maybeWarnAboutApplicationKeyExcludeDependencies(userSentryOptions, bundlerInfo); maybeWarnAboutUnsupportedRunAfterProductionCompileHook(nextJsVersion, userSentryOptions, bundlerInfo); const turboPackConfig = maybeConstructTurbopackConfig( @@ -108,3 +111,23 @@ export function getFinalConfigObject( ...getTurbopackPatch(bundlerInfo, turboPackConfig), }; } + +function maybeWarnAboutApplicationKeyExcludeDependencies( + userSentryOptions: SentryBuildOptions, + bundlerInfo: BundlerInfo, +): void { + if (!userSentryOptions.applicationKeyExcludeDependencies) { + return; + } + + const logger = getBuildLogger(userSentryOptions.silent); + if (!userSentryOptions.applicationKey) { + logger.warn( + '[@sentry/nextjs] `applicationKeyExcludeDependencies` requires `applicationKey` and has no effect without it.', + ); + } else if (bundlerInfo.isWebpack) { + logger.warn( + '[@sentry/nextjs] `applicationKeyExcludeDependencies` only applies to Turbopack builds and has no effect on webpack builds.', + ); + } +} diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index cc749732eb35..9cec2cc71410 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -1302,6 +1302,41 @@ describe('withSentryConfig', () => { }); }); + describe('applicationKeyExcludeDependencies warnings', () => { + beforeEach(() => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('16.0.0'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('warns without applicationKey', () => { + vi.spyOn(util, 'detectActiveBundler').mockReturnValue('turbopack'); + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + materializeFinalNextConfig(exportedNextConfig, undefined, { applicationKeyExcludeDependencies: true }); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[@sentry/nextjs] `applicationKeyExcludeDependencies` requires `applicationKey` and has no effect without it.', + ); + }); + + it('warns on webpack when applicationKey is set', () => { + vi.spyOn(util, 'detectActiveBundler').mockReturnValue('webpack'); + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + materializeFinalNextConfig(exportedNextConfig, undefined, { + applicationKey: 'my-app', + applicationKeyExcludeDependencies: true, + }); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[@sentry/nextjs] `applicationKeyExcludeDependencies` only applies to Turbopack builds and has no effect on webpack builds.', + ); + }); + }); + describe('turbopack version compatibility warnings', () => { const originalTurbopack = process.env.TURBOPACK; const originalNodeEnv = process.env.NODE_ENV;