diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index 3dfef3bbad08..6de802917015 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -2,38 +2,264 @@ import type { Options as SentryBuildPluginOptions } from '@sentry/bundler-plugin import * as path from 'path'; import type { SentryBuildOptions } from './types'; +const LOGGER_PREFIXES = { + 'webpack-nodejs': '[@sentry/nextjs - Node.js]', + 'webpack-edge': '[@sentry/nextjs - Edge]', + 'webpack-client': '[@sentry/nextjs - Client]', + 'after-production-compile-webpack': '[@sentry/nextjs - After Production Compile (Webpack)]', + 'after-production-compile-turbopack': '[@sentry/nextjs - After Production Compile (Turbopack)]', +} as const; + +// File patterns for source map operations +// We use both glob patterns and directory paths for the sourcemap upload and deletion +// -> Direct CLI invocation handles file paths better than glob patterns +// -> Webpack/Bundler needs glob patterns as this is the format that is used by the plugin +const FILE_PATTERNS = { + SERVER: { + GLOB: 'server/**', + PATH: 'server', + }, + SERVERLESS: 'serverless/**', + STATIC_CHUNKS: { + GLOB: 'static/chunks/**', + PATH: 'static/chunks', + }, + STATIC_CHUNKS_PAGES: { + GLOB: 'static/chunks/pages/**', + PATH: 'static/chunks/pages', + }, + STATIC_CHUNKS_APP: { + GLOB: 'static/chunks/app/**', + PATH: 'static/chunks/app', + }, + MAIN_CHUNKS: 'static/chunks/main-*', + FRAMEWORK_CHUNKS: 'static/chunks/framework-*', + FRAMEWORK_CHUNKS_DOT: 'static/chunks/framework.*', + POLYFILLS_CHUNKS: 'static/chunks/polyfills-*', + WEBPACK_CHUNKS: 'static/chunks/webpack-*', +} as const; + +// Source map file extensions to delete +const SOURCEMAP_EXTENSIONS = ['*.js.map', '*.mjs.map', '*.cjs.map'] as const; + +type BuildTool = keyof typeof LOGGER_PREFIXES; + +/** + * Normalizes Windows paths to POSIX format for glob patterns + */ +export function normalizePathForGlob(distPath: string): string { + return distPath.replace(/\\/g, '/'); +} + +/** + * These functions are used to get the correct pattern for the sourcemap upload based on the build tool and the usage context + * -> Direct CLI invocation handles file paths better than glob patterns + */ +function getServerPattern({ useDirectoryPath = false }: { useDirectoryPath?: boolean }): string { + return useDirectoryPath ? FILE_PATTERNS.SERVER.PATH : FILE_PATTERNS.SERVER.GLOB; +} + +function getStaticChunksPattern({ useDirectoryPath = false }: { useDirectoryPath?: boolean }): string { + return useDirectoryPath ? FILE_PATTERNS.STATIC_CHUNKS.PATH : FILE_PATTERNS.STATIC_CHUNKS.GLOB; +} + +function getStaticChunksPagesPattern({ useDirectoryPath = false }: { useDirectoryPath?: boolean }): string { + return useDirectoryPath ? FILE_PATTERNS.STATIC_CHUNKS_PAGES.PATH : FILE_PATTERNS.STATIC_CHUNKS_PAGES.GLOB; +} + +function getStaticChunksAppPattern({ useDirectoryPath = false }: { useDirectoryPath?: boolean }): string { + return useDirectoryPath ? FILE_PATTERNS.STATIC_CHUNKS_APP.PATH : FILE_PATTERNS.STATIC_CHUNKS_APP.GLOB; +} + /** - * Get Sentry Build Plugin options for the runAfterProductionCompile hook. + * Creates file patterns for source map uploads based on build tool and options + */ +function createSourcemapUploadAssetPatterns( + normalizedDistPath: string, + buildTool: BuildTool, + widenClientFileUpload: boolean = false, +): string[] { + const assets: string[] = []; + + if (buildTool.startsWith('after-production-compile')) { + assets.push(path.posix.join(normalizedDistPath, getServerPattern({ useDirectoryPath: true }))); + + if (buildTool === 'after-production-compile-turbopack') { + // In turbopack we always want to upload the full static chunks directory + // as the build output is not split into pages|app chunks + assets.push(path.posix.join(normalizedDistPath, getStaticChunksPattern({ useDirectoryPath: true }))); + } else { + // Webpack client builds in after-production-compile mode + if (widenClientFileUpload) { + assets.push(path.posix.join(normalizedDistPath, getStaticChunksPattern({ useDirectoryPath: true }))); + } else { + assets.push( + path.posix.join(normalizedDistPath, getStaticChunksPagesPattern({ useDirectoryPath: true })), + path.posix.join(normalizedDistPath, getStaticChunksAppPattern({ useDirectoryPath: true })), + ); + } + } + } else { + if (buildTool === 'webpack-nodejs' || buildTool === 'webpack-edge') { + // Server builds + assets.push( + path.posix.join(normalizedDistPath, getServerPattern({ useDirectoryPath: false })), + path.posix.join(normalizedDistPath, FILE_PATTERNS.SERVERLESS), + ); + } else if (buildTool === 'webpack-client') { + // Client builds + if (widenClientFileUpload) { + assets.push(path.posix.join(normalizedDistPath, getStaticChunksPattern({ useDirectoryPath: false }))); + } else { + assets.push( + path.posix.join(normalizedDistPath, getStaticChunksPagesPattern({ useDirectoryPath: false })), + path.posix.join(normalizedDistPath, getStaticChunksAppPattern({ useDirectoryPath: false })), + ); + } + } + } + + return assets; +} + +/** + * Creates ignore patterns for source map uploads + */ +function createSourcemapUploadIgnorePattern( + normalizedDistPath: string, + widenClientFileUpload: boolean = false, +): string[] { + const ignore: string[] = []; + + // We only add main-* files if the user has not opted into it + if (!widenClientFileUpload) { + ignore.push(path.posix.join(normalizedDistPath, FILE_PATTERNS.MAIN_CHUNKS)); + } + + // Always ignore these patterns + ignore.push( + path.posix.join(normalizedDistPath, FILE_PATTERNS.FRAMEWORK_CHUNKS), + path.posix.join(normalizedDistPath, FILE_PATTERNS.FRAMEWORK_CHUNKS_DOT), + path.posix.join(normalizedDistPath, FILE_PATTERNS.POLYFILLS_CHUNKS), + path.posix.join(normalizedDistPath, FILE_PATTERNS.WEBPACK_CHUNKS), + ); + + return ignore; +} + +/** + * Creates file patterns for deletion after source map upload + */ +function createFilesToDeleteAfterUploadPattern( + normalizedDistPath: string, + buildTool: BuildTool, + deleteSourcemapsAfterUpload: boolean, + useRunAfterProductionCompileHook: boolean = false, +): string[] | undefined { + if (!deleteSourcemapsAfterUpload) { + return undefined; + } + + // We don't want to delete source maps for server builds as this led to errors on Vercel in the past + // See: https://github.com/getsentry/sentry-javascript/issues/13099 + if (buildTool === 'webpack-nodejs' || buildTool === 'webpack-edge') { + return undefined; + } + + // Skip deletion for webpack client builds when using the experimental hook + if (buildTool === 'webpack-client' && useRunAfterProductionCompileHook) { + return undefined; + } + + return SOURCEMAP_EXTENSIONS.map(ext => path.posix.join(normalizedDistPath, 'static', '**', ext)); +} + +/** + * Determines if sourcemap uploads should be skipped + */ +function shouldSkipSourcemapUpload(buildTool: BuildTool, useRunAfterProductionCompileHook: boolean = false): boolean { + return useRunAfterProductionCompileHook && buildTool.startsWith('webpack'); +} + +/** + * Source rewriting function for webpack sources + */ +function rewriteWebpackSources(source: string): string { + return source.replace(/^webpack:\/\/(?:_N_E\/)?/, ''); +} + +/** + * Creates release configuration + */ +function createReleaseConfig( + releaseName: string | undefined, + sentryBuildOptions: SentryBuildOptions, +): SentryBuildPluginOptions['release'] { + if (releaseName !== undefined) { + return { + 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, + }; + } + + return { + inject: false, + create: false, + finalize: false, + }; +} + +/** + * Get Sentry Build Plugin options for both webpack and turbopack builds. + * These options can be used in two ways: + * 1. The options can be built in a single operation after the production build completes + * 2. The options can be built in multiple operations, one for each webpack build */ export function getBuildPluginOptions({ sentryBuildOptions, releaseName, distDirAbsPath, + buildTool, + useRunAfterProductionCompileHook, }: { sentryBuildOptions: SentryBuildOptions; releaseName: string | undefined; distDirAbsPath: string; + buildTool: BuildTool; + useRunAfterProductionCompileHook?: boolean; // Whether the user has opted into using the experimental hook }): 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, '/'); + const normalizedDistDirAbsPath = normalizePathForGlob(distDirAbsPath); - sourcemapUploadAssets.push( - path.posix.join(normalizedDistDirAbsPath, '**'), // Next.js build output + const loggerPrefix = LOGGER_PREFIXES[buildTool]; + const widenClientFileUpload = sentryBuildOptions.widenClientFileUpload ?? false; + const deleteSourcemapsAfterUpload = sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload ?? false; + + const sourcemapUploadAssets = createSourcemapUploadAssetPatterns( + normalizedDistDirAbsPath, + buildTool, + widenClientFileUpload, ); - if (sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload) { - filesToDeleteAfterUpload.push( - path.posix.join(normalizedDistDirAbsPath, '**', '*.js.map'), - path.posix.join(normalizedDistDirAbsPath, '**', '*.mjs.map'), - path.posix.join(normalizedDistDirAbsPath, '**', '*.cjs.map'), - ); - } + + const sourcemapUploadIgnore = createSourcemapUploadIgnorePattern(normalizedDistDirAbsPath, widenClientFileUpload); + + const filesToDeleteAfterUpload = createFilesToDeleteAfterUploadPattern( + normalizedDistDirAbsPath, + buildTool, + deleteSourcemapsAfterUpload, + useRunAfterProductionCompileHook, + ); + + const skipSourcemapsUpload = shouldSkipSourcemapUpload(buildTool, useRunAfterProductionCompileHook); return { authToken: sentryBuildOptions.authToken, @@ -43,51 +269,28 @@ export function getBuildPluginOptions({ telemetry: sentryBuildOptions.telemetry, debug: sentryBuildOptions.debug, errorHandler: sentryBuildOptions.errorHandler, - reactComponentAnnotation: { - ...sentryBuildOptions.reactComponentAnnotation, - ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.reactComponentAnnotation, - }, + reactComponentAnnotation: buildTool.startsWith('after-production-compile') + ? undefined + : { + ...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; - } - }, + disable: skipSourcemapsUpload ? true : (sentryBuildOptions.sourcemaps?.disable ?? false), + rewriteSources: rewriteWebpackSources, 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, - }, + release: createReleaseConfig(releaseName, sentryBuildOptions), bundleSizeOptimizations: { ...sentryBuildOptions.bundleSizeOptimizations, }, _metaOptions: { - loggerPrefixOverride: '[@sentry/nextjs]', + loggerPrefixOverride: loggerPrefix, telemetry: { metaFramework: 'nextjs', }, diff --git a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts index 01979b497c72..c8dc35918198 100644 --- a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts +++ b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts @@ -11,12 +11,6 @@ export async function handleRunAfterProductionCompile( { releaseName, distDir, buildTool }: { releaseName?: string; distDir: string; buildTool: 'webpack' | 'turbopack' }, sentryBuildOptions: SentryBuildOptions, ): Promise { - // 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.'); @@ -36,17 +30,17 @@ export async function handleRunAfterProductionCompile( return; } - const sentryBuildPluginManager = createSentryBuildPluginManager( - getBuildPluginOptions({ - sentryBuildOptions, - releaseName, - distDirAbsPath: distDir, - }), - { - buildTool, - loggerPrefix: '[@sentry/nextjs]', - }, - ); + const options = getBuildPluginOptions({ + sentryBuildOptions, + releaseName, + distDirAbsPath: distDir, + buildTool: `after-production-compile-${buildTool}`, + }); + + const sentryBuildPluginManager = createSentryBuildPluginManager(options, { + buildTool, + loggerPrefix: '[@sentry/nextjs - After Production Compile]', + }); await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal(); await sentryBuildPluginManager.createRelease(); diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 8741efe81194..6ba07cd09f8f 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -7,6 +7,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { sync as resolveSync } from 'resolve'; import type { VercelCronsConfig } from '../common/types'; +import { getBuildPluginOptions, normalizePathForGlob } from './getBuildPluginOptions'; import type { RouteManifest } from './manifest/types'; // Note: If you need to import a type from Webpack, do it in `types.ts` and export it from there. Otherwise, our // circular dependency check thinks this file is importing from itself. See https://github.com/pahen/madge/issues/306. @@ -22,7 +23,6 @@ import type { WebpackEntryProperty, } from './types'; import { getNextjsVersion } from './util'; -import { getWebpackPluginOptions } from './webpackPluginOptions'; // Next.js runs webpack 3 times, once for the client, the server, and for edge. Because we don't want to print certain // warnings 3 times, we keep track of them here. @@ -40,13 +40,21 @@ let showedMissingGlobalErrorWarningMsg = false; * @param userSentryOptions The user's SentryWebpackPlugin config, as passed to `withSentryConfig` * @returns The function to set as the nextjs config's `webpack` value */ -export function constructWebpackConfigFunction( - userNextConfig: NextConfigObject = {}, - userSentryOptions: SentryBuildOptions = {}, - releaseName: string | undefined, - routeManifest: RouteManifest | undefined, - nextJsVersion: string | undefined, -): WebpackConfigFunction { +export function constructWebpackConfigFunction({ + userNextConfig = {}, + userSentryOptions = {}, + releaseName, + routeManifest, + nextJsVersion, + useRunAfterProductionCompileHook, +}: { + userNextConfig: NextConfigObject; + userSentryOptions: SentryBuildOptions; + releaseName: string | undefined; + routeManifest: RouteManifest | undefined; + nextJsVersion: string | undefined; + useRunAfterProductionCompileHook: boolean | undefined; +}): WebpackConfigFunction { // Will be called by nextjs and passed its default webpack configuration and context data about the build (whether // we're building server or client, whether we're in dev, what version of webpack we're using, etc). Note that // `incomingConfig` and `buildContext` are referred to as `config` and `options` in the nextjs docs. @@ -408,9 +416,22 @@ export function constructWebpackConfigFunction( } newConfig.plugins = newConfig.plugins || []; + const { config: userNextConfig, dir, nextRuntime } = buildContext; + const buildTool = isServer ? (nextRuntime === 'edge' ? 'webpack-edge' : 'webpack-nodejs') : 'webpack-client'; + const projectDir = normalizePathForGlob(dir); + const distDir = normalizePathForGlob((userNextConfig as NextConfigObject).distDir ?? '.next'); + const distDirAbsPath = path.posix.join(projectDir, distDir); + const sentryWebpackPluginInstance = sentryWebpackPlugin( - getWebpackPluginOptions(buildContext, userSentryOptions, releaseName), + getBuildPluginOptions({ + sentryBuildOptions: userSentryOptions, + releaseName, + distDirAbsPath, + buildTool, + useRunAfterProductionCompileHook, + }), ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access sentryWebpackPluginInstance._name = 'sentry-webpack-plugin'; // For tests and debugging. Serves no other purpose. newConfig.plugins.push(sentryWebpackPluginInstance); diff --git a/packages/nextjs/src/config/webpackPluginOptions.ts b/packages/nextjs/src/config/webpackPluginOptions.ts deleted file mode 100644 index f4ff4363cdb7..000000000000 --- a/packages/nextjs/src/config/webpackPluginOptions.ts +++ /dev/null @@ -1,126 +0,0 @@ -import type { SentryWebpackPluginOptions } from '@sentry/webpack-plugin'; -import * as path from 'path'; -import type { BuildContext, NextConfigObject, SentryBuildOptions } from './types'; - -/** - * Combine default and user-provided SentryWebpackPlugin options, accounting for whether we're building server files or - * client files. - */ -export function getWebpackPluginOptions( - buildContext: BuildContext, - sentryBuildOptions: SentryBuildOptions, - releaseName: string | undefined, -): SentryWebpackPluginOptions { - const { isServer, config: userNextConfig, dir, nextRuntime } = buildContext; - - const prefixInsert = !isServer ? 'Client' : nextRuntime === 'edge' ? 'Edge' : 'Node.js'; - - // 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 projectDir = dir.replace(/\\/g, '/'); - // `.next` is the default directory - const distDir = (userNextConfig as NextConfigObject).distDir?.replace(/\\/g, '/') ?? '.next'; - const distDirAbsPath = path.posix.join(projectDir, distDir); - - const sourcemapUploadAssets: string[] = []; - const sourcemapUploadIgnore: string[] = []; - - if (isServer) { - sourcemapUploadAssets.push( - path.posix.join(distDirAbsPath, 'server', '**'), // This is normally where Next.js outputs things - path.posix.join(distDirAbsPath, 'serverless', '**'), // This was the output location for serverless Next.js - ); - } else { - if (sentryBuildOptions.widenClientFileUpload) { - sourcemapUploadAssets.push(path.posix.join(distDirAbsPath, 'static', 'chunks', '**')); - } else { - sourcemapUploadAssets.push( - path.posix.join(distDirAbsPath, 'static', 'chunks', 'pages', '**'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'app', '**'), - ); - } - - // We want to include main-* files if widenClientFileUpload is true as they have proven to be useful - if (!sentryBuildOptions.widenClientFileUpload) { - sourcemapUploadIgnore.push(path.posix.join(distDirAbsPath, 'static', 'chunks', 'main-*')); - } - - // Always ignore framework, polyfills, and webpack files - sourcemapUploadIgnore.push( - path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework-*'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework.*'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'polyfills-*'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'webpack-*'), - ); - } - - 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.sourcemaps?.deleteSourcemapsAfterUpload - ? [ - // We only care to delete client bundle source maps because they would be the ones being served. - // Removing the server source maps crashes Vercel builds for (thus far) unknown reasons: - // https://github.com/getsentry/sentry-javascript/issues/13099 - path.posix.join(distDirAbsPath, 'static', '**', '*.js.map'), - path.posix.join(distDirAbsPath, 'static', '**', '*.mjs.map'), - path.posix.join(distDirAbsPath, 'static', '**', '*.cjs.map'), - ] - : undefined, - ...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 - ${prefixInsert}]`, - telemetry: { - metaFramework: 'nextjs', - }, - }, - ...sentryBuildOptions.unstable_sentryWebpackPluginOptions, - }; -} diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 4558e5349c5a..494052af26f2 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -329,16 +329,21 @@ function getFinalConfigObject( 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.'); + if (userSentryOptions.debug) { + // 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.', - ); + if (userSentryOptions.debug) { + // 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, @@ -368,13 +373,14 @@ function getFinalConfigObject( webpack: isTurbopack || userSentryOptions.disableSentryWebpackConfig ? incomingUserNextConfigObject.webpack // just return the original webpack config - : constructWebpackConfigFunction( - incomingUserNextConfigObject, + : constructWebpackConfigFunction({ + userNextConfig: incomingUserNextConfigObject, userSentryOptions, releaseName, routeManifest, nextJsVersion, - ), + useRunAfterProductionCompileHook: userSentryOptions._experimental?.useRunAfterProductionCompileHook, + }), ...(isTurbopackSupported && isTurbopack ? { turbopack: constructTurbopackConfig({ diff --git a/packages/nextjs/test/config/getBuildPluginOptions.test.ts b/packages/nextjs/test/config/getBuildPluginOptions.test.ts index 1120084ec76e..0281624584d0 100644 --- a/packages/nextjs/test/config/getBuildPluginOptions.test.ts +++ b/packages/nextjs/test/config/getBuildPluginOptions.test.ts @@ -7,7 +7,7 @@ describe('getBuildPluginOptions', () => { const mockDistDirAbsPath = '/path/to/.next'; describe('basic functionality', () => { - it('returns correct build plugin options with minimal configuration', () => { + it('returns correct build plugin options with minimal configuration for after-production-compile-webpack', () => { const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', project: 'test-project', @@ -18,6 +18,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'after-production-compile-webpack', }); expect(result).toMatchObject({ @@ -25,9 +26,15 @@ describe('getBuildPluginOptions', () => { org: 'test-org', project: 'test-project', sourcemaps: { - assets: ['/path/to/.next/**'], - ignore: [], - filesToDeleteAfterUpload: [], + assets: ['/path/to/.next/server', '/path/to/.next/static/chunks/pages', '/path/to/.next/static/chunks/app'], + ignore: [ + '/path/to/.next/static/chunks/main-*', + '/path/to/.next/static/chunks/framework-*', + '/path/to/.next/static/chunks/framework.*', + '/path/to/.next/static/chunks/polyfills-*', + '/path/to/.next/static/chunks/webpack-*', + ], + filesToDeleteAfterUpload: undefined, rewriteSources: expect.any(Function), }, release: { @@ -37,16 +44,17 @@ describe('getBuildPluginOptions', () => { finalize: undefined, }, _metaOptions: { - loggerPrefixOverride: '[@sentry/nextjs]', + loggerPrefixOverride: '[@sentry/nextjs - After Production Compile (Webpack)]', telemetry: { metaFramework: 'nextjs', }, }, bundleSizeOptimizations: {}, + reactComponentAnnotation: undefined, // Should be undefined for after-production-compile }); }); - it('normalizes Windows paths to posix for glob patterns', () => { + it('normalizes Windows paths to posix for glob patterns in after-production-compile builds', () => { const windowsPath = 'C:\\Users\\test\\.next'; const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', @@ -57,14 +65,253 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: windowsPath, + buildTool: 'after-production-compile-webpack', }); - expect(result.sourcemaps?.assets).toEqual(['C:/Users/test/.next/**']); + expect(result.sourcemaps?.assets).toEqual([ + 'C:/Users/test/.next/server', + 'C:/Users/test/.next/static/chunks/pages', + 'C:/Users/test/.next/static/chunks/app', + ]); + }); + + it('normalizes Windows paths to posix for webpack builds', () => { + const windowsPath = 'C:\\Users\\test\\.next'; + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: windowsPath, + buildTool: 'webpack-client', + }); + + expect(result.sourcemaps?.assets).toEqual([ + 'C:/Users/test/.next/static/chunks/pages/**', + 'C:/Users/test/.next/static/chunks/app/**', + ]); + }); + }); + + describe('build tool specific behavior', () => { + const baseSentryOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + }; + + it('configures webpack-client build correctly', () => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', + }); + + expect(result._metaOptions?.loggerPrefixOverride).toBe('[@sentry/nextjs - Client]'); + expect(result.sourcemaps?.assets).toEqual([ + '/path/to/.next/static/chunks/pages/**', + '/path/to/.next/static/chunks/app/**', + ]); + expect(result.sourcemaps?.ignore).toEqual([ + '/path/to/.next/static/chunks/main-*', + '/path/to/.next/static/chunks/framework-*', + '/path/to/.next/static/chunks/framework.*', + '/path/to/.next/static/chunks/polyfills-*', + '/path/to/.next/static/chunks/webpack-*', + ]); + expect(result.reactComponentAnnotation).toBeDefined(); + }); + + it('configures webpack-client build with widenClientFileUpload correctly', () => { + const result = getBuildPluginOptions({ + sentryBuildOptions: { + ...baseSentryOptions, + widenClientFileUpload: true, + }, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', + }); + + expect(result.sourcemaps?.assets).toEqual(['/path/to/.next/static/chunks/**']); + expect(result.sourcemaps?.ignore).toEqual([ + '/path/to/.next/static/chunks/framework-*', + '/path/to/.next/static/chunks/framework.*', + '/path/to/.next/static/chunks/polyfills-*', + '/path/to/.next/static/chunks/webpack-*', + ]); + }); + + it('configures webpack-nodejs build correctly', () => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-nodejs', + }); + + expect(result._metaOptions?.loggerPrefixOverride).toBe('[@sentry/nextjs - Node.js]'); + expect(result.sourcemaps?.assets).toEqual(['/path/to/.next/server/**', '/path/to/.next/serverless/**']); + expect(result.sourcemaps?.ignore).toEqual([ + '/path/to/.next/static/chunks/main-*', + '/path/to/.next/static/chunks/framework-*', + '/path/to/.next/static/chunks/framework.*', + '/path/to/.next/static/chunks/polyfills-*', + '/path/to/.next/static/chunks/webpack-*', + ]); + expect(result.reactComponentAnnotation).toBeDefined(); + }); + + it('configures webpack-edge build correctly', () => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-edge', + }); + + expect(result._metaOptions?.loggerPrefixOverride).toBe('[@sentry/nextjs - Edge]'); + expect(result.sourcemaps?.assets).toEqual(['/path/to/.next/server/**', '/path/to/.next/serverless/**']); + expect(result.sourcemaps?.ignore).toEqual([ + '/path/to/.next/static/chunks/main-*', + '/path/to/.next/static/chunks/framework-*', + '/path/to/.next/static/chunks/framework.*', + '/path/to/.next/static/chunks/polyfills-*', + '/path/to/.next/static/chunks/webpack-*', + ]); + expect(result.reactComponentAnnotation).toBeDefined(); + }); + + it('configures after-production-compile-webpack build correctly', () => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'after-production-compile-webpack', + }); + + expect(result._metaOptions?.loggerPrefixOverride).toBe('[@sentry/nextjs - After Production Compile (Webpack)]'); + expect(result.sourcemaps?.assets).toEqual([ + '/path/to/.next/server', + '/path/to/.next/static/chunks/pages', + '/path/to/.next/static/chunks/app', + ]); + expect(result.sourcemaps?.ignore).toEqual([ + '/path/to/.next/static/chunks/main-*', + '/path/to/.next/static/chunks/framework-*', + '/path/to/.next/static/chunks/framework.*', + '/path/to/.next/static/chunks/polyfills-*', + '/path/to/.next/static/chunks/webpack-*', + ]); + expect(result.reactComponentAnnotation).toBeUndefined(); + }); + + it('configures after-production-compile-turbopack build correctly', () => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'after-production-compile-turbopack', + }); + + expect(result._metaOptions?.loggerPrefixOverride).toBe('[@sentry/nextjs - After Production Compile (Turbopack)]'); + expect(result.sourcemaps?.assets).toEqual([ + '/path/to/.next/server', + '/path/to/.next/static/chunks', // Turbopack uses broader pattern + ]); + expect(result.sourcemaps?.ignore).toEqual([ + '/path/to/.next/static/chunks/main-*', + '/path/to/.next/static/chunks/framework-*', + '/path/to/.next/static/chunks/framework.*', + '/path/to/.next/static/chunks/polyfills-*', + '/path/to/.next/static/chunks/webpack-*', + ]); + expect(result.reactComponentAnnotation).toBeUndefined(); + }); + }); + + describe('useRunAfterProductionCompileHook functionality', () => { + const baseSentryOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + }; + + it('disables sourcemaps when useRunAfterProductionCompileHook is true for webpack builds', () => { + const webpackBuildTools = ['webpack-client', 'webpack-nodejs', 'webpack-edge'] as const; + + webpackBuildTools.forEach(buildTool => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool, + useRunAfterProductionCompileHook: true, + }); + + expect(result.sourcemaps?.disable).toBe(true); + }); + }); + + it('does not disable sourcemaps when useRunAfterProductionCompileHook is true for after-production-compile builds', () => { + const afterProductionCompileBuildTools = [ + 'after-production-compile-webpack', + 'after-production-compile-turbopack', + ] as const; + + afterProductionCompileBuildTools.forEach(buildTool => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool, + useRunAfterProductionCompileHook: true, + }); + + expect(result.sourcemaps?.disable).toBe(false); + }); + }); + + it('does not disable sourcemaps when useRunAfterProductionCompileHook is false', () => { + const result = getBuildPluginOptions({ + sentryBuildOptions: baseSentryOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', + useRunAfterProductionCompileHook: false, + }); + + expect(result.sourcemaps?.disable).toBe(false); }); }); describe('sourcemap configuration', () => { - it('configures file deletion when deleteSourcemapsAfterUpload is enabled', () => { + it('configures file deletion when deleteSourcemapsAfterUpload is enabled for after-production-compile-webpack', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + sourcemaps: { + deleteSourcemapsAfterUpload: true, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'after-production-compile-webpack', + }); + + expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([ + '/path/to/.next/static/**/*.js.map', + '/path/to/.next/static/**/*.mjs.map', + '/path/to/.next/static/**/*.cjs.map', + ]); + }); + + it('configures file deletion when deleteSourcemapsAfterUpload is enabled for after-production-compile-turbopack', () => { const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', project: 'test-project', @@ -77,15 +324,84 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'after-production-compile-turbopack', }); expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([ - '/path/to/.next/**/*.js.map', - '/path/to/.next/**/*.mjs.map', - '/path/to/.next/**/*.cjs.map', + '/path/to/.next/static/**/*.js.map', + '/path/to/.next/static/**/*.mjs.map', + '/path/to/.next/static/**/*.cjs.map', ]); }); + it('configures file deletion when deleteSourcemapsAfterUpload is enabled for webpack-client without useRunAfterProductionCompileHook', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + sourcemaps: { + deleteSourcemapsAfterUpload: true, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', + useRunAfterProductionCompileHook: false, + }); + + expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([ + '/path/to/.next/static/**/*.js.map', + '/path/to/.next/static/**/*.mjs.map', + '/path/to/.next/static/**/*.cjs.map', + ]); + }); + + it('does not configure file deletion when deleteSourcemapsAfterUpload is enabled for webpack-client with useRunAfterProductionCompileHook', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + sourcemaps: { + deleteSourcemapsAfterUpload: true, + }, + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', + useRunAfterProductionCompileHook: true, + }); + + // File deletion should be undefined when using the hook + expect(result.sourcemaps?.filesToDeleteAfterUpload).toBeUndefined(); + }); + + it('does not configure file deletion for server builds even when deleteSourcemapsAfterUpload is enabled', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + sourcemaps: { + deleteSourcemapsAfterUpload: true, + }, + }; + + const serverBuildTools = ['webpack-nodejs', 'webpack-edge'] as const; + + serverBuildTools.forEach(buildTool => { + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool, + }); + + expect(result.sourcemaps?.filesToDeleteAfterUpload).toBeUndefined(); + }); + }); + it('does not configure file deletion when deleteSourcemapsAfterUpload is disabled', () => { const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', @@ -99,9 +415,10 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); - expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([]); + expect(result.sourcemaps?.filesToDeleteAfterUpload).toBeUndefined(); }); it('uses custom sourcemap assets when provided', () => { @@ -118,6 +435,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); expect(result.sourcemaps?.assets).toEqual(customAssets); @@ -137,6 +455,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); expect(result.sourcemaps?.ignore).toEqual(customIgnore); @@ -155,6 +474,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); expect(result.sourcemaps?.disable).toBe(true); @@ -172,6 +492,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); const rewriteSources = result.sourcemaps?.rewriteSources; @@ -209,6 +530,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); expect(result.release).toMatchObject({ @@ -230,6 +552,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: undefined, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); expect(result.release).toMatchObject({ @@ -263,6 +586,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); // The unstable_sentryWebpackPluginOptions.release is spread at the end and may override base properties @@ -272,7 +596,7 @@ describe('getBuildPluginOptions', () => { }); describe('react component annotation', () => { - it('merges react component annotation options correctly', () => { + it('merges react component annotation options correctly for webpack builds', () => { const sentryBuildOptions: SentryBuildOptions = { org: 'test-org', project: 'test-project', @@ -290,11 +614,38 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); // The unstable options override the base options - in this case enabled should be false expect(result.reactComponentAnnotation).toHaveProperty('enabled', false); }); + + it('sets react component annotation to undefined for after-production-compile builds', () => { + const sentryBuildOptions: SentryBuildOptions = { + org: 'test-org', + project: 'test-project', + reactComponentAnnotation: { + enabled: true, + }, + }; + + const afterProductionCompileBuildTools = [ + 'after-production-compile-webpack', + 'after-production-compile-turbopack', + ] as const; + + afterProductionCompileBuildTools.forEach(buildTool => { + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool, + }); + + expect(result.reactComponentAnnotation).toBeUndefined(); + }); + }); }); describe('other configuration options', () => { @@ -318,6 +669,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); expect(result).toMatchObject({ @@ -352,6 +704,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); expect(result).toMatchObject({ @@ -374,6 +727,7 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: undefined, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'webpack-client', }); expect(result.release).toMatchObject({ @@ -394,13 +748,20 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: mockDistDirAbsPath, + buildTool: 'after-production-compile-webpack', }); expect(result.sourcemaps).toMatchObject({ - disable: undefined, - assets: ['/path/to/.next/**'], - ignore: [], - filesToDeleteAfterUpload: [], + disable: false, + assets: ['/path/to/.next/server', '/path/to/.next/static/chunks/pages', '/path/to/.next/static/chunks/app'], + ignore: [ + '/path/to/.next/static/chunks/main-*', + '/path/to/.next/static/chunks/framework-*', + '/path/to/.next/static/chunks/framework.*', + '/path/to/.next/static/chunks/polyfills-*', + '/path/to/.next/static/chunks/webpack-*', + ], + filesToDeleteAfterUpload: undefined, rewriteSources: expect.any(Function), }); }); @@ -419,13 +780,14 @@ describe('getBuildPluginOptions', () => { sentryBuildOptions, releaseName: mockReleaseName, distDirAbsPath: complexPath, + buildTool: 'after-production-compile-turbopack', }); - expect(result.sourcemaps?.assets).toEqual([`${complexPath}/**`]); + expect(result.sourcemaps?.assets).toEqual([`${complexPath}/server`, `${complexPath}/static/chunks`]); expect(result.sourcemaps?.filesToDeleteAfterUpload).toEqual([ - `${complexPath}/**/*.js.map`, - `${complexPath}/**/*.mjs.map`, - `${complexPath}/**/*.cjs.map`, + `${complexPath}/static/**/*.js.map`, + `${complexPath}/static/**/*.mjs.map`, + `${complexPath}/static/**/*.cjs.map`, ]); }); }); diff --git a/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts b/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts index 22973cb6f15b..f32eb28ddcfc 100644 --- a/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts +++ b/packages/nextjs/test/config/handleRunAfterProductionCompile.test.ts @@ -79,7 +79,7 @@ describe('handleRunAfterProductionCompile', () => { }), { buildTool: 'turbopack', - loggerPrefix: '[@sentry/nextjs]', + loggerPrefix: '[@sentry/nextjs - After Production Compile]', }, ); }); @@ -108,7 +108,7 @@ describe('handleRunAfterProductionCompile', () => { }); describe('webpack builds', () => { - it('skips execution for webpack builds', async () => { + it('executes all build steps for webpack builds', async () => { await handleRunAfterProductionCompile( { releaseName: 'test-release', @@ -118,11 +118,16 @@ describe('handleRunAfterProductionCompile', () => { mockSentryBuildOptions, ); - expect(loadModule).not.toHaveBeenCalled(); - expect(mockCreateSentryBuildPluginManager).not.toHaveBeenCalled(); + 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('does not log debug message for webpack builds when debug is enabled', async () => { + it('logs debug message for webpack builds when debug is enabled', async () => { const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {}); const debugOptions = { @@ -139,7 +144,7 @@ describe('handleRunAfterProductionCompile', () => { debugOptions, ); - expect(consoleSpy).not.toHaveBeenCalledWith('[@sentry/nextjs] Running runAfterProductionCompile logic.'); + expect(consoleSpy).toHaveBeenCalledWith('[@sentry/nextjs] Running runAfterProductionCompile logic.'); consoleSpy.mockRestore(); }); diff --git a/packages/nextjs/test/config/testUtils.ts b/packages/nextjs/test/config/testUtils.ts index 19e2a8f1c326..a644525ce311 100644 --- a/packages/nextjs/test/config/testUtils.ts +++ b/packages/nextjs/test/config/testUtils.ts @@ -56,6 +56,10 @@ export async function materializeFinalWebpackConfig(options: { incomingWebpackConfig: WebpackConfigObject; incomingWebpackBuildContext: BuildContext; sentryBuildTimeOptions?: SentryBuildOptions; + releaseName?: string; + routeManifest?: any; + nextJsVersion?: string; + useRunAfterProductionCompileHook?: boolean; }): Promise { const { exportedNextConfig, incomingWebpackConfig, incomingWebpackBuildContext } = options; @@ -66,11 +70,16 @@ export async function materializeFinalWebpackConfig(options: { : exportedNextConfig; // get the webpack config function we'd normally pass back to next - const webpackConfigFunction = constructWebpackConfigFunction( - materializedUserNextConfig, - options.sentryBuildTimeOptions, - undefined, - ); + const webpackConfigFunction = constructWebpackConfigFunction({ + userNextConfig: materializedUserNextConfig, + userSentryOptions: options.sentryBuildTimeOptions || {}, + releaseName: options.releaseName, + routeManifest: options.routeManifest, + nextJsVersion: options.nextJsVersion, + useRunAfterProductionCompileHook: + options.useRunAfterProductionCompileHook ?? + options.sentryBuildTimeOptions?._experimental?.useRunAfterProductionCompileHook, + }); // call it to get concrete values for comparison const finalWebpackConfigValue = webpackConfigFunction(incomingWebpackConfig, incomingWebpackBuildContext); diff --git a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts index 7371d35c859a..d46bcd917fb7 100644 --- a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts +++ b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts @@ -2,8 +2,8 @@ import '../mocks'; import * as core from '@sentry/core'; import { describe, expect, it, vi } from 'vitest'; +import * as getBuildPluginOptionsModule from '../../../src/config/getBuildPluginOptions'; import * as util from '../../../src/config/util'; -import * as getWebpackPluginOptionsModule from '../../../src/config/webpackPluginOptions'; import { CLIENT_SDK_CONFIG_FILE, clientBuildContext, @@ -55,7 +55,7 @@ describe('constructWebpackConfigFunction()', () => { }); it('automatically enables deleteSourcemapsAfterUpload for client builds when not explicitly set', async () => { - const getWebpackPluginOptionsSpy = vi.spyOn(getWebpackPluginOptionsModule, 'getWebpackPluginOptions'); + const getBuildPluginOptionsSpy = vi.spyOn(getBuildPluginOptionsModule, 'getBuildPluginOptions'); vi.spyOn(core, 'loadModule').mockImplementation(() => ({ sentryWebpackPlugin: () => ({ _name: 'sentry-webpack-plugin', @@ -71,19 +71,100 @@ describe('constructWebpackConfigFunction()', () => { }, }); - expect(getWebpackPluginOptionsSpy).toHaveBeenCalledWith( + expect(getBuildPluginOptionsSpy).toHaveBeenCalledWith( expect.objectContaining({ - isServer: false, + sentryBuildOptions: expect.objectContaining({ + sourcemaps: { + deleteSourcemapsAfterUpload: true, + }, + }), + buildTool: 'webpack-client', + distDirAbsPath: expect.any(String), + releaseName: undefined, + }), + ); + + getBuildPluginOptionsSpy.mockRestore(); + }); + + it('passes useRunAfterProductionCompileHook to getBuildPluginOptions when enabled', async () => { + const getBuildPluginOptionsSpy = vi.spyOn(getBuildPluginOptionsModule, 'getBuildPluginOptions'); + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', }), + })); + + await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: { + _experimental: { + useRunAfterProductionCompileHook: true, + }, + }, + }); + + expect(getBuildPluginOptionsSpy).toHaveBeenCalledWith( expect.objectContaining({ - sourcemaps: { - deleteSourcemapsAfterUpload: true, + useRunAfterProductionCompileHook: true, + }), + ); + + getBuildPluginOptionsSpy.mockRestore(); + }); + + it('passes useRunAfterProductionCompileHook to getBuildPluginOptions when disabled', async () => { + const getBuildPluginOptionsSpy = vi.spyOn(getBuildPluginOptionsModule, 'getBuildPluginOptions'); + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: { + _experimental: { + useRunAfterProductionCompileHook: false, }, + }, + }); + + expect(getBuildPluginOptionsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + useRunAfterProductionCompileHook: false, + }), + ); + + getBuildPluginOptionsSpy.mockRestore(); + }); + + it('passes useRunAfterProductionCompileHook as undefined when not specified', async () => { + const getBuildPluginOptionsSpy = vi.spyOn(getBuildPluginOptionsModule, 'getBuildPluginOptions'); + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: {}, + }); + + expect(getBuildPluginOptionsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + useRunAfterProductionCompileHook: undefined, }), - undefined, ); - getWebpackPluginOptionsSpy.mockRestore(); + getBuildPluginOptionsSpy.mockRestore(); }); it('preserves unrelated webpack config options', async () => { diff --git a/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts b/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts deleted file mode 100644 index e95ab5c82bf8..000000000000 --- a/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import type { BuildContext, NextConfigObject } from '../../../src/config/types'; -import { getWebpackPluginOptions } from '../../../src/config/webpackPluginOptions'; - -function generateBuildContext(overrides: { - dir?: string; - isServer: boolean; - nextjsConfig?: NextConfigObject; -}): BuildContext { - return { - dev: false, // The plugin is not included in dev mode - isServer: overrides.isServer, - buildId: 'test-build-id', - dir: overrides.dir ?? '/my/project/dir', - config: overrides.nextjsConfig ?? {}, - totalPages: 2, - defaultLoaders: true, - webpack: { - version: '4.0.0', - DefinePlugin: {} as any, - }, - }; -} - -describe('getWebpackPluginOptions()', () => { - it('forwards relevant options', () => { - const buildContext = generateBuildContext({ isServer: false }); - const generatedPluginOptions = getWebpackPluginOptions( - buildContext, - { - authToken: 'my-auth-token', - headers: { 'my-test-header': 'test' }, - org: 'my-org', - project: 'my-project', - telemetry: false, - reactComponentAnnotation: { - enabled: true, - ignoredComponents: ['myComponent'], - }, - silent: false, - debug: true, - sentryUrl: 'my-url', - sourcemaps: { - assets: ['my-asset'], - ignore: ['my-ignore'], - }, - release: { - name: 'my-release', - create: false, - finalize: false, - dist: 'my-dist', - vcsRemote: 'my-origin', - setCommits: { - auto: true, - }, - deploy: { - env: 'my-env', - }, - }, - }, - 'my-release', - ); - - expect(generatedPluginOptions.authToken).toBe('my-auth-token'); - expect(generatedPluginOptions.debug).toBe(true); - expect(generatedPluginOptions.headers).toStrictEqual({ 'my-test-header': 'test' }); - expect(generatedPluginOptions.org).toBe('my-org'); - expect(generatedPluginOptions.project).toBe('my-project'); - expect(generatedPluginOptions.reactComponentAnnotation?.enabled).toBe(true); - expect(generatedPluginOptions.reactComponentAnnotation?.ignoredComponents).toStrictEqual(['myComponent']); - expect(generatedPluginOptions.release?.create).toBe(false); - expect(generatedPluginOptions.release?.deploy?.env).toBe('my-env'); - expect(generatedPluginOptions.release?.dist).toBe('my-dist'); - expect(generatedPluginOptions.release?.finalize).toBe(false); - expect(generatedPluginOptions.release?.name).toBe('my-release'); - expect(generatedPluginOptions.release?.setCommits?.auto).toBe(true); - expect(generatedPluginOptions.release?.vcsRemote).toBe('my-origin'); - expect(generatedPluginOptions.silent).toBe(false); - expect(generatedPluginOptions.sourcemaps?.assets).toStrictEqual(['my-asset']); - expect(generatedPluginOptions.sourcemaps?.ignore).toStrictEqual(['my-ignore']); - expect(generatedPluginOptions.telemetry).toBe(false); - expect(generatedPluginOptions.url).toBe('my-url'); - - expect(generatedPluginOptions).toMatchObject({ - authToken: 'my-auth-token', - debug: true, - headers: { - 'my-test-header': 'test', - }, - org: 'my-org', - project: 'my-project', - reactComponentAnnotation: { - enabled: true, - ignoredComponents: ['myComponent'], - }, - release: { - create: false, - deploy: { - env: 'my-env', - }, - dist: 'my-dist', - finalize: false, - inject: false, - name: 'my-release', - setCommits: { - auto: true, - }, - vcsRemote: 'my-origin', - }, - silent: false, - sourcemaps: { - assets: ['my-asset'], - ignore: ['my-ignore'], - }, - telemetry: false, - url: 'my-url', - }); - }); - - it('forwards bundleSizeOptimization options', () => { - const buildContext = generateBuildContext({ isServer: false }); - const generatedPluginOptions = getWebpackPluginOptions( - buildContext, - { - bundleSizeOptimizations: { - excludeTracing: true, - excludeReplayShadowDom: false, - }, - }, - undefined, - ); - - expect(generatedPluginOptions).toMatchObject({ - bundleSizeOptimizations: { - excludeTracing: true, - excludeReplayShadowDom: false, - }, - }); - }); - - it('forwards errorHandler option', () => { - const buildContext = generateBuildContext({ isServer: false }); - const mockErrorHandler = (err: Error) => { - throw err; - }; - - const generatedPluginOptions = getWebpackPluginOptions( - buildContext, - { - errorHandler: mockErrorHandler, - }, - undefined, - ); - - expect(generatedPluginOptions.errorHandler).toBe(mockErrorHandler); - }); - - it('returns the right `assets` and `ignore` values during the server build', () => { - const buildContext = generateBuildContext({ isServer: true }); - const generatedPluginOptions = getWebpackPluginOptions(buildContext, {}, undefined); - expect(generatedPluginOptions.sourcemaps).toMatchObject({ - assets: ['/my/project/dir/.next/server/**', '/my/project/dir/.next/serverless/**'], - ignore: [], - }); - }); - - it('returns the right `assets` and `ignore` values during the client build', () => { - const buildContext = generateBuildContext({ isServer: false }); - const generatedPluginOptions = getWebpackPluginOptions(buildContext, {}, undefined); - expect(generatedPluginOptions.sourcemaps).toMatchObject({ - assets: ['/my/project/dir/.next/static/chunks/pages/**', '/my/project/dir/.next/static/chunks/app/**'], - ignore: [ - '/my/project/dir/.next/static/chunks/main-*', - '/my/project/dir/.next/static/chunks/framework-*', - '/my/project/dir/.next/static/chunks/framework.*', - '/my/project/dir/.next/static/chunks/polyfills-*', - '/my/project/dir/.next/static/chunks/webpack-*', - ], - }); - }); - - it('returns the right `assets` and `ignore` values during the client build with `widenClientFileUpload`', () => { - const buildContext = generateBuildContext({ isServer: false }); - const generatedPluginOptions = getWebpackPluginOptions(buildContext, { widenClientFileUpload: true }, undefined); - expect(generatedPluginOptions.sourcemaps).toMatchObject({ - assets: ['/my/project/dir/.next/static/chunks/**'], - ignore: [ - '/my/project/dir/.next/static/chunks/framework-*', - '/my/project/dir/.next/static/chunks/framework.*', - '/my/project/dir/.next/static/chunks/polyfills-*', - '/my/project/dir/.next/static/chunks/webpack-*', - ], - }); - }); - - it('sets `sourcemaps.disable` plugin options to true when `sourcemaps.disable` is true', () => { - const buildContext = generateBuildContext({ isServer: false }); - const generatedPluginOptions = getWebpackPluginOptions(buildContext, { sourcemaps: { disable: true } }, undefined); - expect(generatedPluginOptions.sourcemaps).toMatchObject({ - disable: true, - }); - }); - - it('passes posix paths to the plugin', () => { - const buildContext = generateBuildContext({ - dir: 'C:\\my\\windows\\project\\dir', - nextjsConfig: { distDir: '.dist\\v1' }, - isServer: false, - }); - const generatedPluginOptions = getWebpackPluginOptions(buildContext, { widenClientFileUpload: true }, undefined); - expect(generatedPluginOptions.sourcemaps).toMatchObject({ - assets: ['C:/my/windows/project/dir/.dist/v1/static/chunks/**'], - ignore: [ - 'C:/my/windows/project/dir/.dist/v1/static/chunks/framework-*', - 'C:/my/windows/project/dir/.dist/v1/static/chunks/framework.*', - 'C:/my/windows/project/dir/.dist/v1/static/chunks/polyfills-*', - 'C:/my/windows/project/dir/.dist/v1/static/chunks/webpack-*', - ], - }); - }); - - it('sets options to not create a release or do any release operations when releaseName is undefined', () => { - const buildContext = generateBuildContext({ isServer: false }); - const generatedPluginOptions = getWebpackPluginOptions(buildContext, {}, undefined); - - expect(generatedPluginOptions).toMatchObject({ - release: { - inject: false, - create: false, - finalize: false, - }, - }); - }); -}); diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index 9303223c97bc..d0b30aa7eae3 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -457,7 +457,11 @@ describe('withSentryConfig', () => { const cleanConfig = { ...exportedNextConfig }; delete cleanConfig.productionBrowserSourceMaps; - materializeFinalNextConfig(cleanConfig); + const sentryOptions = { + debug: true, + }; + + materializeFinalNextConfig(cleanConfig, undefined, sentryOptions); expect(consoleSpy).toHaveBeenCalledWith( '[@sentry/nextjs] Automatically enabling browser source map generation for turbopack build.', @@ -476,6 +480,7 @@ describe('withSentryConfig', () => { delete cleanConfig.productionBrowserSourceMaps; const sentryOptions = { + debug: true, sourcemaps: {}, // triggers automatic deletion };