From 5de3eb74b6edc73d458400d34a903de21da4a17f Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 8 Aug 2025 09:35:40 +0200 Subject: [PATCH 01/17] use runAfterProductionCompile for sourcemaps --- packages/nextjs/package.json | 2 + .../src/config/getBuildPluginOptions.ts | 97 +++++++++++++++++++ .../config/handleRunAfterProductionCompile.ts | 62 ++++++++++++ packages/nextjs/src/config/types.ts | 13 +++ packages/nextjs/src/config/util.ts | 37 +++++++ .../nextjs/src/config/webpackPluginOptions.ts | 7 +- .../nextjs/src/config/withSentryConfig.ts | 34 ++++++- yarn.lock | 81 +++++++++++----- 8 files changed, 308 insertions(+), 25 deletions(-) create mode 100644 packages/nextjs/src/config/getBuildPluginOptions.ts create mode 100644 packages/nextjs/src/config/handleRunAfterProductionCompile.ts diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 57a1076d6cf3..7541221d75bc 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -80,6 +80,7 @@ "@opentelemetry/semantic-conventions": "^1.34.0", "@rollup/plugin-commonjs": "28.0.1", "@sentry-internal/browser-utils": "10.1.0", + "@sentry/bundler-plugin-core": "^4.0.2", "@sentry/core": "10.1.0", "@sentry/node": "10.1.0", "@sentry/opentelemetry": "10.1.0", @@ -87,6 +88,7 @@ "@sentry/vercel-edge": "10.1.0", "@sentry/webpack-plugin": "^4.0.2", "chalk": "3.0.0", + "glob": "^11.0.3", "resolve": "1.22.8", "rollup": "^4.35.0", "stacktrace-parser": "^0.1.10" diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts new file mode 100644 index 000000000000..3dfef3bbad08 --- /dev/null +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -0,0 +1,97 @@ +import type { Options as SentryBuildPluginOptions } from '@sentry/bundler-plugin-core'; +import * as path from 'path'; +import type { SentryBuildOptions } from './types'; + +/** + * Get Sentry Build Plugin options for the runAfterProductionCompile hook. + */ +export function getBuildPluginOptions({ + sentryBuildOptions, + releaseName, + distDirAbsPath, +}: { + sentryBuildOptions: SentryBuildOptions; + releaseName: string | undefined; + distDirAbsPath: string; +}): SentryBuildPluginOptions { + const sourcemapUploadAssets: string[] = []; + const sourcemapUploadIgnore: string[] = []; + + const filesToDeleteAfterUpload: string[] = []; + + // We need to convert paths to posix because Glob patterns use `\` to escape + // glob characters. This clashes with Windows path separators. + // See: https://www.npmjs.com/package/glob + const normalizedDistDirAbsPath = distDirAbsPath.replace(/\\/g, '/'); + + sourcemapUploadAssets.push( + path.posix.join(normalizedDistDirAbsPath, '**'), // Next.js build output + ); + if (sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload) { + filesToDeleteAfterUpload.push( + path.posix.join(normalizedDistDirAbsPath, '**', '*.js.map'), + path.posix.join(normalizedDistDirAbsPath, '**', '*.mjs.map'), + path.posix.join(normalizedDistDirAbsPath, '**', '*.cjs.map'), + ); + } + + return { + authToken: sentryBuildOptions.authToken, + headers: sentryBuildOptions.headers, + org: sentryBuildOptions.org, + project: sentryBuildOptions.project, + telemetry: sentryBuildOptions.telemetry, + debug: sentryBuildOptions.debug, + errorHandler: sentryBuildOptions.errorHandler, + reactComponentAnnotation: { + ...sentryBuildOptions.reactComponentAnnotation, + ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.reactComponentAnnotation, + }, + silent: sentryBuildOptions.silent, + url: sentryBuildOptions.sentryUrl, + sourcemaps: { + disable: sentryBuildOptions.sourcemaps?.disable, + rewriteSources(source) { + if (source.startsWith('webpack://_N_E/')) { + return source.replace('webpack://_N_E/', ''); + } else if (source.startsWith('webpack://')) { + return source.replace('webpack://', ''); + } else { + return source; + } + }, + assets: sentryBuildOptions.sourcemaps?.assets ?? sourcemapUploadAssets, + ignore: sentryBuildOptions.sourcemaps?.ignore ?? sourcemapUploadIgnore, + filesToDeleteAfterUpload, + ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.sourcemaps, + }, + release: + releaseName !== undefined + ? { + inject: false, // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead. + name: releaseName, + create: sentryBuildOptions.release?.create, + finalize: sentryBuildOptions.release?.finalize, + dist: sentryBuildOptions.release?.dist, + vcsRemote: sentryBuildOptions.release?.vcsRemote, + setCommits: sentryBuildOptions.release?.setCommits, + deploy: sentryBuildOptions.release?.deploy, + ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.release, + } + : { + inject: false, + create: false, + finalize: false, + }, + bundleSizeOptimizations: { + ...sentryBuildOptions.bundleSizeOptimizations, + }, + _metaOptions: { + loggerPrefixOverride: '[@sentry/nextjs]', + telemetry: { + metaFramework: 'nextjs', + }, + }, + ...sentryBuildOptions.unstable_sentryWebpackPluginOptions, + }; +} diff --git a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts new file mode 100644 index 000000000000..0978a8c9babe --- /dev/null +++ b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts @@ -0,0 +1,62 @@ +import type { createSentryBuildPluginManager as createSentryBuildPluginManagerType } from '@sentry/bundler-plugin-core'; +import { loadModule } from '@sentry/core'; +import { glob } from 'glob'; +import { getBuildPluginOptions } from './getBuildPluginOptions'; +import type { SentryBuildOptions } from './types'; + +/** + * This function is called by Next.js after the production build is complete. + * It is used to upload sourcemaps to Sentry. + */ +export async function handleRunAfterProductionCompile( + { releaseName, distDir, buildTool }: { releaseName?: string; distDir: string; buildTool: 'webpack' | 'turbopack' }, + sentryBuildOptions: SentryBuildOptions, +): Promise { + if (sentryBuildOptions.debug) { + // eslint-disable-next-line no-console + console.debug('[@sentry/nextjs] Running runAfterProductionCompile logic.'); + } + + const { createSentryBuildPluginManager } = + loadModule<{ createSentryBuildPluginManager: typeof createSentryBuildPluginManagerType }>( + '@sentry/bundler-plugin-core', + module, + ) ?? {}; + + if (!createSentryBuildPluginManager) { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/nextjs] Could not load build manager package. Will not run runAfterProductionCompile logic.', + ); + return; + } + + const sentryBuildPluginManager = createSentryBuildPluginManager( + getBuildPluginOptions({ + sentryBuildOptions, + releaseName, + distDirAbsPath: distDir, + }), + { + buildTool, + loggerPrefix: '[@sentry/nextjs]', + }, + ); + + const buildArtifactsPromise = glob( + ['/**/*.js', '/**/*.mjs', '/**/*.cjs', '/**/*.js.map', '/**/*.mjs.map', '/**/*.cjs.map'].map( + q => `${q}?(\\?*)?(#*)`, // We want to allow query and hashes strings at the end of files + ), + { + root: distDir, + absolute: true, + nodir: true, + }, + ); + + await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal(); + await sentryBuildPluginManager.createRelease(); + // 🔜 await sentryBuildPluginManager.injectDebugIds(); + await sentryBuildPluginManager.uploadSourcemaps(await buildArtifactsPromise); + await sentryBuildPluginManager.deleteArtifacts(); +} diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index b29fbb6881af..79cb8d7a1985 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -52,6 +52,9 @@ export type NextConfigObject = { env?: Record; serverExternalPackages?: string[]; // next >= v15.0.0 turbopack?: TurbopackOptions; + compiler?: { + runAfterProductionCompile?: (context: { distDir: string; projectDir: string }) => Promise | void; + }; }; export type SentryBuildOptions = { @@ -498,6 +501,16 @@ export type SentryBuildOptions = { */ disableSentryWebpackConfig?: boolean; + /** + * When true (and Next.js >= 15), use the runAfterProductionCompile hook to consolidate sourcemap uploads + * into a single operation after all webpack/turbopack builds complete, reducing build time. + * + * When false, use the traditional approach of uploading sourcemaps during each webpack build. + * + * @default false + */ + useRunAfterProductionCompileHook?: boolean; + /** * Contains a set of experimental flags that might change in future releases. These flags enable * features that are still in development and may be modified, renamed, or removed without notice. diff --git a/packages/nextjs/src/config/util.ts b/packages/nextjs/src/config/util.ts index a88e68a57135..a5def59a66fe 100644 --- a/packages/nextjs/src/config/util.ts +++ b/packages/nextjs/src/config/util.ts @@ -1,3 +1,4 @@ +import { parseSemver } from '@sentry/core'; import * as fs from 'fs'; import { sync as resolveSync } from 'resolve'; @@ -27,3 +28,39 @@ function resolveNextjsPackageJson(): string | undefined { return undefined; } } + +/** + * Checks if the current Next.js version supports the runAfterProductionCompile hook. + * This hook was introduced in Next.js 15.4.1. (https://github.com/vercel/next.js/pull/77345) + * + * @returns true if Next.js version is 15.4.1 or higher + */ +export function supportsProductionCompileHook(): boolean { + const version = getNextjsVersion(); + if (!version) { + return false; + } + + const { major, minor, patch } = parseSemver(version); + + if (major === undefined || minor === undefined || patch === undefined) { + return false; + } + + if (major > 15) { + return true; + } + + // For major version 15, check if it's 15.4.1 or higher + if (major === 15) { + if (minor > 4) { + return true; + } + if (minor === 4 && patch >= 1) { + return true; + } + return false; + } + + return false; +} diff --git a/packages/nextjs/src/config/webpackPluginOptions.ts b/packages/nextjs/src/config/webpackPluginOptions.ts index f4ff4363cdb7..cd25f6219635 100644 --- a/packages/nextjs/src/config/webpackPluginOptions.ts +++ b/packages/nextjs/src/config/webpackPluginOptions.ts @@ -70,7 +70,11 @@ export function getWebpackPluginOptions( silent: sentryBuildOptions.silent, url: sentryBuildOptions.sentryUrl, sourcemaps: { + // if the user has enabled the runAfterProductionCompileHook, we handle sourcemap uploads a later step disable: sentryBuildOptions.sourcemaps?.disable, + // TODO: disable: sentryBuildOptions.useRunAfterProductionCompileHook + // ? 'disable-upload' + // : sentryBuildOptions.sourcemaps?.disable, rewriteSources(source) { if (source.startsWith('webpack://_N_E/')) { return source.replace('webpack://_N_E/', ''); @@ -95,7 +99,8 @@ export function getWebpackPluginOptions( ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.sourcemaps, }, release: - releaseName !== undefined + // if the user has enabled the runAfterProductionCompileHook, we handle release creation a later step + releaseName !== undefined && !sentryBuildOptions.useRunAfterProductionCompileHook ? { inject: false, // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead. name: releaseName, diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 57fff867f64a..248911d61c71 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -5,6 +5,7 @@ import { getSentryRelease } from '@sentry/node'; import * as childProcess from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; +import { handleRunAfterProductionCompile } from './handleRunAfterProductionCompile'; import { createRouteManifest } from './manifest/createRouteManifest'; import type { RouteManifest } from './manifest/types'; import { constructTurbopackConfig } from './turbopack'; @@ -14,7 +15,7 @@ import type { NextConfigObject, SentryBuildOptions, } from './types'; -import { getNextjsVersion } from './util'; +import { getNextjsVersion, supportsProductionCompileHook } from './util'; import { constructWebpackConfigFunction } from './webpack'; let showedExportModeTunnelWarning = false; @@ -293,6 +294,37 @@ function getFinalConfigObject( } } + if (userSentryOptions.useRunAfterProductionCompileHook === true && supportsProductionCompileHook()) { + if (incomingUserNextConfigObject?.compiler?.runAfterProductionCompile === undefined) { + incomingUserNextConfigObject.compiler ??= {}; + incomingUserNextConfigObject.compiler.runAfterProductionCompile = async ({ distDir }) => { + await handleRunAfterProductionCompile( + { releaseName, distDir, buildTool: isTurbopack ? 'turbopack' : 'webpack' }, + userSentryOptions, + ); + }; + } else if (typeof incomingUserNextConfigObject.compiler.runAfterProductionCompile === 'function') { + incomingUserNextConfigObject.compiler.runAfterProductionCompile = new Proxy( + incomingUserNextConfigObject.compiler.runAfterProductionCompile, + { + async apply(target, thisArg, argArray) { + const { distDir }: { distDir: string } = argArray[0] ?? { distDir: '.next' }; + await target.apply(thisArg, argArray); + await handleRunAfterProductionCompile( + { releaseName, distDir, buildTool: isTurbopack ? 'turbopack' : 'webpack' }, + userSentryOptions, + ); + }, + }, + ); + } else { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/nextjs] The configured `compiler.runAfterProductionCompile` option is not a function. Will not run source map and release management logic.', + ); + } + } + return { ...incomingUserNextConfigObject, ...(nextMajor && nextMajor >= 15 diff --git a/yarn.lock b/yarn.lock index 7fe57741e388..221fc256e2ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4708,6 +4708,18 @@ resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== +"@isaacs/balanced-match@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" + integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== + +"@isaacs/brace-expansion@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3" + integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== + dependencies: + "@isaacs/balanced-match" "^4.0.1" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -6952,7 +6964,7 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/bundler-plugin-core@4.0.2": +"@sentry/bundler-plugin-core@4.0.2", "@sentry/bundler-plugin-core@^4.0.2": version "4.0.2" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.0.2.tgz#2e106ac564d2e4e83e8dbc84f1e84f4eed1d6dde" integrity sha512-LeARs8qHhEw19tk+KZd9DDV+Rh/UeapIH0+C09fTmff9p8Y82Cj89pEQ2a1rdUiF/oYIjQX45vnZscB7ra42yw== @@ -9402,7 +9414,7 @@ postcss "^8.4.47" source-map-js "^1.2.0" -"@vue/compiler-sfc@^3.4.15", "@vue/compiler-sfc@^3.5.13", "@vue/compiler-sfc@^3.5.4": +"@vue/compiler-sfc@^3.4.15", "@vue/compiler-sfc@^3.5.4": version "3.5.17" resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.17.tgz#c518871276e26593612bdab36f3f5bcd053b13bf" integrity sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww== @@ -13410,6 +13422,15 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + "crossws@>=0.2.0 <0.4.0", crossws@^0.3.4, crossws@^0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/crossws/-/crossws-0.3.5.tgz#daad331d44148ea6500098bc858869f3a5ab81a6" @@ -14149,9 +14170,6 @@ detective-scss@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/detective-scss/-/detective-scss-5.0.1.tgz#6a7f792dc9c0e8cfc0d252a50ba26a6df12596a7" integrity sha512-MAyPYRgS6DCiS6n6AoSBJXLGVOydsr9huwXORUlJ37K3YLyiN0vYHpzs3AdJOgHobBfispokoqrEon9rbmKacg== - dependencies: - gonzales-pe "^4.3.0" - node-source-walk "^7.0.1" detective-stylus@^4.0.0: version "4.0.0" @@ -14186,14 +14204,6 @@ detective-vue2@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/detective-vue2/-/detective-vue2-2.2.0.tgz#35fd1d39e261b064aca9fcaf20e136c76877482a" integrity sha512-sVg/t6O2z1zna8a/UIV6xL5KUa2cMTQbdTIIvqNM0NIPswp52fe43Nwmbahzj3ww4D844u/vC2PYfiGLvD3zFA== - dependencies: - "@dependents/detective-less" "^5.0.1" - "@vue/compiler-sfc" "^3.5.13" - detective-es6 "^5.0.1" - detective-sass "^6.0.1" - detective-scss "^5.0.1" - detective-stylus "^5.0.1" - detective-typescript "^14.0.0" deterministic-object-hash@^1.3.1: version "1.3.1" @@ -16725,9 +16735,6 @@ fetch-blob@^3.1.2, fetch-blob@^3.1.4: version "3.2.0" resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== - dependencies: - node-domexception "^1.0.0" - web-streams-polyfill "^3.0.3" fflate@0.8.2, fflate@^0.8.2: version "0.8.2" @@ -17094,6 +17101,14 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" +foreground-child@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" + integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== + dependencies: + cross-spawn "^7.0.6" + signal-exit "^4.0.1" + form-data@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" @@ -17696,6 +17711,18 @@ glob@^10.0.0, glob@^10.2.2, glob@^10.3.10, glob@^10.3.4, glob@^10.3.7, glob@^10. package-json-from-dist "^1.0.0" path-scurry "^1.11.1" +glob@^11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.3.tgz#9d8087e6d72ddb3c4707b1d2778f80ea3eaefcd6" + integrity sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA== + dependencies: + foreground-child "^3.3.1" + jackspeak "^4.1.1" + minimatch "^10.0.3" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^2.0.0" + glob@^5.0.10: version "5.0.15" resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" @@ -19775,6 +19802,13 @@ jackspeak@^4.0.1: dependencies: "@isaacs/cliui" "^8.0.2" +jackspeak@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.1.1.tgz#96876030f450502047fc7e8c7fcf8ce8124e43ae" + integrity sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + jake@^10.8.5: version "10.8.5" resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" @@ -22033,6 +22067,13 @@ minimatch@^10.0.0: dependencies: brace-expansion "^2.0.1" +minimatch@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa" + integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw== + dependencies: + "@isaacs/brace-expansion" "^5.0.0" + minimatch@^7.4.1: version "7.4.6" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.6.tgz#845d6f254d8f4a5e4fd6baf44d5f10c8448365fb" @@ -22877,11 +22918,6 @@ node-cron@^3.0.3: dependencies: uuid "8.3.2" -node-domexception@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" - integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== - node-fetch-native@^1.4.0, node-fetch-native@^1.6.3, node-fetch-native@^1.6.4, node-fetch-native@^1.6.6: version "1.6.6" resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.6.tgz#ae1d0e537af35c2c0b0de81cbff37eedd410aa37" @@ -28567,7 +28603,6 @@ stylus@0.59.0, stylus@^0.59.0: sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills: version "3.36.0" - uid fd682f6129e507c00bb4e6319cc5d6b767e36061 resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061" dependencies: "@jridgewell/gen-mapping" "^0.3.2" @@ -30930,7 +30965,7 @@ web-namespaces@^2.0.0: resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== -web-streams-polyfill@^3.0.3, web-streams-polyfill@^3.1.1: +web-streams-polyfill@^3.1.1: version "3.3.3" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== From 0dca5991478690e088c45e66922b230aa0e0c3c6 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 12 Aug 2025 14:02:03 +0200 Subject: [PATCH 02/17] dedupe --- yarn.lock | 39 ++++----------------------------------- 1 file changed, 4 insertions(+), 35 deletions(-) diff --git a/yarn.lock b/yarn.lock index 214b66b7ef51..d7662314adba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13413,16 +13413,7 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -cross-spawn@^7.0.6: +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -17093,15 +17084,7 @@ foreach@^2.0.5: resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= -foreground-child@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" - integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== - dependencies: - cross-spawn "^7.0.0" - signal-exit "^4.0.1" - -foreground-child@^3.3.1: +foreground-child@^3.1.0, foreground-child@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== @@ -19795,14 +19778,7 @@ jackspeak@^3.1.2: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" -jackspeak@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.1.0.tgz#c489c079f2b636dc4cbe9b0312a13ff1282e561b" - integrity sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw== - dependencies: - "@isaacs/cliui" "^8.0.2" - -jackspeak@^4.1.1: +jackspeak@^4.0.1, jackspeak@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.1.1.tgz#96876030f450502047fc7e8c7fcf8ce8124e43ae" integrity sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ== @@ -22060,14 +22036,7 @@ minimatch@5.1.0, minimatch@^5.0.1, minimatch@^5.1.0: dependencies: brace-expansion "^2.0.1" -minimatch@^10.0.0: - version "10.0.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b" - integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^10.0.3: +minimatch@^10.0.0, minimatch@^10.0.3: version "10.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa" integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw== From 9ca26008ebcb1b30c2ba8fbef08421c90d6f41aa Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 12 Aug 2025 14:14:46 +0200 Subject: [PATCH 03/17] bump bundler plugin and add update sourcemap handling --- packages/nextjs/package.json | 4 +-- .../config/handleRunAfterProductionCompile.ts | 6 ++-- .../nextjs/src/config/webpackPluginOptions.ts | 7 ++-- yarn.lock | 32 +++++++++++++++++-- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index f89bfa291a8a..48e0bbca33e8 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -79,14 +79,14 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@rollup/plugin-commonjs": "28.0.1", - "@sentry/bundler-plugin-core": "^4.0.2", "@sentry-internal/browser-utils": "10.4.0", "@sentry/core": "10.4.0", "@sentry/node": "10.4.0", "@sentry/opentelemetry": "10.4.0", "@sentry/react": "10.4.0", "@sentry/vercel-edge": "10.4.0", - "@sentry/webpack-plugin": "^4.0.2", + "@sentry/webpack-plugin": "^4.1.0", + "@sentry/bundler-plugin-core": "^4.1.0", "chalk": "3.0.0", "glob": "^11.0.3", "resolve": "1.22.8", diff --git a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts index 0978a8c9babe..36203e801354 100644 --- a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts +++ b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts @@ -43,7 +43,7 @@ export async function handleRunAfterProductionCompile( }, ); - const buildArtifactsPromise = glob( + const buildArtifacts = await glob( ['/**/*.js', '/**/*.mjs', '/**/*.cjs', '/**/*.js.map', '/**/*.mjs.map', '/**/*.cjs.map'].map( q => `${q}?(\\?*)?(#*)`, // We want to allow query and hashes strings at the end of files ), @@ -56,7 +56,7 @@ export async function handleRunAfterProductionCompile( await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal(); await sentryBuildPluginManager.createRelease(); - // 🔜 await sentryBuildPluginManager.injectDebugIds(); - await sentryBuildPluginManager.uploadSourcemaps(await buildArtifactsPromise); + await sentryBuildPluginManager.injectDebugIds(buildArtifacts); + await sentryBuildPluginManager.uploadSourcemaps(buildArtifacts); await sentryBuildPluginManager.deleteArtifacts(); } diff --git a/packages/nextjs/src/config/webpackPluginOptions.ts b/packages/nextjs/src/config/webpackPluginOptions.ts index cd25f6219635..a48e59dfaf48 100644 --- a/packages/nextjs/src/config/webpackPluginOptions.ts +++ b/packages/nextjs/src/config/webpackPluginOptions.ts @@ -71,10 +71,9 @@ export function getWebpackPluginOptions( url: sentryBuildOptions.sentryUrl, sourcemaps: { // if the user has enabled the runAfterProductionCompileHook, we handle sourcemap uploads a later step - disable: sentryBuildOptions.sourcemaps?.disable, - // TODO: disable: sentryBuildOptions.useRunAfterProductionCompileHook - // ? 'disable-upload' - // : sentryBuildOptions.sourcemaps?.disable, + disable: sentryBuildOptions.useRunAfterProductionCompileHook + ? 'disable-upload' + : sentryBuildOptions.sourcemaps?.disable, rewriteSources(source) { if (source.startsWith('webpack://_N_E/')) { return source.replace('webpack://_N_E/', ''); diff --git a/yarn.lock b/yarn.lock index d7662314adba..47cb89f961bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6950,6 +6950,11 @@ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.0.2.tgz#7c8eb80a38b5e6b4c4cea4c391d07581020c91e4" integrity sha512-Nr/VamvpQs6w642EI5t+qaCUGnVEro0qqk+S8XO1gc8qSdpc8kkZJFnUk7ozAr+ljYWGfVgWXrxI9lLiriLsRA== +"@sentry/babel-plugin-component-annotate@4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.1.0.tgz#6e7168f5fa59f53ac4b68e3f79c5fd54adc13f2e" + integrity sha512-UkcnqC7Bp9ODyoBN7BKcRotd1jz/I2vyruE/qjNfRC7UnP+jIRItUWYaXxQPON1fTw+N+egKdByk0M1y2OPv/Q== + "@sentry/bundler-plugin-core@4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.0.0.tgz#564463cf53f869496ab5d4986e97f86618a67677" @@ -6964,7 +6969,7 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/bundler-plugin-core@4.0.2", "@sentry/bundler-plugin-core@^4.0.2": +"@sentry/bundler-plugin-core@4.0.2": version "4.0.2" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.0.2.tgz#2e106ac564d2e4e83e8dbc84f1e84f4eed1d6dde" integrity sha512-LeARs8qHhEw19tk+KZd9DDV+Rh/UeapIH0+C09fTmff9p8Y82Cj89pEQ2a1rdUiF/oYIjQX45vnZscB7ra42yw== @@ -6978,6 +6983,20 @@ magic-string "0.30.8" unplugin "1.0.1" +"@sentry/bundler-plugin-core@4.1.0", "@sentry/bundler-plugin-core@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.1.0.tgz#c1b2f7a890a44e5ac5decc984a133aacf6147dd4" + integrity sha512-/5XBtCF6M+9frEXrrvfSWOdOC2q6I1L7oY7qbUVegNkp3kYVGihNZZnJIXGzo9rmwnA0IV7jI3o0pF/HDRqPeA== + dependencies: + "@babel/core" "^7.18.5" + "@sentry/babel-plugin-component-annotate" "4.1.0" + "@sentry/cli" "^2.51.0" + dotenv "^16.3.1" + find-up "^5.0.0" + glob "^9.3.2" + magic-string "0.30.8" + unplugin "1.0.1" + "@sentry/cli-darwin@2.51.1": version "2.51.1" resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.51.1.tgz#3a1db065651893f72dad3a502b2d7c2f5e6a7dd8" @@ -7018,7 +7037,7 @@ resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.51.1.tgz#d361e37146c9269d40c37459271a6c2cfa1fa8a6" integrity sha512-v2hreYUPPTNK1/N7+DeX7XBN/zb7p539k+2Osf0HFyVBaoUC3Y3+KBwSf4ASsnmgTAK7HCGR+X0NH1vP+icw4w== -"@sentry/cli@^2.49.0", "@sentry/cli@^2.51.1": +"@sentry/cli@^2.49.0", "@sentry/cli@^2.51.0", "@sentry/cli@^2.51.1": version "2.51.1" resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.51.1.tgz#c6bdc6025e8f600e44fc76f8274c369aeb5d4df4" integrity sha512-FU+54kNcKJABU0+ekvtnoXHM9zVrDe1zXVFbQT7mS0On0m1P0zFRGdzbnWe2XzpzuEAJXtK6aog/W+esRU9AIA== @@ -7063,6 +7082,15 @@ unplugin "1.0.1" uuid "^9.0.0" +"@sentry/webpack-plugin@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-4.1.0.tgz#e95e2dcd10e71dc8c3a16ba5cad9153f5e78c3bc" + integrity sha512-YqfDfyGAuT/9YW1kgAPfD7kGUKQCh1E5co+qMdToxi/Mz4xsWJY02rFS5GrJixYktYJfSMze8NiRr89yJMxYHw== + dependencies: + "@sentry/bundler-plugin-core" "4.1.0" + unplugin "1.0.1" + uuid "^9.0.0" + "@sigstore/protobuf-specs@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@sigstore/protobuf-specs/-/protobuf-specs-0.1.0.tgz#957cb64ea2f5ce527cc9cf02a096baeb0d2b99b4" From 6a1332e06c5e857f9af1426105ea3a44ec381273 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 1 Sep 2025 17:42:51 +0200 Subject: [PATCH 04/17] disable hook for webpack and move flag to experimental --- packages/nextjs/package.json | 4 +-- .../config/handleRunAfterProductionCompile.ts | 11 ++++++- packages/nextjs/src/config/types.ts | 22 +++++++------- .../nextjs/src/config/webpackPluginOptions.ts | 8 ++--- .../nextjs/src/config/withSentryConfig.ts | 2 +- yarn.lock | 30 ++++++++++++++++++- 6 files changed, 55 insertions(+), 22 deletions(-) diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 67ec4f82a76d..b5c8cd99ba78 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -79,14 +79,14 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@rollup/plugin-commonjs": "28.0.1", - "@sentry/bundler-plugin-core": "^4.1.0", + "@sentry/bundler-plugin-core": "^4.2.0", "@sentry-internal/browser-utils": "10.8.0", "@sentry/core": "10.8.0", "@sentry/node": "10.8.0", "@sentry/opentelemetry": "10.8.0", "@sentry/react": "10.8.0", "@sentry/vercel-edge": "10.8.0", - "@sentry/webpack-plugin": "^4.1.1", + "@sentry/webpack-plugin": "^4.2.0", "chalk": "3.0.0", "glob": "^11.0.3", "resolve": "1.22.8", diff --git a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts index 36203e801354..a5068334f6a8 100644 --- a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts +++ b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts @@ -17,6 +17,12 @@ export async function handleRunAfterProductionCompile( console.debug('[@sentry/nextjs] Running runAfterProductionCompile logic.'); } + // We don't want to do anything for webpack at this point because the plugin already handles this + // TODO: Actually implement this for webpack as well + if (buildTool === 'webpack') { + return; + } + const { createSentryBuildPluginManager } = loadModule<{ createSentryBuildPluginManager: typeof createSentryBuildPluginManagerType }>( '@sentry/bundler-plugin-core', @@ -57,6 +63,9 @@ export async function handleRunAfterProductionCompile( await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal(); await sentryBuildPluginManager.createRelease(); await sentryBuildPluginManager.injectDebugIds(buildArtifacts); - await sentryBuildPluginManager.uploadSourcemaps(buildArtifacts); + await sentryBuildPluginManager.uploadSourcemaps(buildArtifacts, { + // We don't want to prepare the artifacts because we injected debug ids manually before + prepareArtifacts: true, + }); await sentryBuildPluginManager.deleteArtifacts(); } diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 9a9e4948033e..6c9ba73134b8 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -501,22 +501,22 @@ export type SentryBuildOptions = { */ disableSentryWebpackConfig?: boolean; - /** - * When true (and Next.js >= 15), use the runAfterProductionCompile hook to consolidate sourcemap uploads - * into a single operation after all webpack/turbopack builds complete, reducing build time. - * - * When false, use the traditional approach of uploading sourcemaps during each webpack build. - * - * @default false - */ - useRunAfterProductionCompileHook?: boolean; /** * Contains a set of experimental flags that might change in future releases. These flags enable * features that are still in development and may be modified, renamed, or removed without notice. * Use with caution in production environments. - */ - _experimental?: Partial<{ + */ + _experimental?: Partial<{ + /** + * When true (and Next.js >= 15), use the runAfterProductionCompile hook to consolidate sourcemap uploads + * into a single operation after all webpack/turbopack builds complete, reducing build time. + * + * When false, use the traditional approach of uploading sourcemaps during each webpack build. + * + * @default false + */ + useRunAfterProductionCompileHook?: boolean; thirdPartyOriginStackFrames: boolean; }>; }; diff --git a/packages/nextjs/src/config/webpackPluginOptions.ts b/packages/nextjs/src/config/webpackPluginOptions.ts index a48e59dfaf48..f4ff4363cdb7 100644 --- a/packages/nextjs/src/config/webpackPluginOptions.ts +++ b/packages/nextjs/src/config/webpackPluginOptions.ts @@ -70,10 +70,7 @@ export function getWebpackPluginOptions( silent: sentryBuildOptions.silent, url: sentryBuildOptions.sentryUrl, sourcemaps: { - // if the user has enabled the runAfterProductionCompileHook, we handle sourcemap uploads a later step - disable: sentryBuildOptions.useRunAfterProductionCompileHook - ? 'disable-upload' - : sentryBuildOptions.sourcemaps?.disable, + disable: sentryBuildOptions.sourcemaps?.disable, rewriteSources(source) { if (source.startsWith('webpack://_N_E/')) { return source.replace('webpack://_N_E/', ''); @@ -98,8 +95,7 @@ export function getWebpackPluginOptions( ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.sourcemaps, }, release: - // if the user has enabled the runAfterProductionCompileHook, we handle release creation a later step - releaseName !== undefined && !sentryBuildOptions.useRunAfterProductionCompileHook + releaseName !== undefined ? { inject: false, // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead. name: releaseName, diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 50feb529c28b..b67a1ea3b75d 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -294,7 +294,7 @@ function getFinalConfigObject( } } - if (userSentryOptions.useRunAfterProductionCompileHook === true && supportsProductionCompileHook()) { + if (userSentryOptions?._experimental?.useRunAfterProductionCompileHook === true && supportsProductionCompileHook()) { if (incomingUserNextConfigObject?.compiler?.runAfterProductionCompile === undefined) { incomingUserNextConfigObject.compiler ??= {}; incomingUserNextConfigObject.compiler.runAfterProductionCompile = async ({ distDir }) => { diff --git a/yarn.lock b/yarn.lock index f7c4f779072e..7232132d4ebd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6941,7 +6941,12 @@ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.1.1.tgz#371415afc602f6b2ba0987b51123bd34d1603193" integrity sha512-HUpqrCK7zDVojTV6KL6BO9ZZiYrEYQqvYQrscyMsq04z+WCupXaH6YEliiNRvreR8DBJgdsG3lBRpebhUGmvfA== -"@sentry/bundler-plugin-core@4.1.0", "@sentry/bundler-plugin-core@^4.1.0": +"@sentry/babel-plugin-component-annotate@4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.2.0.tgz#6c616e6d645f49f15f83b891ef42a795ba4dbb3f" + integrity sha512-GFpS3REqaHuyX4LCNqlneAQZIKyHb5ePiI1802n0fhtYjk68I1DTQ3PnbzYi50od/vAsTQVCknaS5F6tidNqTQ== + +"@sentry/bundler-plugin-core@4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.1.0.tgz#c1b2f7a890a44e5ac5decc984a133aacf6147dd4" integrity sha512-/5XBtCF6M+9frEXrrvfSWOdOC2q6I1L7oY7qbUVegNkp3kYVGihNZZnJIXGzo9rmwnA0IV7jI3o0pF/HDRqPeA== @@ -6969,6 +6974,20 @@ magic-string "0.30.8" unplugin "1.0.1" +"@sentry/bundler-plugin-core@4.2.0", "@sentry/bundler-plugin-core@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.2.0.tgz#b607937f7cd0a769aa26974c4af3fca94abad63f" + integrity sha512-EDG6ELSEN/Dzm4KUQOynoI2suEAdPdgwaBXVN4Ww705zdrYT79OGh51rkz74KGhovt7GukaPf0Z9LJwORXUbhg== + dependencies: + "@babel/core" "^7.18.5" + "@sentry/babel-plugin-component-annotate" "4.2.0" + "@sentry/cli" "^2.51.0" + dotenv "^16.3.1" + find-up "^5.0.0" + glob "^9.3.2" + magic-string "0.30.8" + unplugin "1.0.1" + "@sentry/cli-darwin@2.52.0": version "2.52.0" resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.52.0.tgz#05178cd819c2a33eb22a6e90bf7bb8f853f1b476" @@ -7054,6 +7073,15 @@ unplugin "1.0.1" uuid "^9.0.0" +"@sentry/webpack-plugin@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-4.2.0.tgz#00b89aeb1261ae15f7bc81ee9e5b5a23ca3d2dbf" + integrity sha512-2lPuvJhbiEOd/NAQv5EL8at9QVKchkEmWFDioDsOG6csFqbZ8hdWtTcbsXnhzH9j+CM1LmdeDNVjIF+SMoxCNg== + dependencies: + "@sentry/bundler-plugin-core" "4.2.0" + unplugin "1.0.1" + uuid "^9.0.0" + "@sigstore/protobuf-specs@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@sigstore/protobuf-specs/-/protobuf-specs-0.1.0.tgz#957cb64ea2f5ce527cc9cf02a096baeb0d2b99b4" From 6112eb1d9918ff7f67a0a9cda467d33c712e4318 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 2 Sep 2025 13:12:40 +0200 Subject: [PATCH 05/17] automatic source map generation for turbopack --- .../config/handleRunAfterProductionCompile.ts | 19 ++++------------ .../nextjs/src/config/withSentryConfig.ts | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts index a5068334f6a8..bc30d0a2bef4 100644 --- a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts +++ b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts @@ -1,6 +1,6 @@ import type { createSentryBuildPluginManager as createSentryBuildPluginManagerType } from '@sentry/bundler-plugin-core'; import { loadModule } from '@sentry/core'; -import { glob } from 'glob'; +// import { glob } from 'glob'; import { getBuildPluginOptions } from './getBuildPluginOptions'; import type { SentryBuildOptions } from './types'; @@ -49,23 +49,12 @@ export async function handleRunAfterProductionCompile( }, ); - const buildArtifacts = await glob( - ['/**/*.js', '/**/*.mjs', '/**/*.cjs', '/**/*.js.map', '/**/*.mjs.map', '/**/*.cjs.map'].map( - q => `${q}?(\\?*)?(#*)`, // We want to allow query and hashes strings at the end of files - ), - { - root: distDir, - absolute: true, - nodir: true, - }, - ); - await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal(); await sentryBuildPluginManager.createRelease(); - await sentryBuildPluginManager.injectDebugIds(buildArtifacts); - await sentryBuildPluginManager.uploadSourcemaps(buildArtifacts, { + await sentryBuildPluginManager.injectDebugIds([distDir]); + await sentryBuildPluginManager.uploadSourcemaps([distDir], { // We don't want to prepare the artifacts because we injected debug ids manually before - prepareArtifacts: true, + prepareArtifacts: false, }); await sentryBuildPluginManager.deleteArtifacts(); } diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index b67a1ea3b75d..713ca4187c38 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -325,6 +325,28 @@ function getFinalConfigObject( } } + // Enable source maps for turbopack builds + if (isTurbopackSupported && isTurbopack && !userSentryOptions.sourcemaps?.disable) { + // Only set if not already configured by user + if (incomingUserNextConfigObject.productionBrowserSourceMaps === undefined) { + // eslint-disable-next-line no-console + console.log('[@sentry/nextjs] Automatically enabling browser source map generation for turbopack build.'); + incomingUserNextConfigObject.productionBrowserSourceMaps = true; + } + + // Enable source map deletion if not explicitly disabled + if (userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload === undefined) { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/nextjs] Source maps will be automatically deleted after being uploaded to Sentry. If you want to keep the source maps, set the `sourcemaps.deleteSourcemapsAfterUpload` option to false in `withSentryConfig()`. If you do not want to generate and upload sourcemaps at all, set the `sourcemaps.disable` option to true.', + ); + userSentryOptions.sourcemaps = { + ...userSentryOptions.sourcemaps, + deleteSourcemapsAfterUpload: true, + }; + } + } + return { ...incomingUserNextConfigObject, ...(nextMajor && nextMajor >= 15 From 8d488eb608379364729ccb0edfa991ff5c88b1b9 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 2 Sep 2025 13:26:52 +0200 Subject: [PATCH 06/17] add tests for turbopack source map generation --- .../test/config/withSentryConfig.test.ts | 365 ++++++++++++++++++ 1 file changed, 365 insertions(+) diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index 3b872d810c49..e7efe91e80b8 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -269,6 +269,371 @@ describe('withSentryConfig', () => { }); }); + describe('turbopack sourcemap configuration', () => { + const originalTurbopack = process.env.TURBOPACK; + + afterEach(() => { + vi.restoreAllMocks(); + process.env.TURBOPACK = originalTurbopack; + }); + + it('enables productionBrowserSourceMaps for supported turbopack builds when sourcemaps are not disabled', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.productionBrowserSourceMaps).toBe(true); + }); + + it('does not enable productionBrowserSourceMaps when sourcemaps are disabled', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.productionBrowserSourceMaps; + + const sentryOptions = { + sourcemaps: { + disable: true, + }, + }; + + const finalConfig = materializeFinalNextConfig(cleanConfig, undefined, sentryOptions); + + expect(finalConfig.productionBrowserSourceMaps).toBeUndefined(); + }); + + it('does not enable productionBrowserSourceMaps when turbopack is not enabled', () => { + delete process.env.TURBOPACK; + + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.productionBrowserSourceMaps; + + const finalConfig = materializeFinalNextConfig(cleanConfig); + + expect(finalConfig.productionBrowserSourceMaps).toBeUndefined(); + }); + + it('does not enable productionBrowserSourceMaps when turbopack version is not supported', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.2.0'); // unsupported version + + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.productionBrowserSourceMaps; + + const finalConfig = materializeFinalNextConfig(cleanConfig); + + expect(finalConfig.productionBrowserSourceMaps).toBeUndefined(); + }); + + it('preserves user-configured productionBrowserSourceMaps setting', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const configWithSourceMaps = { + ...exportedNextConfig, + productionBrowserSourceMaps: false, // user explicitly disabled + }; + + const finalConfig = materializeFinalNextConfig(configWithSourceMaps); + + expect(finalConfig.productionBrowserSourceMaps).toBe(false); + }); + + it('preserves user-configured productionBrowserSourceMaps: true setting', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const configWithSourceMaps = { + ...exportedNextConfig, + productionBrowserSourceMaps: true, // user explicitly enabled + }; + + const sentryOptions = { + sourcemaps: { + disable: true, // Sentry disabled, but user wants Next.js sourcemaps + }, + }; + + const finalConfig = materializeFinalNextConfig(configWithSourceMaps, undefined, sentryOptions); + + expect(finalConfig.productionBrowserSourceMaps).toBe(true); + }); + + it('automatically enables deleteSourcemapsAfterUpload for turbopack builds when not explicitly set', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const sentryOptions = { + sourcemaps: {}, // no deleteSourcemapsAfterUpload setting + }; + + materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + expect(sentryOptions.sourcemaps).toHaveProperty('deleteSourcemapsAfterUpload', true); + }); + + it('preserves explicitly configured deleteSourcemapsAfterUpload setting', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const sentryOptions = { + sourcemaps: { + deleteSourcemapsAfterUpload: false, // user wants to keep sourcemaps + }, + }; + + materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + expect(sentryOptions.sourcemaps.deleteSourcemapsAfterUpload).toBe(false); + }); + + it('does not modify deleteSourcemapsAfterUpload when sourcemaps are disabled', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const sentryOptions = { + sourcemaps: { + disable: true, + }, + }; + + materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + expect(sentryOptions.sourcemaps).not.toHaveProperty('deleteSourcemapsAfterUpload'); + }); + + it('logs correct message when enabling sourcemaps for turbopack', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.productionBrowserSourceMaps; + + materializeFinalNextConfig(cleanConfig); + + expect(consoleSpy).toHaveBeenCalledWith( + '[@sentry/nextjs] Automatically enabling browser source map generation for turbopack build.', + ); + + consoleSpy.mockRestore(); + }); + + it('warns about automatic sourcemap deletion for turbopack builds', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const sentryOptions = { + sourcemaps: {}, // triggers automatic deletion + }; + + materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[@sentry/nextjs] Source maps will be automatically deleted after being uploaded to Sentry. If you want to keep the source maps, set the `sourcemaps.deleteSourcemapsAfterUpload` option to false in `withSentryConfig()`. If you do not want to generate and upload sourcemaps at all, set the `sourcemaps.disable` option to true.', + ); + + consoleWarnSpy.mockRestore(); + }); + + describe('version compatibility', () => { + it('enables sourcemaps for Next.js 15.3.0', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.productionBrowserSourceMaps).toBe(true); + }); + + it('enables sourcemaps for Next.js 15.4.0', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.0'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.productionBrowserSourceMaps).toBe(true); + }); + + it('enables sourcemaps for Next.js 16.0.0', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('16.0.0'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.productionBrowserSourceMaps).toBe(true); + }); + + it('does not enable sourcemaps for Next.js 15.2.9', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.2.9'); + + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.productionBrowserSourceMaps; + + const finalConfig = materializeFinalNextConfig(cleanConfig); + + expect(finalConfig.productionBrowserSourceMaps).toBeUndefined(); + }); + + it('enables sourcemaps for supported canary versions', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0-canary.28'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.productionBrowserSourceMaps).toBe(true); + }); + + it('does not enable sourcemaps for unsupported canary versions', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0-canary.27'); + + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.productionBrowserSourceMaps; + + const finalConfig = materializeFinalNextConfig(cleanConfig); + + expect(finalConfig.productionBrowserSourceMaps).toBeUndefined(); + }); + }); + + describe('edge cases', () => { + it('handles undefined sourcemaps option', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const sentryOptions = {}; // no sourcemaps property + + const finalConfig = materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + expect(finalConfig.productionBrowserSourceMaps).toBe(true); + }); + + it('handles empty sourcemaps object', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const sentryOptions = { + sourcemaps: {}, // empty object + }; + + materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + expect(sentryOptions.sourcemaps).toHaveProperty('deleteSourcemapsAfterUpload', true); + }); + + it('works when TURBOPACK env var is truthy string', () => { + process.env.TURBOPACK = 'true'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.productionBrowserSourceMaps).toBe(true); + }); + + it('does not enable sourcemaps when TURBOPACK env var is falsy', () => { + process.env.TURBOPACK = ''; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.productionBrowserSourceMaps; + + const finalConfig = materializeFinalNextConfig(cleanConfig); + + expect(finalConfig.productionBrowserSourceMaps).toBeUndefined(); + }); + + it('works correctly with tunnel route configuration', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const sentryOptions = { + tunnelRoute: '/custom-tunnel', + sourcemaps: {}, + }; + + const finalConfig = materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + expect(finalConfig.productionBrowserSourceMaps).toBe(true); + expect(sentryOptions.sourcemaps).toHaveProperty('deleteSourcemapsAfterUpload', true); + expect(finalConfig.rewrites).toBeInstanceOf(Function); + }); + + it('works correctly with custom release configuration', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + // Clear environment variable to test custom release name + const originalSentryRelease = process.env.SENTRY_RELEASE; + delete process.env.SENTRY_RELEASE; + + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.env; + + const sentryOptions = { + release: { + name: 'custom-release-1.0.0', + }, + sourcemaps: {}, + }; + + const finalConfig = materializeFinalNextConfig(cleanConfig, undefined, sentryOptions); + + expect(finalConfig.productionBrowserSourceMaps).toBe(true); + expect(sentryOptions.sourcemaps).toHaveProperty('deleteSourcemapsAfterUpload', true); + expect(finalConfig.env).toHaveProperty('_sentryRelease', 'custom-release-1.0.0'); + + // Restore original env var + if (originalSentryRelease) { + process.env.SENTRY_RELEASE = originalSentryRelease; + } + }); + + it('does not interfere with other Next.js configuration options', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const configWithOtherOptions = { + ...exportedNextConfig, + assetPrefix: 'https://cdn.example.com', + basePath: '/app', + distDir: 'custom-dist', + }; + + const finalConfig = materializeFinalNextConfig(configWithOtherOptions); + + expect(finalConfig.productionBrowserSourceMaps).toBe(true); + expect(finalConfig.assetPrefix).toBe('https://cdn.example.com'); + expect(finalConfig.basePath).toBe('/app'); + expect(finalConfig.distDir).toBe('custom-dist'); + }); + + it('works correctly when turbopack config already exists', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const configWithTurbopack = { + ...exportedNextConfig, + turbopack: { + resolveAlias: { + '@': './src', + }, + }, + }; + + const finalConfig = materializeFinalNextConfig(configWithTurbopack); + + expect(finalConfig.productionBrowserSourceMaps).toBe(true); + expect(finalConfig.turbopack).toBeDefined(); + expect(finalConfig.turbopack?.resolveAlias).toEqual({ '@': './src' }); + }); + }); + }); + describe('release injection behavior', () => { afterEach(() => { vi.restoreAllMocks(); From 5d299c0091e4b419581533e59a6c3821b4c64872 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 2 Sep 2025 15:06:34 +0200 Subject: [PATCH 07/17] more tests --- .../test/config/getBuildPluginOptions.test.ts | 432 ++++++++++++++++++ .../handleRunAfterProductionCompile.test.ts | 284 ++++++++++++ packages/nextjs/test/config/util.test.ts | 149 ++++++ .../test/config/withSentryConfig.test.ts | 249 ++++++++++ 4 files changed, 1114 insertions(+) create mode 100644 packages/nextjs/test/config/getBuildPluginOptions.test.ts create mode 100644 packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts create mode 100644 packages/nextjs/test/config/util.test.ts diff --git a/packages/nextjs/test/config/getBuildPluginOptions.test.ts b/packages/nextjs/test/config/getBuildPluginOptions.test.ts new file mode 100644 index 000000000000..1120084ec76e --- /dev/null +++ b/packages/nextjs/test/config/getBuildPluginOptions.test.ts @@ -0,0 +1,432 @@ +import { describe, expect, it, vi } from 'vitest'; +import { getBuildPluginOptions } from '../../src/config/getBuildPluginOptions'; +import type { SentryBuildOptions } from '../../src/config/types'; + +describe('getBuildPluginOptions', () => { + const mockReleaseName = 'test-release-1.0.0'; + const mockDistDirAbsPath = '/path/to/.next'; + + describe('basic functionality', () => { + it('returns correct build plugin options with minimal configuration', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + authToken: 'test-token', + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + }); + + expect(result).toMatchObject({ + authToken: 'test-token', + org: 'test-org', + project: 'test-project', + sourcemaps: { + assets: ['/path/to/.next/**'], + ignore: [], + filesToDeleteAfterUpload: [], + rewriteSources: expect.any(Function), + }, + release: { + inject: false, + name: mockReleaseName, + create: undefined, + finalize: undefined, + }, + _metaOptions: { + loggerPrefixOverride: '[@sentry/nextjs]', + telemetry: { + metaFramework: 'nextjs', + }, + }, + bundleSizeOptimizations: {}, + }); + }); + + it('normalizes Windows paths to posix for glob patterns', () => { + const windowsPath = 'C:\\Users\\test\\.next'; + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: windowsPath, + }); + + expect(result.sourcemaps?.assets).toEqual(['C:/Users/test/.next/**']); + }); + }); + + describe('sourcemap configuration', () => { + it('configures file deletion when deleteSourcemapsAfterUpload is enabled', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + sourcemaps: { + deleteSourcemapsAfterUpload: true, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + }); + + expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([ + '/path/to/.next/**/*.js.map', + '/path/to/.next/**/*.mjs.map', + '/path/to/.next/**/*.cjs.map', + ]); + }); + + it('does not configure file deletion when deleteSourcemapsAfterUpload is disabled', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + sourcemaps: { + deleteSourcemapsAfterUpload: false, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + }); + + expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([]); + }); + + it('uses custom sourcemap assets when provided', () => { + const customAssets = ['custom/path/**', 'another/path/**']; + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + sourcemaps: { + assets: customAssets, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + }); + + expect(result.sourcemaps?.assets).toEqual(customAssets); + }); + + it('uses custom sourcemap ignore patterns when provided', () => { + const customIgnore = ['**/vendor/**', '**/node_modules/**']; + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + sourcemaps: { + ignore: customIgnore, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + }); + + expect(result.sourcemaps?.ignore).toEqual(customIgnore); + }); + + it('disables sourcemaps when disable flag is set', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + sourcemaps: { + disable: true, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + }); + + expect(result.sourcemaps?.disable).toBe(true); + }); + }); + + describe('source rewriting functionality', () => { + it('rewrites webpack sources correctly', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + }); + + const rewriteSources = result.sourcemaps?.rewriteSources; + expect(rewriteSources).toBeDefined(); + + if (rewriteSources) { + // Test webpack://_N_E/ prefix removal + expect(rewriteSources('webpack://_N_E/src/pages/index.js', {})).toBe('src/pages/index.js'); + + // Test general webpack:// prefix removal + expect(rewriteSources('webpack://project/src/components/Button.js', {})).toBe( + 'project/src/components/Button.js', + ); + + // Test no rewriting for normal paths + expect(rewriteSources('src/utils/helpers.js', {})).toBe('src/utils/helpers.js'); + expect(rewriteSources('./components/Layout.tsx', {})).toBe('./components/Layout.tsx'); + } + }); + }); + + describe('release configuration', () => { + it('configures release with injection disabled when release name is provided', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + release: { + create: true, + finalize: true, + dist: 'production', + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + }); + + expect(result.release).toMatchObject({ + inject: false, + name: mockReleaseName, + create: true, + finalize: true, + dist: 'production', + }); + }); + + it('configures release as disabled when no release name is provided', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: undefined, + distDirAbsPath: mockDistDirAbsPath, + }); + + expect(result.release).toMatchObject({ + inject: false, + create: false, + finalize: false, + }); + }); + + it('merges webpack plugin release options correctly', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + release: { + create: true, + vcsRemote: 'origin', + }, + unstable_sentryWebpackPluginOptions: { + release: { + setCommits: { + auto: true, + }, + deploy: { + env: 'production', + }, + }, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + }); + + // The 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'); + }); + }); + + describe('react component annotation', () => { + it('merges react component annotation options correctly', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + reactComponentAnnotation: { + enabled: true, + }, + unstable_sentryWebpackPluginOptions: { + reactComponentAnnotation: { + enabled: false, // This will override the base setting + }, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + }); + + // The unstable options override the base options - in this case enabled should be false + expect(result.reactComponentAnnotation).toHaveProperty('enabled', false); + }); + }); + + describe('other configuration options', () => { + it('passes through all standard configuration options', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + authToken: 'test-token', + headers: { 'Custom-Header': 'value' }, + telemetry: false, + debug: true, + errorHandler: vi.fn(), + silent: true, + sentryUrl: 'https://custom.sentry.io', + bundleSizeOptimizations: { + excludeDebugStatements: true, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + }); + + expect(result).toMatchObject({ + authToken: 'test-token', + headers: { 'Custom-Header': 'value' }, + org: 'test-org', + project: 'test-project', + telemetry: false, + debug: true, + errorHandler: sentryBuildOptions.errorHandler, + silent: true, + url: 'https://custom.sentry.io', + bundleSizeOptimizations: { + excludeDebugStatements: true, + }, + }); + }); + + it('merges unstable webpack plugin options correctly', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + unstable_sentryWebpackPluginOptions: { + applicationKey: 'test-app-key', + sourcemaps: { + disable: false, + }, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + }); + + expect(result).toMatchObject({ + applicationKey: 'test-app-key', + sourcemaps: expect.objectContaining({ + disable: false, + }), + }); + }); + }); + + describe('edge cases', () => { + it('handles undefined release name gracefully', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: undefined, + distDirAbsPath: mockDistDirAbsPath, + }); + + expect(result.release).toMatchObject({ + inject: false, + create: false, + finalize: false, + }); + }); + + it('handles empty sourcemaps configuration', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + sourcemaps: {}, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + }); + + expect(result.sourcemaps).toMatchObject({ + disable: undefined, + assets: ['/path/to/.next/**'], + ignore: [], + filesToDeleteAfterUpload: [], + rewriteSources: expect.any(Function), + }); + }); + + it('handles complex nested path structures', () => { + const complexPath = '/very/deep/nested/path/with/multiple/segments/.next'; + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + sourcemaps: { + deleteSourcemapsAfterUpload: true, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: complexPath, + }); + + expect(result.sourcemaps?.assets).toEqual([`${complexPath}/**`]); + expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([ + `${complexPath}/**/*.js.map`, + `${complexPath}/**/*.mjs.map`, + `${complexPath}/**/*.cjs.map`, + ]); + }); + }); +}); diff --git a/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts b/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts new file mode 100644 index 000000000000..3e01487cde12 --- /dev/null +++ b/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts @@ -0,0 +1,284 @@ +import { loadModule } from '@sentry/core'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { handleRunAfterProductionCompile } from '../../src/config/handleRunAfterProductionCompile'; +import type { SentryBuildOptions } from '../../src/config/types'; + +vi.mock('@sentry/core', () => ({ + loadModule: vi.fn(), +})); + +vi.mock('../../src/config/getBuildPluginOptions', () => ({ + getBuildPluginOptions: vi.fn(() => ({ + org: 'test-org', + project: 'test-project', + sourcemaps: {}, + })), +})); + +describe('handleRunAfterProductionCompile', () => { + const mockCreateSentryBuildPluginManager = vi.fn(); + const mockSentryBuildPluginManager = { + telemetry: { + emitBundlerPluginExecutionSignal: vi.fn().mockResolvedValue(undefined), + }, + createRelease: vi.fn().mockResolvedValue(undefined), + injectDebugIds: vi.fn().mockResolvedValue(undefined), + uploadSourcemaps: vi.fn().mockResolvedValue(undefined), + deleteArtifacts: vi.fn().mockResolvedValue(undefined), + }; + + const mockSentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + authToken: 'test-token', + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockCreateSentryBuildPluginManager.mockReturnValue(mockSentryBuildPluginManager); + (loadModule as any).mockReturnValue({ + createSentryBuildPluginManager: mockCreateSentryBuildPluginManager, + }); + }); + + describe('turbopack builds', () => { + it('executes all build steps for turbopack builds', async () => { + await handleRunAfterProductionCompile( + { + releaseName: 'test-release', + distDir: '/path/to/.next', + buildTool: 'turbopack', + }, + mockSentryBuildOptions, + ); + + expect(mockSentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal).toHaveBeenCalledTimes(1); + expect(mockSentryBuildPluginManager.createRelease).toHaveBeenCalledTimes(1); + expect(mockSentryBuildPluginManager.injectDebugIds).toHaveBeenCalledWith(['/path/to/.next']); + expect(mockSentryBuildPluginManager.uploadSourcemaps).toHaveBeenCalledWith(['/path/to/.next'], { + prepareArtifacts: false, + }); + expect(mockSentryBuildPluginManager.deleteArtifacts).toHaveBeenCalledTimes(1); + }); + + it('calls createSentryBuildPluginManager with correct options', async () => { + await handleRunAfterProductionCompile( + { + releaseName: 'test-release', + distDir: '/path/to/.next', + buildTool: 'turbopack', + }, + mockSentryBuildOptions, + ); + + expect(mockCreateSentryBuildPluginManager).toHaveBeenCalledWith( + expect.objectContaining({ + org: 'test-org', + project: 'test-project', + sourcemaps: expect.any(Object), + }), + { + buildTool: 'turbopack', + loggerPrefix: '[@sentry/nextjs]', + }, + ); + }); + + it('handles debug mode correctly', async () => { + const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {}); + + const debugOptions = { + ...mockSentryBuildOptions, + debug: true, + }; + + await handleRunAfterProductionCompile( + { + releaseName: 'test-release', + distDir: '/path/to/.next', + buildTool: 'turbopack', + }, + debugOptions, + ); + + expect(consoleSpy).toHaveBeenCalledWith('[@sentry/nextjs] Running runAfterProductionCompile logic.'); + + consoleSpy.mockRestore(); + }); + }); + + describe('webpack builds', () => { + it('skips execution for webpack builds', async () => { + await handleRunAfterProductionCompile( + { + releaseName: 'test-release', + distDir: '/path/to/.next', + buildTool: 'webpack', + }, + mockSentryBuildOptions, + ); + + expect(loadModule).not.toHaveBeenCalled(); + expect(mockCreateSentryBuildPluginManager).not.toHaveBeenCalled(); + }); + + it('logs debug message even for webpack builds when debug is enabled', async () => { + const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {}); + + const debugOptions = { + ...mockSentryBuildOptions, + debug: true, + }; + + await handleRunAfterProductionCompile( + { + releaseName: 'test-release', + distDir: '/path/to/.next', + buildTool: 'webpack', + }, + debugOptions, + ); + + expect(consoleSpy).toHaveBeenCalledWith('[@sentry/nextjs] Running runAfterProductionCompile logic.'); + + consoleSpy.mockRestore(); + }); + }); + + describe('error handling', () => { + it('handles missing bundler plugin core gracefully', async () => { + (loadModule as any).mockReturnValue(null); + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + await handleRunAfterProductionCompile( + { + releaseName: 'test-release', + distDir: '/path/to/.next', + buildTool: 'turbopack', + }, + mockSentryBuildOptions, + ); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[@sentry/nextjs] Could not load build manager package. Will not run runAfterProductionCompile logic.', + ); + expect(mockCreateSentryBuildPluginManager).not.toHaveBeenCalled(); + + consoleWarnSpy.mockRestore(); + }); + + it('handles missing createSentryBuildPluginManager export gracefully', async () => { + (loadModule as any).mockReturnValue({}); + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + await handleRunAfterProductionCompile( + { + releaseName: 'test-release', + distDir: '/path/to/.next', + buildTool: 'turbopack', + }, + mockSentryBuildOptions, + ); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[@sentry/nextjs] Could not load build manager package. Will not run runAfterProductionCompile logic.', + ); + expect(mockCreateSentryBuildPluginManager).not.toHaveBeenCalled(); + + consoleWarnSpy.mockRestore(); + }); + + it('propagates errors from build plugin manager operations', async () => { + const mockError = new Error('Test error'); + mockSentryBuildPluginManager.createRelease.mockRejectedValue(mockError); + + await expect( + handleRunAfterProductionCompile( + { + releaseName: 'test-release', + distDir: '/path/to/.next', + buildTool: 'turbopack', + }, + mockSentryBuildOptions, + ), + ).rejects.toThrow('Test error'); + }); + }); + + describe('step execution order', () => { + it('executes build steps in correct order', async () => { + const executionOrder: string[] = []; + + mockSentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal.mockImplementation(async () => { + executionOrder.push('telemetry'); + }); + mockSentryBuildPluginManager.createRelease.mockImplementation(async () => { + executionOrder.push('createRelease'); + }); + mockSentryBuildPluginManager.injectDebugIds.mockImplementation(async () => { + executionOrder.push('injectDebugIds'); + }); + mockSentryBuildPluginManager.uploadSourcemaps.mockImplementation(async () => { + executionOrder.push('uploadSourcemaps'); + }); + mockSentryBuildPluginManager.deleteArtifacts.mockImplementation(async () => { + executionOrder.push('deleteArtifacts'); + }); + + await handleRunAfterProductionCompile( + { + releaseName: 'test-release', + distDir: '/path/to/.next', + buildTool: 'turbopack', + }, + mockSentryBuildOptions, + ); + + expect(executionOrder).toEqual([ + 'telemetry', + 'createRelease', + 'injectDebugIds', + 'uploadSourcemaps', + 'deleteArtifacts', + ]); + }); + }); + + describe('path handling', () => { + it('correctly passes distDir to debug ID injection', async () => { + const customDistDir = '/custom/dist/path'; + + await handleRunAfterProductionCompile( + { + releaseName: 'test-release', + distDir: customDistDir, + buildTool: 'turbopack', + }, + mockSentryBuildOptions, + ); + + expect(mockSentryBuildPluginManager.injectDebugIds).toHaveBeenCalledWith([customDistDir]); + expect(mockSentryBuildPluginManager.uploadSourcemaps).toHaveBeenCalledWith([customDistDir], { + prepareArtifacts: false, + }); + }); + + it('works with relative paths', async () => { + const relativeDistDir = '.next'; + + await handleRunAfterProductionCompile( + { + releaseName: 'test-release', + distDir: relativeDistDir, + buildTool: 'turbopack', + }, + mockSentryBuildOptions, + ); + + expect(mockSentryBuildPluginManager.injectDebugIds).toHaveBeenCalledWith([relativeDistDir]); + expect(mockSentryBuildPluginManager.uploadSourcemaps).toHaveBeenCalledWith([relativeDistDir], { + prepareArtifacts: false, + }); + }); + }); +}); diff --git a/packages/nextjs/test/config/util.test.ts b/packages/nextjs/test/config/util.test.ts new file mode 100644 index 000000000000..54a6f4b20c67 --- /dev/null +++ b/packages/nextjs/test/config/util.test.ts @@ -0,0 +1,149 @@ +import { parseSemver } from '@sentry/core'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import * as util from '../../src/config/util'; + +vi.mock('@sentry/core', () => ({ + parseSemver: vi.fn(), +})); + +describe('util', () => { + describe('supportsProductionCompileHook', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('supported versions', () => { + it('returns true for Next.js 15.4.1', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1'); + (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 1 }); + expect(util.supportsProductionCompileHook()).toBe(true); + }); + + it('returns true for Next.js 15.4.2', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.2'); + (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 2 }); + expect(util.supportsProductionCompileHook()).toBe(true); + }); + + it('returns true for Next.js 15.5.0', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.5.0'); + (parseSemver as any).mockReturnValue({ major: 15, minor: 5, patch: 0 }); + expect(util.supportsProductionCompileHook()).toBe(true); + }); + + it('returns true for Next.js 16.0.0', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('16.0.0'); + (parseSemver as any).mockReturnValue({ major: 16, minor: 0, patch: 0 }); + expect(util.supportsProductionCompileHook()).toBe(true); + }); + + it('returns true for Next.js 17.0.0', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('17.0.0'); + (parseSemver as any).mockReturnValue({ major: 17, minor: 0, patch: 0 }); + expect(util.supportsProductionCompileHook()).toBe(true); + }); + + it('returns true for supported canary versions', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1-canary.42'); + (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 1 }); + expect(util.supportsProductionCompileHook()).toBe(true); + }); + + it('returns true for supported rc versions', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1-rc.1'); + (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 1 }); + expect(util.supportsProductionCompileHook()).toBe(true); + }); + }); + + describe('unsupported versions', () => { + it('returns false for Next.js 15.4.0', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.0'); + (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 0 }); + expect(util.supportsProductionCompileHook()).toBe(false); + }); + + it('returns false for Next.js 15.3.9', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.9'); + (parseSemver as any).mockReturnValue({ major: 15, minor: 3, patch: 9 }); + expect(util.supportsProductionCompileHook()).toBe(false); + }); + + it('returns false for Next.js 15.0.0', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.0.0'); + (parseSemver as any).mockReturnValue({ major: 15, minor: 0, patch: 0 }); + expect(util.supportsProductionCompileHook()).toBe(false); + }); + + it('returns false for Next.js 14.2.0', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('14.2.0'); + (parseSemver as any).mockReturnValue({ major: 14, minor: 2, patch: 0 }); + expect(util.supportsProductionCompileHook()).toBe(false); + }); + + it('returns false for unsupported canary versions', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.0-canary.42'); + (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 0 }); + expect(util.supportsProductionCompileHook()).toBe(false); + }); + }); + + describe('edge cases', () => { + it('returns false for invalid version strings', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('invalid.version'); + (parseSemver as any).mockReturnValue({ major: undefined, minor: undefined, patch: undefined }); + expect(util.supportsProductionCompileHook()).toBe(false); + }); + + it('handles versions with build metadata', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1+build.123'); + (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 1 }); + expect(util.supportsProductionCompileHook()).toBe(true); + }); + + it('handles versions with pre-release identifiers', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1-alpha.1'); + (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 1 }); + expect(util.supportsProductionCompileHook()).toBe(true); + }); + + it('returns false for versions missing patch number', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4'); + (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: undefined }); + expect(util.supportsProductionCompileHook()).toBe(false); + }); + + it('returns false for versions missing minor number', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15'); + (parseSemver as any).mockReturnValue({ major: 15, minor: undefined, patch: undefined }); + expect(util.supportsProductionCompileHook()).toBe(false); + }); + }); + + describe('version boundary tests', () => { + it('returns false for 15.4.0 (just below threshold)', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.0'); + (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 0 }); + expect(util.supportsProductionCompileHook()).toBe(false); + }); + + it('returns true for 15.4.1 (exact threshold)', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1'); + (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 1 }); + expect(util.supportsProductionCompileHook()).toBe(true); + }); + + it('returns true for 15.4.2 (just above threshold)', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.2'); + (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 2 }); + expect(util.supportsProductionCompileHook()).toBe(true); + }); + + it('returns false for 15.3.999 (high patch but wrong minor)', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.999'); + (parseSemver as any).mockReturnValue({ major: 15, minor: 3, patch: 999 }); + expect(util.supportsProductionCompileHook()).toBe(false); + }); + }); + }); +}); diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index e7efe91e80b8..534eb4e81132 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -700,4 +700,253 @@ describe('withSentryConfig', () => { expect(finalConfig.env).toHaveProperty('_sentryRelease', 'env-release-1.5.0'); }); }); + + describe('runAfterProductionCompile hook integration', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('sets up runAfterProductionCompile hook when experimental flag is enabled and version is supported', () => { + vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(true); + + const sentryOptions = { + _experimental: { + useRunAfterProductionCompileHook: true, + }, + }; + + const finalConfig = materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + expect(finalConfig.compiler?.runAfterProductionCompile).toBeInstanceOf(Function); + }); + + it('does not set up hook when experimental flag is disabled', () => { + vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(true); + + const sentryOptions = { + _experimental: { + useRunAfterProductionCompileHook: false, + }, + }; + + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.compiler; + + const finalConfig = materializeFinalNextConfig(cleanConfig, undefined, sentryOptions); + + expect(finalConfig.compiler?.runAfterProductionCompile).toBeUndefined(); + }); + + it('does not set up hook when Next.js version is not supported', () => { + vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(false); + + const sentryOptions = { + _experimental: { + useRunAfterProductionCompileHook: true, + }, + }; + + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.compiler; + + const finalConfig = materializeFinalNextConfig(cleanConfig, undefined, sentryOptions); + + expect(finalConfig.compiler?.runAfterProductionCompile).toBeUndefined(); + }); + + it('preserves existing runAfterProductionCompile hook using proxy', () => { + vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(true); + + const originalHook = vi.fn().mockResolvedValue(undefined); + const configWithExistingHook = { + ...exportedNextConfig, + compiler: { + runAfterProductionCompile: originalHook, + }, + }; + + const sentryOptions = { + _experimental: { + useRunAfterProductionCompileHook: true, + }, + }; + + const finalConfig = materializeFinalNextConfig(configWithExistingHook, undefined, sentryOptions); + + expect(finalConfig.compiler?.runAfterProductionCompile).toBeInstanceOf(Function); + expect(finalConfig.compiler?.runAfterProductionCompile).not.toBe(originalHook); + }); + + it('warns when existing runAfterProductionCompile is not a function', () => { + vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(true); + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const configWithInvalidHook = { + ...exportedNextConfig, + compiler: { + runAfterProductionCompile: 'invalid-hook' as any, + }, + }; + + const sentryOptions = { + _experimental: { + useRunAfterProductionCompileHook: true, + }, + }; + + materializeFinalNextConfig(configWithInvalidHook, undefined, sentryOptions); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[@sentry/nextjs] The configured `compiler.runAfterProductionCompile` option is not a function. Will not run source map and release management logic.', + ); + + consoleWarnSpy.mockRestore(); + }); + + it('creates compiler object when it does not exist', () => { + vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(true); + + const configWithoutCompiler = { ...exportedNextConfig }; + delete configWithoutCompiler.compiler; + + const sentryOptions = { + _experimental: { + useRunAfterProductionCompileHook: true, + }, + }; + + const finalConfig = materializeFinalNextConfig(configWithoutCompiler, undefined, sentryOptions); + + expect(finalConfig.compiler).toBeDefined(); + expect(finalConfig.compiler?.runAfterProductionCompile).toBeInstanceOf(Function); + }); + + it('works with turbopack builds when TURBOPACK env is set', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1'); + vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(true); + + const sentryOptions = { + _experimental: { + useRunAfterProductionCompileHook: true, + }, + }; + + const finalConfig = materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + expect(finalConfig.compiler?.runAfterProductionCompile).toBeInstanceOf(Function); + + delete process.env.TURBOPACK; + }); + + it('works with webpack builds when TURBOPACK env is not set', () => { + delete process.env.TURBOPACK; + vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(true); + + const sentryOptions = { + _experimental: { + useRunAfterProductionCompileHook: true, + }, + }; + + const finalConfig = materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + expect(finalConfig.compiler?.runAfterProductionCompile).toBeInstanceOf(Function); + }); + }); + + describe('experimental flag handling', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('respects useRunAfterProductionCompileHook: true', () => { + vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(true); + + const sentryOptions = { + _experimental: { + useRunAfterProductionCompileHook: true, + }, + }; + + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.compiler; + + const finalConfig = materializeFinalNextConfig(cleanConfig, undefined, sentryOptions); + + expect(finalConfig.compiler?.runAfterProductionCompile).toBeInstanceOf(Function); + }); + + it('respects useRunAfterProductionCompileHook: false', () => { + vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(true); + + const sentryOptions = { + _experimental: { + useRunAfterProductionCompileHook: false, + }, + }; + + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.compiler; + + const finalConfig = materializeFinalNextConfig(cleanConfig, undefined, sentryOptions); + + expect(finalConfig.compiler?.runAfterProductionCompile).toBeUndefined(); + }); + + it('does not set up hook when experimental flag is undefined', () => { + vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(true); + + const sentryOptions = { + _experimental: { + // useRunAfterProductionCompileHook not specified + }, + }; + + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.compiler; + + const finalConfig = materializeFinalNextConfig(cleanConfig, undefined, sentryOptions); + + expect(finalConfig.compiler?.runAfterProductionCompile).toBeUndefined(); + }); + + it('does not set up hook when _experimental is undefined', () => { + vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(true); + + const sentryOptions = { + // no _experimental property + }; + + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.compiler; + + const finalConfig = materializeFinalNextConfig(cleanConfig, undefined, sentryOptions); + + expect(finalConfig.compiler?.runAfterProductionCompile).toBeUndefined(); + }); + + it('combines experimental flag with other configurations correctly', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1'); + vi.spyOn(util, 'supportsProductionCompileHook').mockReturnValue(true); + + const sentryOptions = { + _experimental: { + useRunAfterProductionCompileHook: true, + }, + sourcemaps: {}, + tunnelRoute: '/tunnel', + }; + + const finalConfig = materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + // Should have both turbopack sourcemap config AND runAfterProductionCompile hook + expect(finalConfig.productionBrowserSourceMaps).toBe(true); + expect(finalConfig.compiler?.runAfterProductionCompile).toBeInstanceOf(Function); + expect(finalConfig.rewrites).toBeInstanceOf(Function); + + delete process.env.TURBOPACK; + }); + }); }); From ce54409ecebe21f3a331b0bf4ef2a11e01711bb2 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 2 Sep 2025 15:24:06 +0200 Subject: [PATCH 08/17] format --- packages/nextjs/src/config/types.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 6c9ba73134b8..475bab0dc5de 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -501,13 +501,12 @@ export type SentryBuildOptions = { */ disableSentryWebpackConfig?: boolean; - /** * Contains a set of experimental flags that might change in future releases. These flags enable * features that are still in development and may be modified, renamed, or removed without notice. * Use with caution in production environments. - */ - _experimental?: Partial<{ + */ + _experimental?: Partial<{ /** * When true (and Next.js >= 15), use the runAfterProductionCompile hook to consolidate sourcemap uploads * into a single operation after all webpack/turbopack builds complete, reducing build time. From 2522da08dc4ffb9e81c7480e9acf3e70aeb35dde Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 2 Sep 2025 15:28:25 +0200 Subject: [PATCH 09/17] dedupe again --- yarn.lock | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7232132d4ebd..9b7738c17983 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7064,16 +7064,7 @@ "@sentry/bundler-plugin-core" "4.1.0" unplugin "1.0.1" -"@sentry/webpack-plugin@^4.1.1": - version "4.1.1" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-4.1.1.tgz#638c6b65cbc19b5027ffbb6bcd68094e0b0f82c6" - integrity sha512-2gFWcQMW1HdJDo/7rADeFs9crkH02l+mW4O1ORbxSjuegauyp1W8SBe7EfPoXbUmLdA3zwnpIxEXjjQpP5Etzg== - dependencies: - "@sentry/bundler-plugin-core" "4.1.1" - unplugin "1.0.1" - uuid "^9.0.0" - -"@sentry/webpack-plugin@^4.2.0": +"@sentry/webpack-plugin@^4.1.1", "@sentry/webpack-plugin@^4.2.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-4.2.0.tgz#00b89aeb1261ae15f7bc81ee9e5b5a23ca3d2dbf" integrity sha512-2lPuvJhbiEOd/NAQv5EL8at9QVKchkEmWFDioDsOG6csFqbZ8hdWtTcbsXnhzH9j+CM1LmdeDNVjIF+SMoxCNg== From a80564c16589c48f04e09177ff889d7f13a01c41 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 2 Sep 2025 17:43:49 +0200 Subject: [PATCH 10/17] bump plugin --- packages/nextjs/package.json | 3 +-- yarn.lock | 39 +++++++++++++++++++++--------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index b5c8cd99ba78..58b75a7b7ca9 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -79,7 +79,7 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@rollup/plugin-commonjs": "28.0.1", - "@sentry/bundler-plugin-core": "^4.2.0", + "@sentry/bundler-plugin-core": "^4.3.0", "@sentry-internal/browser-utils": "10.8.0", "@sentry/core": "10.8.0", "@sentry/node": "10.8.0", @@ -88,7 +88,6 @@ "@sentry/vercel-edge": "10.8.0", "@sentry/webpack-plugin": "^4.2.0", "chalk": "3.0.0", - "glob": "^11.0.3", "resolve": "1.22.8", "rollup": "^4.35.0", "stacktrace-parser": "^0.1.10" diff --git a/yarn.lock b/yarn.lock index 9b7738c17983..d158ed63ada8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6946,6 +6946,11 @@ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.2.0.tgz#6c616e6d645f49f15f83b891ef42a795ba4dbb3f" integrity sha512-GFpS3REqaHuyX4LCNqlneAQZIKyHb5ePiI1802n0fhtYjk68I1DTQ3PnbzYi50od/vAsTQVCknaS5F6tidNqTQ== +"@sentry/babel-plugin-component-annotate@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.3.0.tgz#c5b6cbb986952596d3ad233540a90a1fd18bad80" + integrity sha512-OuxqBprXRyhe8Pkfyz/4yHQJc5c3lm+TmYWSSx8u48g5yKewSQDOxkiLU5pAk3WnbLPy8XwU/PN+2BG0YFU9Nw== + "@sentry/bundler-plugin-core@4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.1.0.tgz#c1b2f7a890a44e5ac5decc984a133aacf6147dd4" @@ -6974,7 +6979,7 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/bundler-plugin-core@4.2.0", "@sentry/bundler-plugin-core@^4.2.0": +"@sentry/bundler-plugin-core@4.2.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.2.0.tgz#b607937f7cd0a769aa26974c4af3fca94abad63f" integrity sha512-EDG6ELSEN/Dzm4KUQOynoI2suEAdPdgwaBXVN4Ww705zdrYT79OGh51rkz74KGhovt7GukaPf0Z9LJwORXUbhg== @@ -6988,6 +6993,20 @@ magic-string "0.30.8" unplugin "1.0.1" +"@sentry/bundler-plugin-core@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.3.0.tgz#cf302522a3e5b8a3bf727635d0c6a7bece981460" + integrity sha512-dmR4DJhJ4jqVWGWppuTL2blNFqOZZnt4aLkewbD1myFG3KVfUx8CrMQWEmGjkgPOtj5TO6xH9PyTJjXC6o5tnA== + dependencies: + "@babel/core" "^7.18.5" + "@sentry/babel-plugin-component-annotate" "4.3.0" + "@sentry/cli" "^2.51.0" + dotenv "^16.3.1" + find-up "^5.0.0" + glob "^9.3.2" + magic-string "0.30.8" + unplugin "1.0.1" + "@sentry/cli-darwin@2.52.0": version "2.52.0" resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.52.0.tgz#05178cd819c2a33eb22a6e90bf7bb8f853f1b476" @@ -17094,7 +17113,7 @@ foreach@^2.0.5: resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= -foreground-child@^3.1.0, foreground-child@^3.3.1: +foreground-child@^3.1.0: version "3.3.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== @@ -17704,18 +17723,6 @@ glob@^10.0.0, glob@^10.2.2, glob@^10.3.10, glob@^10.3.4, glob@^10.3.7, glob@^10. package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^11.0.3: - version "11.0.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.3.tgz#9d8087e6d72ddb3c4707b1d2778f80ea3eaefcd6" - integrity sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA== - dependencies: - foreground-child "^3.3.1" - jackspeak "^4.1.1" - minimatch "^10.0.3" - minipass "^7.1.2" - package-json-from-dist "^1.0.0" - path-scurry "^2.0.0" - glob@^5.0.10: version "5.0.15" resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" @@ -19788,7 +19795,7 @@ jackspeak@^3.1.2: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" -jackspeak@^4.0.1, jackspeak@^4.1.1: +jackspeak@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.1.1.tgz#96876030f450502047fc7e8c7fcf8ce8124e43ae" integrity sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ== @@ -22046,7 +22053,7 @@ minimatch@5.1.0, minimatch@^5.0.1, minimatch@^5.1.0: dependencies: brace-expansion "^2.0.1" -minimatch@^10.0.0, minimatch@^10.0.3: +minimatch@^10.0.0: version "10.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa" integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw== From 729d3367c6dc4fb7fb4bb8758de733d3e0ec349d Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 3 Sep 2025 13:32:34 +0200 Subject: [PATCH 11/17] Update packages/nextjs/src/config/handleRunAfterProductionCompile.ts Co-authored-by: Andrei <168741329+andreiborza@users.noreply.github.com> --- packages/nextjs/src/config/handleRunAfterProductionCompile.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts index bc30d0a2bef4..8ed7d9d1e8ca 100644 --- a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts +++ b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts @@ -1,6 +1,5 @@ import type { createSentryBuildPluginManager as createSentryBuildPluginManagerType } from '@sentry/bundler-plugin-core'; import { loadModule } from '@sentry/core'; -// import { glob } from 'glob'; import { getBuildPluginOptions } from './getBuildPluginOptions'; import type { SentryBuildOptions } from './types'; From 1c73c29317be85ecba3ed93c6c378c8c14d1ec91 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 3 Sep 2025 13:46:05 +0200 Subject: [PATCH 12/17] log only after webpack check --- .../src/config/handleRunAfterProductionCompile.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts index 8ed7d9d1e8ca..01979b497c72 100644 --- a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts +++ b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts @@ -11,17 +11,17 @@ export async function handleRunAfterProductionCompile( { releaseName, distDir, buildTool }: { releaseName?: string; distDir: string; buildTool: 'webpack' | 'turbopack' }, sentryBuildOptions: SentryBuildOptions, ): Promise { - if (sentryBuildOptions.debug) { - // eslint-disable-next-line no-console - console.debug('[@sentry/nextjs] Running runAfterProductionCompile logic.'); - } - // We don't want to do anything for webpack at this point because the plugin already handles this // TODO: Actually implement this for webpack as well if (buildTool === 'webpack') { return; } + if (sentryBuildOptions.debug) { + // eslint-disable-next-line no-console + console.debug('[@sentry/nextjs] Running runAfterProductionCompile logic.'); + } + const { createSentryBuildPluginManager } = loadModule<{ createSentryBuildPluginManager: typeof createSentryBuildPluginManagerType }>( '@sentry/bundler-plugin-core', From 2ee3c570481f36848deccfe756ffb9f2275ab547 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 3 Sep 2025 13:47:01 +0200 Subject: [PATCH 13/17] Update packages/nextjs/src/config/types.ts Co-authored-by: Andrei <168741329+andreiborza@users.noreply.github.com> --- packages/nextjs/src/config/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 475bab0dc5de..1ca5eaa6bab0 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -509,7 +509,7 @@ export type SentryBuildOptions = { _experimental?: Partial<{ /** * When true (and Next.js >= 15), use the runAfterProductionCompile hook to consolidate sourcemap uploads - * into a single operation after all webpack/turbopack builds complete, reducing build time. + * into a single operation after turbopack builds complete, reducing build time. * * When false, use the traditional approach of uploading sourcemaps during each webpack build. * From e418dd2d47b2dd0ccd2978d404af43aec3714c5e Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 3 Sep 2025 14:10:27 +0200 Subject: [PATCH 14/17] bump webpack plugin version --- packages/nextjs/package.json | 2 +- yarn.lock | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 58b75a7b7ca9..2ed94494845e 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -86,7 +86,7 @@ "@sentry/opentelemetry": "10.8.0", "@sentry/react": "10.8.0", "@sentry/vercel-edge": "10.8.0", - "@sentry/webpack-plugin": "^4.2.0", + "@sentry/webpack-plugin": "^4.3.0", "chalk": "3.0.0", "resolve": "1.22.8", "rollup": "^4.35.0", diff --git a/yarn.lock b/yarn.lock index d158ed63ada8..13964d27a33b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6993,7 +6993,7 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/bundler-plugin-core@^4.3.0": +"@sentry/bundler-plugin-core@4.3.0", "@sentry/bundler-plugin-core@^4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.3.0.tgz#cf302522a3e5b8a3bf727635d0c6a7bece981460" integrity sha512-dmR4DJhJ4jqVWGWppuTL2blNFqOZZnt4aLkewbD1myFG3KVfUx8CrMQWEmGjkgPOtj5TO6xH9PyTJjXC6o5tnA== @@ -7083,7 +7083,7 @@ "@sentry/bundler-plugin-core" "4.1.0" unplugin "1.0.1" -"@sentry/webpack-plugin@^4.1.1", "@sentry/webpack-plugin@^4.2.0": +"@sentry/webpack-plugin@^4.1.1": version "4.2.0" resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-4.2.0.tgz#00b89aeb1261ae15f7bc81ee9e5b5a23ca3d2dbf" integrity sha512-2lPuvJhbiEOd/NAQv5EL8at9QVKchkEmWFDioDsOG6csFqbZ8hdWtTcbsXnhzH9j+CM1LmdeDNVjIF+SMoxCNg== @@ -7092,6 +7092,15 @@ unplugin "1.0.1" uuid "^9.0.0" +"@sentry/webpack-plugin@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-4.3.0.tgz#a96db7d8ada8646ec3ffdec2a7db6143c8061e85" + integrity sha512-K4nU1SheK/tvyakBws2zfd+MN6hzmpW+wPTbSbDWn1+WL9+g9hsPh8hjFFiVe47AhhUoUZ3YgiH2HyeHXjHflA== + dependencies: + "@sentry/bundler-plugin-core" "4.3.0" + unplugin "1.0.1" + uuid "^9.0.0" + "@sigstore/protobuf-specs@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@sigstore/protobuf-specs/-/protobuf-specs-0.1.0.tgz#957cb64ea2f5ce527cc9cf02a096baeb0d2b99b4" From a6d8e5f906a00c7db49965d93a824389fb909073 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 3 Sep 2025 14:10:37 +0200 Subject: [PATCH 15/17] update tests --- .../handleRunAfterProductionCompile.test.ts | 4 +- packages/nextjs/test/config/util.test.ts | 115 +++++++++++------- 2 files changed, 70 insertions(+), 49 deletions(-) diff --git a/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts b/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts index 3e01487cde12..22973cb6f15b 100644 --- a/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts +++ b/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts @@ -122,7 +122,7 @@ describe('handleRunAfterProductionCompile', () => { expect(mockCreateSentryBuildPluginManager).not.toHaveBeenCalled(); }); - it('logs debug message even for webpack builds when debug is enabled', async () => { + it('does not log debug message for webpack builds when debug is enabled', async () => { const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {}); const debugOptions = { @@ -139,7 +139,7 @@ describe('handleRunAfterProductionCompile', () => { debugOptions, ); - expect(consoleSpy).toHaveBeenCalledWith('[@sentry/nextjs] Running runAfterProductionCompile logic.'); + expect(consoleSpy).not.toHaveBeenCalledWith('[@sentry/nextjs] Running runAfterProductionCompile logic.'); consoleSpy.mockRestore(); }); diff --git a/packages/nextjs/test/config/util.test.ts b/packages/nextjs/test/config/util.test.ts index 54a6f4b20c67..01b94480ea5f 100644 --- a/packages/nextjs/test/config/util.test.ts +++ b/packages/nextjs/test/config/util.test.ts @@ -1,10 +1,9 @@ -import { parseSemver } from '@sentry/core'; +import * as fs from 'fs'; import { afterEach, describe, expect, it, vi } from 'vitest'; import * as util from '../../src/config/util'; -vi.mock('@sentry/core', () => ({ - parseSemver: vi.fn(), -})); +// Mock fs to control what getNextjsVersion reads +vi.mock('fs'); describe('util', () => { describe('supportsProductionCompileHook', () => { @@ -14,134 +13,156 @@ describe('util', () => { describe('supported versions', () => { it('returns true for Next.js 15.4.1', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1'); - (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 1 }); - expect(util.supportsProductionCompileHook()).toBe(true); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15.4.1' })); + + const result = util.supportsProductionCompileHook(); + expect(result).toBe(true); }); it('returns true for Next.js 15.4.2', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.2'); - (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 2 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15.4.2' })); + expect(util.supportsProductionCompileHook()).toBe(true); }); it('returns true for Next.js 15.5.0', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.5.0'); - (parseSemver as any).mockReturnValue({ major: 15, minor: 5, patch: 0 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15.5.0' })); + expect(util.supportsProductionCompileHook()).toBe(true); }); it('returns true for Next.js 16.0.0', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('16.0.0'); - (parseSemver as any).mockReturnValue({ major: 16, minor: 0, patch: 0 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '16.0.0' })); + expect(util.supportsProductionCompileHook()).toBe(true); }); it('returns true for Next.js 17.0.0', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('17.0.0'); - (parseSemver as any).mockReturnValue({ major: 17, minor: 0, patch: 0 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '17.0.0' })); + expect(util.supportsProductionCompileHook()).toBe(true); }); it('returns true for supported canary versions', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1-canary.42'); - (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 1 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15.4.1-canary.42' })); + expect(util.supportsProductionCompileHook()).toBe(true); }); it('returns true for supported rc versions', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1-rc.1'); - (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 1 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15.4.1-rc.1' })); + expect(util.supportsProductionCompileHook()).toBe(true); }); }); describe('unsupported versions', () => { it('returns false for Next.js 15.4.0', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.0'); - (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 0 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15.4.0' })); + expect(util.supportsProductionCompileHook()).toBe(false); }); it('returns false for Next.js 15.3.9', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.9'); - (parseSemver as any).mockReturnValue({ major: 15, minor: 3, patch: 9 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15.3.9' })); + expect(util.supportsProductionCompileHook()).toBe(false); }); it('returns false for Next.js 15.0.0', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.0.0'); - (parseSemver as any).mockReturnValue({ major: 15, minor: 0, patch: 0 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15.0.0' })); + expect(util.supportsProductionCompileHook()).toBe(false); }); it('returns false for Next.js 14.2.0', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('14.2.0'); - (parseSemver as any).mockReturnValue({ major: 14, minor: 2, patch: 0 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '14.2.0' })); + expect(util.supportsProductionCompileHook()).toBe(false); }); it('returns false for unsupported canary versions', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.0-canary.42'); - (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 0 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15.4.0-canary.42' })); + expect(util.supportsProductionCompileHook()).toBe(false); }); }); describe('edge cases', () => { it('returns false for invalid version strings', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('invalid.version'); - (parseSemver as any).mockReturnValue({ major: undefined, minor: undefined, patch: undefined }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: 'invalid.version' })); + expect(util.supportsProductionCompileHook()).toBe(false); }); it('handles versions with build metadata', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1+build.123'); - (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 1 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15.4.1+build.123' })); + expect(util.supportsProductionCompileHook()).toBe(true); }); it('handles versions with pre-release identifiers', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1-alpha.1'); - (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 1 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15.4.1-alpha.1' })); + expect(util.supportsProductionCompileHook()).toBe(true); }); it('returns false for versions missing patch number', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4'); - (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: undefined }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15.4' })); + expect(util.supportsProductionCompileHook()).toBe(false); }); it('returns false for versions missing minor number', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15'); - (parseSemver as any).mockReturnValue({ major: 15, minor: undefined, patch: undefined }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15' })); + expect(util.supportsProductionCompileHook()).toBe(false); }); }); describe('version boundary tests', () => { it('returns false for 15.4.0 (just below threshold)', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.0'); - (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 0 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15.4.0' })); + expect(util.supportsProductionCompileHook()).toBe(false); }); it('returns true for 15.4.1 (exact threshold)', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1'); - (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 1 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15.4.1' })); + expect(util.supportsProductionCompileHook()).toBe(true); }); it('returns true for 15.4.2 (just above threshold)', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.2'); - (parseSemver as any).mockReturnValue({ major: 15, minor: 4, patch: 2 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15.4.2' })); + expect(util.supportsProductionCompileHook()).toBe(true); }); it('returns false for 15.3.999 (high patch but wrong minor)', () => { - vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.999'); - (parseSemver as any).mockReturnValue({ major: 15, minor: 3, patch: 999 }); + const mockReadFileSync = fs.readFileSync as any; + mockReadFileSync.mockReturnValue(JSON.stringify({ version: '15.3.999' })); + expect(util.supportsProductionCompileHook()).toBe(false); }); }); From c766201798df3995393fcbd2de60d1bfdc6f6d5c Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 3 Sep 2025 14:59:12 +0200 Subject: [PATCH 16/17] dedupe --- yarn.lock | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index 13964d27a33b..5ed389ab0a1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7083,16 +7083,7 @@ "@sentry/bundler-plugin-core" "4.1.0" unplugin "1.0.1" -"@sentry/webpack-plugin@^4.1.1": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-4.2.0.tgz#00b89aeb1261ae15f7bc81ee9e5b5a23ca3d2dbf" - integrity sha512-2lPuvJhbiEOd/NAQv5EL8at9QVKchkEmWFDioDsOG6csFqbZ8hdWtTcbsXnhzH9j+CM1LmdeDNVjIF+SMoxCNg== - dependencies: - "@sentry/bundler-plugin-core" "4.2.0" - unplugin "1.0.1" - uuid "^9.0.0" - -"@sentry/webpack-plugin@^4.3.0": +"@sentry/webpack-plugin@^4.1.1", "@sentry/webpack-plugin@^4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-4.3.0.tgz#a96db7d8ada8646ec3ffdec2a7db6143c8061e85" integrity sha512-K4nU1SheK/tvyakBws2zfd+MN6hzmpW+wPTbSbDWn1+WL9+g9hsPh8hjFFiVe47AhhUoUZ3YgiH2HyeHXjHflA== From 975361015653182f36a3febe14979841b3986966 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 3 Sep 2025 23:14:15 +0200 Subject: [PATCH 17/17] update sourcemap deletion logic --- .../nextjs/src/config/withSentryConfig.ts | 22 +++---- .../test/config/withSentryConfig.test.ts | 66 +++++++++++++++++-- 2 files changed, 73 insertions(+), 15 deletions(-) diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 713ca4187c38..4558e5349c5a 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -332,18 +332,18 @@ function getFinalConfigObject( // eslint-disable-next-line no-console console.log('[@sentry/nextjs] Automatically enabling browser source map generation for turbopack build.'); incomingUserNextConfigObject.productionBrowserSourceMaps = true; - } - // Enable source map deletion if not explicitly disabled - if (userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload === undefined) { - // eslint-disable-next-line no-console - console.warn( - '[@sentry/nextjs] Source maps will be automatically deleted after being uploaded to Sentry. If you want to keep the source maps, set the `sourcemaps.deleteSourcemapsAfterUpload` option to false in `withSentryConfig()`. If you do not want to generate and upload sourcemaps at all, set the `sourcemaps.disable` option to true.', - ); - userSentryOptions.sourcemaps = { - ...userSentryOptions.sourcemaps, - deleteSourcemapsAfterUpload: true, - }; + // Enable source map deletion if not explicitly disabled + if (userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload === undefined) { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/nextjs] Source maps will be automatically deleted after being uploaded to Sentry. If you want to keep the source maps, set the `sourcemaps.deleteSourcemapsAfterUpload` option to false in `withSentryConfig()`. If you do not want to generate and upload sourcemaps at all, set the `sourcemaps.disable` option to true.', + ); + userSentryOptions.sourcemaps = { + ...userSentryOptions.sourcemaps, + deleteSourcemapsAfterUpload: true, + }; + } } } diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index 534eb4e81132..9303223c97bc 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -365,12 +365,18 @@ describe('withSentryConfig', () => { process.env.TURBOPACK = '1'; vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + // Use a clean config without productionBrowserSourceMaps to ensure it gets auto-enabled + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.productionBrowserSourceMaps; + const sentryOptions = { sourcemaps: {}, // no deleteSourcemapsAfterUpload setting }; - materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + const finalConfig = materializeFinalNextConfig(cleanConfig, undefined, sentryOptions); + // Both productionBrowserSourceMaps and deleteSourcemapsAfterUpload should be enabled + expect(finalConfig.productionBrowserSourceMaps).toBe(true); expect(sentryOptions.sourcemaps).toHaveProperty('deleteSourcemapsAfterUpload', true); }); @@ -404,6 +410,45 @@ describe('withSentryConfig', () => { expect(sentryOptions.sourcemaps).not.toHaveProperty('deleteSourcemapsAfterUpload'); }); + it('does not enable deleteSourcemapsAfterUpload when user pre-configured productionBrowserSourceMaps: true', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const configWithSourceMapsPreEnabled = { + ...exportedNextConfig, + productionBrowserSourceMaps: true, // User already enabled + }; + + const sentryOptions = { + sourcemaps: {}, // no explicit deleteSourcemapsAfterUpload setting + }; + + materializeFinalNextConfig(configWithSourceMapsPreEnabled, undefined, sentryOptions); + + // Should NOT automatically enable deletion because productionBrowserSourceMaps was already set by user + expect(sentryOptions.sourcemaps).not.toHaveProperty('deleteSourcemapsAfterUpload'); + }); + + it('does not enable sourcemaps or deletion when user explicitly sets productionBrowserSourceMaps: false', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + + const configWithSourceMapsDisabled = { + ...exportedNextConfig, + productionBrowserSourceMaps: false, // User explicitly disabled + }; + + const sentryOptions = { + sourcemaps: {}, // no explicit deleteSourcemapsAfterUpload setting + }; + + const finalConfig = materializeFinalNextConfig(configWithSourceMapsDisabled, undefined, sentryOptions); + + // Should NOT modify productionBrowserSourceMaps or enable deletion when user explicitly set to false + expect(finalConfig.productionBrowserSourceMaps).toBe(false); + expect(sentryOptions.sourcemaps).not.toHaveProperty('deleteSourcemapsAfterUpload'); + }); + it('logs correct message when enabling sourcemaps for turbopack', () => { process.env.TURBOPACK = '1'; vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); @@ -426,11 +471,15 @@ describe('withSentryConfig', () => { vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + // Use a clean config without productionBrowserSourceMaps to trigger automatic enablement + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.productionBrowserSourceMaps; + const sentryOptions = { sourcemaps: {}, // triggers automatic deletion }; - materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + materializeFinalNextConfig(cleanConfig, undefined, sentryOptions); expect(consoleWarnSpy).toHaveBeenCalledWith( '[@sentry/nextjs] Source maps will be automatically deleted after being uploaded to Sentry. If you want to keep the source maps, set the `sourcemaps.deleteSourcemapsAfterUpload` option to false in `withSentryConfig()`. If you do not want to generate and upload sourcemaps at all, set the `sourcemaps.disable` option to true.', @@ -517,11 +566,15 @@ describe('withSentryConfig', () => { process.env.TURBOPACK = '1'; vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + // Use a clean config without productionBrowserSourceMaps to trigger automatic enablement + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.productionBrowserSourceMaps; + const sentryOptions = { sourcemaps: {}, // empty object }; - materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + materializeFinalNextConfig(cleanConfig, undefined, sentryOptions); expect(sentryOptions.sourcemaps).toHaveProperty('deleteSourcemapsAfterUpload', true); }); @@ -551,12 +604,16 @@ describe('withSentryConfig', () => { process.env.TURBOPACK = '1'; vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.3.0'); + // Use a clean config without productionBrowserSourceMaps to trigger automatic enablement + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.productionBrowserSourceMaps; + const sentryOptions = { tunnelRoute: '/custom-tunnel', sourcemaps: {}, }; - const finalConfig = materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + const finalConfig = materializeFinalNextConfig(cleanConfig, undefined, sentryOptions); expect(finalConfig.productionBrowserSourceMaps).toBe(true); expect(sentryOptions.sourcemaps).toHaveProperty('deleteSourcemapsAfterUpload', true); @@ -573,6 +630,7 @@ describe('withSentryConfig', () => { const cleanConfig = { ...exportedNextConfig }; delete cleanConfig.env; + delete cleanConfig.productionBrowserSourceMaps; // Ensure it gets auto-enabled const sentryOptions = { release: {