From 907c1b6a994b299150378caffba92dbbad57be83 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 21 Oct 2025 09:41:57 +0200 Subject: [PATCH] add requiresInstrumentationHook --- packages/nextjs/src/config/util.ts | 55 +++++++++++++ .../nextjs/src/config/withSentryConfig.ts | 58 ++++--------- packages/nextjs/test/config/util.test.ts | 82 +++++++++++++++++++ 3 files changed, 154 insertions(+), 41 deletions(-) diff --git a/packages/nextjs/src/config/util.ts b/packages/nextjs/src/config/util.ts index 0970e9573ba9..0d4a55687d2f 100644 --- a/packages/nextjs/src/config/util.ts +++ b/packages/nextjs/src/config/util.ts @@ -108,6 +108,61 @@ export function supportsNativeDebugIds(version: string): boolean { return false; } +/** + * Checks if the given Next.js version requires the `experimental.instrumentationHook` option. + * Next.js 15.0.0 and higher (including certain RC and canary versions) no longer require this option + * and will print a warning if it is set. + * + * @param version - version string to check. + * @returns true if the version requires the instrumentationHook option to be set + */ +export function requiresInstrumentationHook(version: string): boolean { + if (!version) { + return true; // Default to requiring it if version cannot be determined + } + + const { major, minor, patch, prerelease } = parseSemver(version); + + if (major === undefined || minor === undefined || patch === undefined) { + return true; // Default to requiring it if parsing fails + } + + // Next.js 16+ never requires the hook + if (major >= 16) { + return false; + } + + // Next.js 14 and below always require the hook + if (major < 15) { + return true; + } + + // At this point, we know it's Next.js 15.x.y + // Stable releases (15.0.0+) don't require the hook + if (!prerelease) { + return false; + } + + // Next.js 15.x.y with x > 0 or y > 0 don't require the hook + if (minor > 0 || patch > 0) { + return false; + } + + // Check specific prerelease versions that don't require the hook + if (prerelease.startsWith('rc.')) { + const rcNumber = parseInt(prerelease.split('.')[1] || '0', 10); + return rcNumber === 0; // Only rc.0 requires the hook + } + + if (prerelease.startsWith('canary.')) { + const canaryNumber = parseInt(prerelease.split('.')[1] || '0', 10); + return canaryNumber < 124; // canary.124+ doesn't require the hook + } + + // All other 15.0.0 prerelease versions (alpha, beta, etc.) require the hook + return true; +} + /** * Determines which bundler is actually being used based on environment variables, * and CLI flags. diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 1f3f14479656..7ac61d73aa73 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -16,7 +16,12 @@ import type { SentryBuildOptions, TurbopackOptions, } from './types'; -import { detectActiveBundler, getNextjsVersion, supportsProductionCompileHook } from './util'; +import { + detectActiveBundler, + getNextjsVersion, + requiresInstrumentationHook, + supportsProductionCompileHook, +} from './util'; import { constructWebpackConfigFunction } from './webpack'; let showedExportModeTunnelWarning = false; @@ -178,47 +183,18 @@ function getFinalConfigObject( // From Next.js version (15.0.0-canary.124) onwards, Next.js does no longer require the `experimental.instrumentationHook` option and will // print a warning when it is set, so we need to conditionally provide it for lower versions. - if (nextJsVersion) { - const { major, minor, patch, prerelease } = parseSemver(nextJsVersion); - const isFullySupportedRelease = - major !== undefined && - minor !== undefined && - patch !== undefined && - major >= 15 && - ((minor === 0 && patch === 0 && prerelease === undefined) || minor > 0 || patch > 0); - const isSupportedV15Rc = - major !== undefined && - minor !== undefined && - patch !== undefined && - prerelease !== undefined && - major === 15 && - minor === 0 && - patch === 0 && - prerelease.startsWith('rc.') && - parseInt(prerelease.split('.')[1] || '', 10) > 0; - const isSupportedCanary = - minor !== undefined && - patch !== undefined && - prerelease !== undefined && - major === 15 && - minor === 0 && - patch === 0 && - prerelease.startsWith('canary.') && - parseInt(prerelease.split('.')[1] || '', 10) >= 124; - - if (!isFullySupportedRelease && !isSupportedV15Rc && !isSupportedCanary) { - if (incomingUserNextConfigObject.experimental?.instrumentationHook === false) { - // eslint-disable-next-line no-console - console.warn( - '[@sentry/nextjs] You turned off the `experimental.instrumentationHook` option. Note that Sentry will not be initialized if you did not set it up inside `instrumentation.(js|ts)`.', - ); - } - incomingUserNextConfigObject.experimental = { - instrumentationHook: true, - ...incomingUserNextConfigObject.experimental, - }; + if (nextJsVersion && requiresInstrumentationHook(nextJsVersion)) { + if (incomingUserNextConfigObject.experimental?.instrumentationHook === false) { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/nextjs] You turned off the `experimental.instrumentationHook` option. Note that Sentry will not be initialized if you did not set it up inside `instrumentation.(js|ts)`.', + ); } - } else { + incomingUserNextConfigObject.experimental = { + instrumentationHook: true, + ...incomingUserNextConfigObject.experimental, + }; + } else if (!nextJsVersion) { // If we cannot detect a Next.js version for whatever reason, the sensible default is to set the `experimental.instrumentationHook`, even though it may create a warning. if ( incomingUserNextConfigObject.experimental && diff --git a/packages/nextjs/test/config/util.test.ts b/packages/nextjs/test/config/util.test.ts index 37e4079376cd..7335139b5037 100644 --- a/packages/nextjs/test/config/util.test.ts +++ b/packages/nextjs/test/config/util.test.ts @@ -213,6 +213,88 @@ describe('util', () => { }); }); + describe('requiresInstrumentationHook', () => { + describe('versions that do NOT require the hook (returns false)', () => { + it.each([ + // Fully supported releases (15.0.0 or higher) + ['15.0.0', 'Next.js 15.0.0'], + ['15.0.1', 'Next.js 15.0.1'], + ['15.1.0', 'Next.js 15.1.0'], + ['15.2.0', 'Next.js 15.2.0'], + ['16.0.0', 'Next.js 16.0.0'], + ['17.0.0', 'Next.js 17.0.0'], + ['20.0.0', 'Next.js 20.0.0'], + + // Supported v15.0.0-rc.1 or higher + ['15.0.0-rc.1', 'Next.js 15.0.0-rc.1'], + ['15.0.0-rc.2', 'Next.js 15.0.0-rc.2'], + ['15.0.0-rc.5', 'Next.js 15.0.0-rc.5'], + ['15.0.0-rc.100', 'Next.js 15.0.0-rc.100'], + + // Supported v15.0.0-canary.124 or higher + ['15.0.0-canary.124', 'Next.js 15.0.0-canary.124 (exact threshold)'], + ['15.0.0-canary.125', 'Next.js 15.0.0-canary.125'], + ['15.0.0-canary.130', 'Next.js 15.0.0-canary.130'], + ['15.0.0-canary.200', 'Next.js 15.0.0-canary.200'], + + // Next.js 16+ prerelease versions (all supported) + ['16.0.0-beta.0', 'Next.js 16.0.0-beta.0'], + ['16.0.0-beta.1', 'Next.js 16.0.0-beta.1'], + ['16.0.0-rc.0', 'Next.js 16.0.0-rc.0'], + ['16.0.0-rc.1', 'Next.js 16.0.0-rc.1'], + ['16.0.0-canary.1', 'Next.js 16.0.0-canary.1'], + ['16.0.0-alpha.1', 'Next.js 16.0.0-alpha.1'], + ['17.0.0-canary.1', 'Next.js 17.0.0-canary.1'], + ])('returns false for %s (%s)', version => { + expect(util.requiresInstrumentationHook(version)).toBe(false); + }); + }); + + describe('versions that DO require the hook (returns true)', () => { + it.each([ + // Next.js 14 and below + ['14.2.0', 'Next.js 14.2.0'], + ['14.0.0', 'Next.js 14.0.0'], + ['13.5.0', 'Next.js 13.5.0'], + ['12.0.0', 'Next.js 12.0.0'], + + // Unsupported v15.0.0-rc.0 + ['15.0.0-rc.0', 'Next.js 15.0.0-rc.0'], + + // Unsupported v15.0.0-canary versions below 124 + ['15.0.0-canary.123', 'Next.js 15.0.0-canary.123'], + ['15.0.0-canary.100', 'Next.js 15.0.0-canary.100'], + ['15.0.0-canary.50', 'Next.js 15.0.0-canary.50'], + ['15.0.0-canary.1', 'Next.js 15.0.0-canary.1'], + ['15.0.0-canary.0', 'Next.js 15.0.0-canary.0'], + + // Other prerelease versions + ['15.0.0-alpha.1', 'Next.js 15.0.0-alpha.1'], + ['15.0.0-beta.1', 'Next.js 15.0.0-beta.1'], + ])('returns true for %s (%s)', version => { + expect(util.requiresInstrumentationHook(version)).toBe(true); + }); + }); + + describe('edge cases', () => { + it('returns true for empty string', () => { + expect(util.requiresInstrumentationHook('')).toBe(true); + }); + + it('returns true for invalid version strings', () => { + expect(util.requiresInstrumentationHook('invalid.version')).toBe(true); + }); + + it('returns true for versions missing patch number', () => { + expect(util.requiresInstrumentationHook('15.4')).toBe(true); + }); + + it('returns true for versions missing minor number', () => { + expect(util.requiresInstrumentationHook('15')).toBe(true); + }); + }); + }); + describe('detectActiveBundler', () => { const originalArgv = process.argv; const originalEnv = process.env;