var { _nullishCoalesce, _optionalChain } = require('@sentry/utils'); Object.defineProperty(exports, '__esModule', { value: true }); const fs = require('fs'); const path = require('path'); const node = require('@sentry/node'); const utils = require('@sentry/utils'); const chalk = require('chalk'); const resolve = require('resolve'); const debugBuild = require('../common/debug-build.js'); const RUNTIME_TO_SDK_ENTRYPOINT_MAP = { client: './client', server: './server', edge: './edge', } ; // 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. let showedMissingAuthTokenErrorMsg = false; let showedMissingOrgSlugErrorMsg = false; let showedMissingProjectSlugErrorMsg = false; let showedHiddenSourceMapsWarningMsg = false; let showedMissingCliBinaryWarningMsg = false; let showedMissingGlobalErrorWarningMsg = false; // TODO: merge default SentryWebpackPlugin ignore with their SentryWebpackPlugin ignore or ignoreFile // TODO: merge default SentryWebpackPlugin include with their SentryWebpackPlugin include // TODO: drop merged keys from override check? `includeDefaults` option? /** * Construct the function which will be used as the nextjs config's `webpack` value. * * Sets: * - `devtool`, to ensure high-quality sourcemaps are generated * - `entry`, to include user's sentry config files (where `Sentry.init` is called) in the build * - `plugins`, to add SentryWebpackPlugin * * @param userNextConfig The user's existing nextjs config, as passed to `withSentryConfig` * @param userSentryWebpackPluginOptions The user's SentryWebpackPlugin config, as passed to `withSentryConfig` * @returns The function to set as the nextjs config's `webpack` value */ function constructWebpackConfigFunction( userNextConfig = {}, userSentryWebpackPluginOptions = {}, userSentryOptions = {}, ) { // 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. return function newWebpackFunction( incomingConfig, buildContext, ) { const { isServer, dev: isDev, dir: projectDir } = buildContext; const runtime = isServer ? (buildContext.nextRuntime === 'edge' ? 'edge' : 'server') : 'client'; let rawNewConfig = { ...incomingConfig }; // if user has custom webpack config (which always takes the form of a function), run it so we have actual values to // work with if ('webpack' in userNextConfig && typeof userNextConfig.webpack === 'function') { rawNewConfig = userNextConfig.webpack(rawNewConfig, buildContext); } // This mutates `rawNewConfig` in place, but also returns it in order to switch its type to one in which // `newConfig.module.rules` is required, so we don't have to keep asserting its existence const newConfig = setUpModuleRules(rawNewConfig); // Add a loader which will inject code that sets global values addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions, buildContext, userSentryWebpackPluginOptions); newConfig.module.rules.push({ test: /node_modules[/\\]@sentry[/\\]nextjs/, use: [ { loader: path.resolve(__dirname, 'loaders', 'sdkMultiplexerLoader.js'), options: { importTarget: RUNTIME_TO_SDK_ENTRYPOINT_MAP[runtime], }, }, ], }); let pagesDirPath; const maybePagesDirPath = path.join(projectDir, 'pages'); const maybeSrcPagesDirPath = path.join(projectDir, 'src', 'pages'); if (fs.existsSync(maybePagesDirPath) && fs.lstatSync(maybePagesDirPath).isDirectory()) { pagesDirPath = maybePagesDirPath; } else if (fs.existsSync(maybeSrcPagesDirPath) && fs.lstatSync(maybeSrcPagesDirPath).isDirectory()) { pagesDirPath = maybeSrcPagesDirPath; } let appDirPath; const maybeAppDirPath = path.join(projectDir, 'app'); const maybeSrcAppDirPath = path.join(projectDir, 'src', 'app'); if (fs.existsSync(maybeAppDirPath) && fs.lstatSync(maybeAppDirPath).isDirectory()) { appDirPath = maybeAppDirPath; } else if (fs.existsSync(maybeSrcAppDirPath) && fs.lstatSync(maybeSrcAppDirPath).isDirectory()) { appDirPath = maybeSrcAppDirPath; } const apiRoutesPath = pagesDirPath ? path.join(pagesDirPath, 'api') : undefined; const middlewareLocationFolder = pagesDirPath ? path.join(pagesDirPath, '..') : appDirPath ? path.join(appDirPath, '..') : projectDir; // Default page extensions per https://github.com/vercel/next.js/blob/f1dbc9260d48c7995f6c52f8fbcc65f08e627992/packages/next/server/config-shared.ts#L161 const pageExtensions = userNextConfig.pageExtensions || ['tsx', 'ts', 'jsx', 'js']; const dotPrefixedPageExtensions = pageExtensions.map(ext => `.${ext}`); const pageExtensionRegex = pageExtensions.map(utils.escapeStringForRegex).join('|'); const staticWrappingLoaderOptions = { appDir: appDirPath, pagesDir: pagesDirPath, pageExtensionRegex, excludeServerRoutes: userSentryOptions.excludeServerRoutes, sentryConfigFilePath: getUserConfigFilePath(projectDir, runtime), nextjsRequestAsyncStorageModulePath: getRequestAsyncStorageModuleLocation( projectDir, _optionalChain([rawNewConfig, 'access', _ => _.resolve, 'optionalAccess', _2 => _2.modules]), ), }; const normalizeLoaderResourcePath = (resourcePath) => { // `resourcePath` may be an absolute path or a path relative to the context of the webpack config let absoluteResourcePath; if (path.isAbsolute(resourcePath)) { absoluteResourcePath = resourcePath; } else { absoluteResourcePath = path.join(projectDir, resourcePath); } return path.normalize(absoluteResourcePath); }; const isPageResource = (resourcePath) => { const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath); return ( pagesDirPath !== undefined && normalizedAbsoluteResourcePath.startsWith(pagesDirPath + path.sep) && !normalizedAbsoluteResourcePath.startsWith(apiRoutesPath + path.sep) && dotPrefixedPageExtensions.some(ext => normalizedAbsoluteResourcePath.endsWith(ext)) ); }; const isApiRouteResource = (resourcePath) => { const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath); return ( normalizedAbsoluteResourcePath.startsWith(apiRoutesPath + path.sep) && dotPrefixedPageExtensions.some(ext => normalizedAbsoluteResourcePath.endsWith(ext)) ); }; const possibleMiddlewareLocations = ['js', 'jsx', 'ts', 'tsx'].map(middlewareFileEnding => { return path.join(middlewareLocationFolder, `middleware.${middlewareFileEnding}`); }); const isMiddlewareResource = (resourcePath) => { const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath); return possibleMiddlewareLocations.includes(normalizedAbsoluteResourcePath); }; const isServerComponentResource = (resourcePath) => { const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath); // ".js, .jsx, or .tsx file extensions can be used for Pages" // https://beta.nextjs.org/docs/routing/pages-and-layouts#pages:~:text=.js%2C%20.jsx%2C%20or%20.tsx%20file%20extensions%20can%20be%20used%20for%20Pages. return ( appDirPath !== undefined && normalizedAbsoluteResourcePath.startsWith(appDirPath + path.sep) && !!normalizedAbsoluteResourcePath.match(/[\\/](page|layout|loading|head|not-found)\.(js|jsx|tsx)$/) ); }; const isRouteHandlerResource = (resourcePath) => { const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath); return ( appDirPath !== undefined && normalizedAbsoluteResourcePath.startsWith(appDirPath + path.sep) && !!normalizedAbsoluteResourcePath.match(/[\\/]route\.(js|jsx|ts|tsx)$/) ); }; if (isServer && userSentryOptions.autoInstrumentServerFunctions !== false) { // It is very important that we insert our loaders at the beginning of the array because we expect any sort of transformations/transpilations (e.g. TS -> JS) to already have happened. // Wrap pages newConfig.module.rules.unshift({ test: isPageResource, use: [ { loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'), options: { ...staticWrappingLoaderOptions, wrappingTargetKind: 'page', }, }, ], }); let vercelCronsConfig = undefined; try { if (process.env.VERCEL && userSentryOptions.automaticVercelMonitors) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access vercelCronsConfig = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'vercel.json'), 'utf8')).crons; if (vercelCronsConfig) { utils.logger.info( `${chalk.cyan( 'info', )} - Creating Sentry cron monitors for your Vercel Cron Jobs. You can disable this feature by setting the ${chalk.bold.cyan( 'automaticVercelMonitors', )} option to false in you Next.js config.`, ); } } } catch (e) { if ((e ).code === 'ENOENT') ; else { // log but noop utils.logger.error(`${chalk.red('error')} - Sentry failed to read vercel.json`, e); } } // Wrap api routes newConfig.module.rules.unshift({ test: isApiRouteResource, use: [ { loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'), options: { ...staticWrappingLoaderOptions, vercelCronsConfig, wrappingTargetKind: 'api-route', }, }, ], }); // Wrap middleware if (_nullishCoalesce(userSentryOptions.autoInstrumentMiddleware, () => ( true))) { newConfig.module.rules.unshift({ test: isMiddlewareResource, use: [ { loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'), options: { ...staticWrappingLoaderOptions, wrappingTargetKind: 'middleware', }, }, ], }); } } if (isServer && userSentryOptions.autoInstrumentAppDirectory !== false) { // Wrap server components newConfig.module.rules.unshift({ test: isServerComponentResource, use: [ { loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'), options: { ...staticWrappingLoaderOptions, wrappingTargetKind: 'server-component', }, }, ], }); // Wrap route handlers newConfig.module.rules.unshift({ test: isRouteHandlerResource, use: [ { loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'), options: { ...staticWrappingLoaderOptions, wrappingTargetKind: 'route-handler', }, }, ], }); } if (isServer) { // Import the Sentry config in every user file newConfig.module.rules.unshift({ test: resourcePath => { return ( isPageResource(resourcePath) || isApiRouteResource(resourcePath) || isMiddlewareResource(resourcePath) || isServerComponentResource(resourcePath) || isRouteHandlerResource(resourcePath) ); }, use: [ { loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'), options: { ...staticWrappingLoaderOptions, wrappingTargetKind: 'sentry-init', }, }, ], }); } if (appDirPath) { const hasGlobalErrorFile = ['global-error.js', 'global-error.jsx', 'global-error.ts', 'global-error.tsx'].some( globalErrorFile => fs.existsSync(path.join(appDirPath, globalErrorFile)), ); if (!hasGlobalErrorFile && !showedMissingGlobalErrorWarningMsg) { // eslint-disable-next-line no-console console.log( `${chalk.yellow( 'warn', )} - It seems like you don't have a global error handler set up. It is recommended that you add a ${chalk.cyan( 'global-error.js', )} file with Sentry instrumentation so that React rendering errors are reported to Sentry. Read more: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#react-render-errors-in-app-router`, ); showedMissingGlobalErrorWarningMsg = true; } } // The SDK uses syntax (ES6 and ES6+ features like object spread) which isn't supported by older browsers. For users // who want to support such browsers, `transpileClientSDK` allows them to force the SDK code to go through the same // transpilation that their code goes through. We don't turn this on by default because it increases bundle size // fairly massively. if (!isServer && _optionalChain([userSentryOptions, 'optionalAccess', _3 => _3.transpileClientSDK])) { // Find all loaders which apply transpilation to user code const transpilationRules = findTranspilationRules(_optionalChain([newConfig, 'access', _4 => _4.module, 'optionalAccess', _5 => _5.rules]), projectDir); // For each matching rule, wrap its `exclude` function so that it won't exclude SDK files, even though they're in // `node_modules` (which is otherwise excluded) transpilationRules.forEach(rule => { // All matching rules will necessarily have an `exclude` property, but this keeps TS happy if (rule.exclude && typeof rule.exclude === 'function') { const origExclude = rule.exclude; const newExclude = (filepath) => { if (filepath.includes('@sentry')) { // `false` in this case means "don't exclude it" return false; } return origExclude(filepath); }; rule.exclude = newExclude; } }); } // Tell webpack to inject user config files (containing the two `Sentry.init()` calls) into the appropriate output // bundles. Store a separate reference to the original `entry` value to avoid an infinite loop. (If we don't do // this, we'll have a statement of the form `x.y = () => f(x.y)`, where one of the things `f` does is call `x.y`. // Since we're setting `x.y` to be a callback (which, by definition, won't run until some time later), by the time // the function runs (causing `f` to run, causing `x.y` to run), `x.y` will point to the callback itself, rather // than its original value. So calling it will call the callback which will call `f` which will call `x.y` which // will call the callback which will call `f` which will call `x.y`... and on and on. Theoretically this could also // be fixed by using `bind`, but this is way simpler.) const origEntryProperty = newConfig.entry; newConfig.entry = async () => addSentryToEntryProperty(origEntryProperty, buildContext, userSentryOptions); // Enable the Sentry plugin (which uploads source maps to Sentry when not in dev) by default if (shouldEnableWebpackPlugin(buildContext, userSentryOptions)) { // TODO Handle possibility that user is using `SourceMapDevToolPlugin` (see // https://webpack.js.org/plugins/source-map-dev-tool-plugin/) // TODO (v9 or v10, maybe): Remove this handleSourcemapHidingOptionWarning(userSentryOptions, isServer); // Next doesn't let you change `devtool` in dev even if you want to, so don't bother trying - see // https://github.com/vercel/next.js/blob/master/errors/improper-devtool.md if (!isDev) { // TODO (v8): Default `hideSourceMaps` to `true` // `hidden-source-map` produces the same sourcemaps as `source-map`, but doesn't include the `sourceMappingURL` // comment at the bottom. For folks who aren't publicly hosting their sourcemaps, this is helpful because then // the browser won't look for them and throw errors into the console when it can't find them. Because this is a // front-end-only problem, and because `sentry-cli` handles sourcemaps more reliably with the comment than // without, the option to use `hidden-source-map` only applies to the client-side build. newConfig.devtool = userSentryOptions.hideSourceMaps && !isServer ? 'hidden-source-map' : 'source-map'; const SentryWebpackPlugin = utils.loadModule('@sentry/webpack-plugin'); if (SentryWebpackPlugin) { newConfig.plugins = newConfig.plugins || []; newConfig.plugins.push(new SentryCliDownloadPlugin()); newConfig.plugins.push( // @ts-expect-error - this exists, the dynamic import just doesn't know about it new SentryWebpackPlugin( getWebpackPluginOptions(buildContext, userSentryWebpackPluginOptions, userSentryOptions), ), ); } } } if (userSentryOptions.disableLogger) { newConfig.plugins = newConfig.plugins || []; newConfig.plugins.push( new buildContext.webpack.DefinePlugin({ __SENTRY_DEBUG__: false, }), ); } return newConfig; }; } /** * Determine if this `module.rules` entry is one which will transpile user code * * @param rule The rule to check * @param projectDir The path to the user's project directory * @returns True if the rule transpiles user code, and false otherwise */ function isMatchingRule(rule, projectDir) { // We want to run our SDK code through the same transformations the user's code will go through, so we test against a // sample user code path const samplePagePath = path.resolve(projectDir, 'pageFile.js'); if (rule.test && rule.test instanceof RegExp && !rule.test.test(samplePagePath)) { return false; } if (Array.isArray(rule.include) && !rule.include.includes(projectDir)) { return false; } // `rule.use` can be an object or an array of objects. For simplicity, force it to be an array. const useEntries = utils.arrayify(rule.use); // Depending on the version of nextjs we're talking about, the loader which does the transpiling is either // // 'next-babel-loader' (next 10), // '/abs/path/to/node_modules/next/more/path/babel/even/more/path/loader/yet/more/path/index.js' (next 11), or // 'next-swc-loader' (next 12). // // The next 11 option is ugly, but thankfully 'next', 'babel', and 'loader' do appear in it in the same order as in // 'next-babel-loader', so we can use the same regex to test for both. if (!useEntries.some(entry => _optionalChain([entry, 'optionalAccess', _6 => _6.loader]) && /next.*(babel|swc).*loader/.test(entry.loader))) { return false; } return true; } /** * Find all rules in `module.rules` which transpile user code. * * @param rules The `module.rules` value * @param projectDir The path to the user's project directory * @returns An array of matching rules */ function findTranspilationRules(rules, projectDir) { if (!rules) { return []; } const matchingRules = []; // Each entry in `module.rules` is either a rule in and of itself or an object with a `oneOf` property, whose value is // an array of rules rules.forEach(rule => { // if (rule.oneOf) { if (isMatchingRule(rule, projectDir)) { matchingRules.push(rule); } else if (rule.oneOf) { const matchingOneOfRules = rule.oneOf.filter(oneOfRule => isMatchingRule(oneOfRule, projectDir)); matchingRules.push(...matchingOneOfRules); // } else if (isMatchingRule(rule, projectDir)) { } }); return matchingRules; } /** * Modify the webpack `entry` property so that the code in `sentry.server.config.js` and `sentry.client.config.js` is * included in the the necessary bundles. * * @param currentEntryProperty The value of the property before Sentry code has been injected * @param buildContext Object passed by nextjs containing metadata about the build * @returns The value which the new `entry` property (which will be a function) will return (TODO: this should return * the function, rather than the function's return value) */ async function addSentryToEntryProperty( currentEntryProperty, buildContext, userSentryOptions, ) { // The `entry` entry in a webpack config can be a string, array of strings, object, or function. By default, nextjs // sets it to an async function which returns the promise of an object of string arrays. Because we don't know whether // someone else has come along before us and changed that, we need to check a few things along the way. The one thing // we know is that it won't have gotten *simpler* in form, so we only need to worry about the object and function // options. See https://webpack.js.org/configuration/entry-context/#entry. const { isServer, dir: projectDir, nextRuntime, dev: isDevMode } = buildContext; const runtime = isServer ? (buildContext.nextRuntime === 'edge' ? 'edge' : 'node') : 'browser'; const newEntryProperty = typeof currentEntryProperty === 'function' ? await currentEntryProperty() : { ...currentEntryProperty }; // `sentry.server.config.js` or `sentry.client.config.js` (or their TS equivalents) const userConfigFile = nextRuntime === 'edge' ? getUserConfigFile(projectDir, 'edge') : isServer ? getUserConfigFile(projectDir, 'server') : getUserConfigFile(projectDir, 'client'); // we need to turn the filename into a path so webpack can find it const filesToInject = userConfigFile ? [`./${userConfigFile}`] : []; // inject into all entry points which might contain user's code for (const entryPointName in newEntryProperty) { if (shouldAddSentryToEntryPoint(entryPointName, runtime)) { addFilesToExistingEntryPoint(newEntryProperty, entryPointName, filesToInject, isDevMode); } else { if ( isServer && // If the user has asked to exclude pages, confirm for them that it's worked userSentryOptions.excludeServerRoutes && // We always skip these, so it's not worth telling the user that we've done so !['pages/_app', 'pages/_document'].includes(entryPointName) ) { debugBuild.DEBUG_BUILD && utils.logger.log(`Skipping Sentry injection for ${entryPointName.replace(/^pages/, '')}`); } } } return newEntryProperty; } /** * Search the project directory for a valid user config file for the given platform, allowing for it to be either a * TypeScript or JavaScript file. * * @param projectDir The root directory of the project, where the file should be located * @param platform Either "server", "client" or "edge", so that we know which file to look for * @returns The name of the relevant file. If the server or client file is not found, this method throws an error. The * edge file is optional, if it is not found this function will return `undefined`. */ function getUserConfigFile(projectDir, platform) { const possibilities = [`sentry.${platform}.config.ts`, `sentry.${platform}.config.js`]; for (const filename of possibilities) { if (fs.existsSync(path.resolve(projectDir, filename))) { return filename; } } // Edge config file is optional if (platform === 'edge') { // eslint-disable-next-line no-console console.warn( '[@sentry/nextjs] You are using Next.js features that run on the Edge Runtime. Please add a "sentry.edge.config.js" or a "sentry.edge.config.ts" file to your project root in which you initialize the Sentry SDK with "Sentry.init()".', ); return; } else { throw new Error(`Cannot find '${possibilities[0]}' or '${possibilities[1]}' in '${projectDir}'.`); } } /** * Gets the absolute path to a sentry config file for a particular platform. Returns `undefined` if it doesn't exist. */ function getUserConfigFilePath(projectDir, platform) { const possibilities = [`sentry.${platform}.config.ts`, `sentry.${platform}.config.js`]; for (const filename of possibilities) { const configPath = path.resolve(projectDir, filename); if (fs.existsSync(configPath)) { return configPath; } } return undefined; } /** * Add files to a specific element of the given `entry` webpack config property. * * @param entryProperty The existing `entry` config object * @param entryPointName The key where the file should be injected * @param filesToInsert An array of paths to the injected files */ function addFilesToExistingEntryPoint( entryProperty, entryPointName, filesToInsert, isDevMode, ) { // BIG FAT NOTE: Order of insertion seems to matter here. If we insert the new files before the `currentEntrypoint`s, // the Next.js dev server breaks. Because we generally still want the SDK to be initialized as early as possible we // still keep it at the start of the entrypoints if we are not in dev mode. // can be a string, array of strings, or object whose `import` property is one of those two const currentEntryPoint = entryProperty[entryPointName]; let newEntryPoint = currentEntryPoint; if (typeof currentEntryPoint === 'string' || Array.isArray(currentEntryPoint)) { newEntryPoint = utils.arrayify(currentEntryPoint); if (newEntryPoint.some(entry => filesToInsert.includes(entry))) { return; } if (isDevMode) { // Inserting at beginning breaks dev mode so we insert at the end newEntryPoint.push(...filesToInsert); } else { // In other modes we insert at the beginning so that the SDK initializes as early as possible newEntryPoint.unshift(...filesToInsert); } } // descriptor object (webpack 5+) else if (typeof currentEntryPoint === 'object' && 'import' in currentEntryPoint) { const currentImportValue = currentEntryPoint.import; const newImportValue = utils.arrayify(currentImportValue); if (newImportValue.some(entry => filesToInsert.includes(entry))) { return; } if (isDevMode) { // Inserting at beginning breaks dev mode so we insert at the end newImportValue.push(...filesToInsert); } else { // In other modes we insert at the beginning so that the SDK initializes as early as possible newImportValue.unshift(...filesToInsert); } newEntryPoint = { ...currentEntryPoint, import: newImportValue, }; } // malformed entry point (use `console.error` rather than `logger.error` because it will always be printed, regardless // of SDK settings) else { // eslint-disable-next-line no-console console.error( 'Sentry Logger [Error]:', `Could not inject SDK initialization code into entry point ${entryPointName}, as its current value is not in a recognized format.\n`, 'Expected: string | Array | { [key:string]: any, import: string | Array }\n', `Got: ${currentEntryPoint}`, ); } entryProperty[entryPointName] = newEntryPoint; } /** * Check the SentryWebpackPlugin options provided by the user against the options we set by default, and warn if any of * our default options are getting overridden. (Note: If any of our default values is undefined, it won't be included in * the warning.) * * @param defaultOptions Default SentryWebpackPlugin options * @param userOptions The user's SentryWebpackPlugin options */ function checkWebpackPluginOverrides( defaultOptions, userOptions, ) { // warn if any of the default options for the webpack plugin are getting overridden const sentryWebpackPluginOptionOverrides = Object.keys(defaultOptions).filter(key => key in userOptions); if (sentryWebpackPluginOptionOverrides.length > 0) { debugBuild.DEBUG_BUILD && utils.logger.warn( '[Sentry] You are overriding the following automatically-set SentryWebpackPlugin config options:\n' + `\t${sentryWebpackPluginOptionOverrides.toString()},\n` + "which has the possibility of breaking source map upload and application. This is only a good idea if you know what you're doing.", ); } } /** * Determine if this is an entry point into which both `Sentry.init()` code and the release value should be injected * * @param entryPointName The name of the entry point in question * @param isServer Whether or not this function is being called in the context of a server build * @param excludeServerRoutes A list of excluded serverside entrypoints provided by the user * @returns `true` if sentry code should be injected, and `false` otherwise */ function shouldAddSentryToEntryPoint(entryPointName, runtime) { return ( runtime === 'browser' && (entryPointName === 'pages/_app' || // entrypoint for `/app` pages entryPointName === 'main-app') ); } /** * Combine default and user-provided SentryWebpackPlugin options, accounting for whether we're building server files or * client files. * * @param buildContext Nexjs-provided data about the current build * @param userPluginOptions User-provided SentryWebpackPlugin options * @returns Final set of combined options */ function getWebpackPluginOptions( buildContext, userPluginOptions, userSentryOptions, ) { const { buildId, isServer, config, dir: projectDir } = buildContext; const userNextConfig = config ; const distDirAbsPath = path.resolve(projectDir, userNextConfig.distDir || '.next'); // `.next` is the default directory const isServerless = userNextConfig.target === 'experimental-serverless-trace'; const hasSentryProperties = fs.existsSync(path.resolve(projectDir, 'sentry.properties')); const urlPrefix = '~/_next'; const serverInclude = isServerless ? [{ paths: [`${distDirAbsPath}/serverless/`], urlPrefix: `${urlPrefix}/serverless` }] : [{ paths: [`${distDirAbsPath}/server/`], urlPrefix: `${urlPrefix}/server` }]; const serverIgnore = []; const clientInclude = userSentryOptions.widenClientFileUpload ? [{ paths: [`${distDirAbsPath}/static/chunks`], urlPrefix: `${urlPrefix}/static/chunks` }] : [ { paths: [`${distDirAbsPath}/static/chunks/pages`], urlPrefix: `${urlPrefix}/static/chunks/pages` }, { paths: [`${distDirAbsPath}/static/chunks/app`], urlPrefix: `${urlPrefix}/static/chunks/app` }, ]; // Widening the upload scope is necessarily going to lead to us uploading files we don't need to (ones which // don't include any user code). In order to lessen that where we can, exclude the internal nextjs files we know // will be there. const clientIgnore = userSentryOptions.widenClientFileUpload ? ['framework-*', 'framework.*', 'main-*', 'polyfills-*', 'webpack-*'] : []; const defaultPluginOptions = utils.dropUndefinedKeys({ include: isServer ? serverInclude : clientInclude, ignore: isServer ? serverIgnore : clientIgnore, url: process.env.SENTRY_URL, org: process.env.SENTRY_ORG, project: process.env.SENTRY_PROJECT, authToken: process.env.SENTRY_AUTH_TOKEN, configFile: hasSentryProperties ? 'sentry.properties' : undefined, stripPrefix: ['webpack://_N_E/', 'webpack://'], urlPrefix, entries: [], // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead. release: node.getSentryRelease(buildId), }); checkWebpackPluginOverrides(defaultPluginOptions, userPluginOptions); return { ...defaultPluginOptions, ...userPluginOptions, errorHandler(err, invokeErr, compilation) { if (err) { const errorMessagePrefix = `${chalk.red('error')} -`; if (err.message.includes('ENOENT')) { if (!showedMissingCliBinaryWarningMsg) { // eslint-disable-next-line no-console console.error( `\n${errorMessagePrefix} ${chalk.bold( 'The Sentry binary to upload sourcemaps could not be found.', )} Source maps will not be uploaded. Please check that post-install scripts are enabled in your package manager when installing your dependencies and please run your build once without any caching to avoid caching issues of dependencies.\n`, ); showedMissingCliBinaryWarningMsg = true; } return; } // Hardcoded way to check for missing auth token until we have a better way of doing this. if (err.message.includes('Authentication credentials were not provided.')) { let msg; if (process.env.VERCEL) { msg = `To fix this, use Sentry's Vercel integration to automatically set the ${chalk.bold.cyan( 'SENTRY_AUTH_TOKEN', )} environment variable: https://vercel.com/integrations/sentry`; } else { msg = 'You can find information on how to generate a Sentry auth token here: https://docs.sentry.io/api/auth/\n' + `After generating a Sentry auth token, set it via the ${chalk.bold.cyan( 'SENTRY_AUTH_TOKEN', )} environment variable during the build.`; } if (!showedMissingAuthTokenErrorMsg) { // eslint-disable-next-line no-console console.error( `${errorMessagePrefix} ${chalk.bold( 'No Sentry auth token configured.', )} Source maps will not be uploaded.\n${msg}\n`, ); showedMissingAuthTokenErrorMsg = true; } return; } // Hardcoded way to check for missing org slug until we have a better way of doing this. if (err.message.includes('An organization slug is required')) { let msg; if (process.env.VERCEL) { msg = `To fix this, use Sentry's Vercel integration to automatically set the ${chalk.bold.cyan( 'SENTRY_ORG', )} environment variable: https://vercel.com/integrations/sentry`; } else { msg = `To fix this, set the ${chalk.bold.cyan( 'SENTRY_ORG', )} environment variable to the to your organization slug during the build.`; } if (!showedMissingOrgSlugErrorMsg) { // eslint-disable-next-line no-console console.error( `${errorMessagePrefix} ${chalk.bold( 'No Sentry organization slug configured.', )} Source maps will not be uploaded.\n${msg}\n`, ); showedMissingOrgSlugErrorMsg = true; } return; } // Hardcoded way to check for missing project slug until we have a better way of doing this. if (err.message.includes('A project slug is required')) { let msg; if (process.env.VERCEL) { msg = `To fix this, use Sentry's Vercel integration to automatically set the ${chalk.bold.cyan( 'SENTRY_PROJECT', )} environment variable: https://vercel.com/integrations/sentry`; } else { msg = `To fix this, set the ${chalk.bold.cyan( 'SENTRY_PROJECT', )} environment variable to the name of your Sentry project during the build.`; } if (!showedMissingProjectSlugErrorMsg) { // eslint-disable-next-line no-console console.error( `${errorMessagePrefix} ${chalk.bold( 'No Sentry project slug configured.', )} Source maps will not be uploaded.\n${msg}\n`, ); showedMissingProjectSlugErrorMsg = true; } return; } } if (userPluginOptions.errorHandler) { return userPluginOptions.errorHandler(err, invokeErr, compilation); } return invokeErr(); }, }; } /** Check various conditions to decide if we should run the plugin */ function shouldEnableWebpackPlugin(buildContext, userSentryOptions) { const { isServer } = buildContext; const { disableServerWebpackPlugin, disableClientWebpackPlugin } = userSentryOptions; if (isServer && disableServerWebpackPlugin !== undefined) { return !disableServerWebpackPlugin; } else if (!isServer && disableClientWebpackPlugin !== undefined) { return !disableClientWebpackPlugin; } return true; } /** Handle warning messages about `hideSourceMaps` option. Can be removed in v9 or v10 (or whenever we consider that * enough people will have upgraded the SDK that the warning about the default in v8 - currently commented out - is * overkill). */ function handleSourcemapHidingOptionWarning(userSentryOptions, isServer) { // This is nextjs's own logging formatting, vendored since it's not exported. See // https://github.com/vercel/next.js/blob/c3ceeb03abb1b262032bd96457e224497d3bbcef/packages/next/build/output/log.ts#L3-L11 // and // https://github.com/vercel/next.js/blob/de7aa2d6e486c40b8be95a1327639cbed75a8782/packages/next/lib/eslint/runLintCheck.ts#L321-L323. const codeFormat = (str) => chalk.bold.cyan(str); const _warningPrefix_ = `${chalk.yellow('warn')} -`; const _sentryNextjs_ = codeFormat('@sentry/nextjs'); const _hideSourceMaps_ = codeFormat('hideSourceMaps'); const _true_ = codeFormat('true'); const _false_ = codeFormat('false'); const _sentry_ = codeFormat('sentry'); const _nextConfigJS_ = codeFormat('next.config.js'); if (isServer && userSentryOptions.hideSourceMaps === undefined && !showedHiddenSourceMapsWarningMsg) { // eslint-disable-next-line no-console console.warn( `\n${_warningPrefix_} In order to be able to deminify errors, ${_sentryNextjs_} creates sourcemaps and uploads ` + 'them to the Sentry server. Depending on your deployment setup, this means your original code may be visible ' + `in browser devtools in production. To prevent this, set ${_hideSourceMaps_} to ${_true_} in the ${_sentry_} ` + `options in your ${_nextConfigJS_}. To disable this warning without changing sourcemap behavior, set ` + `${_hideSourceMaps_} to ${_false_}. (In ${_sentryNextjs_} version 8.0.0 and beyond, this option will default ` + `to ${_true_}.) See https://webpack.js.org/configuration/devtool/ and ` + 'https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-hidden-source-map for more ' + 'information.\n', ); showedHiddenSourceMapsWarningMsg = true; } // TODO (v8): Remove the check above in favor of the one below // const infoPrefix = `${chalk.cyan('info')} -`; // // if (isServer && userSentryOptions.hideSourceMaps === true) { // // eslint-disable-next-line no-console // console.log( // `\n${infoPrefix} Starting in ${_sentryNextjs_} version 8.0.0, ${_hideSourceMaps_} defaults to ${_true_}, and ` + // `thus can be removed from the ${_sentry_} options in ${_nextConfigJS_}. See ` + // 'https://webpack.js.org/configuration/devtool/ and ' + // 'https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-hidden-source-map for more ' + // 'information.\n', // ); // } } /** * Ensure that `newConfig.module.rules` exists. Modifies the given config in place but also returns it in order to * change its type. * * @param newConfig A webpack config object which may or may not contain `module` and `module.rules` * @returns The same object, with an empty `module.rules` array added if necessary */ function setUpModuleRules(newConfig) { newConfig.module = { ...newConfig.module, rules: [...(_optionalChain([newConfig, 'access', _7 => _7.module, 'optionalAccess', _8 => _8.rules]) || [])], }; // Surprising that we have to assert the type here, since we've demonstrably guaranteed the existence of // `newConfig.module.rules` just above, but ¯\_(ツ)_/¯ return newConfig ; } /** * Adds loaders to inject values on the global object based on user configuration. */ function addValueInjectionLoader( newConfig, userNextConfig, userSentryOptions, buildContext, sentryWebpackPluginOptions, ) { const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || ''; const isomorphicValues = { // `rewritesTunnel` set by the user in Next.js config __sentryRewritesTunnelPath__: userSentryOptions.tunnelRoute !== undefined && userNextConfig.output !== 'export' ? `${_nullishCoalesce(userNextConfig.basePath, () => ( ''))}${userSentryOptions.tunnelRoute}` : undefined, // The webpack plugin's release injection breaks the `app` directory so we inject the release manually here instead. // Having a release defined in dev-mode spams releases in Sentry so we only set one in non-dev mode SENTRY_RELEASE: buildContext.dev ? undefined : { id: _nullishCoalesce(sentryWebpackPluginOptions.release, () => ( node.getSentryRelease(buildContext.buildId))) }, __sentryBasePath: buildContext.dev ? userNextConfig.basePath : undefined, }; const serverValues = { ...isomorphicValues, // Make sure that if we have a windows path, the backslashes are interpreted as such (rather than as escape // characters) __rewriteFramesDistDir__: _optionalChain([userNextConfig, 'access', _9 => _9.distDir, 'optionalAccess', _10 => _10.replace, 'call', _11 => _11(/\\/g, '\\\\')]) || '.next', }; const clientValues = { ...isomorphicValues, // Get the path part of `assetPrefix`, minus any trailing slash. (We use a placeholder for the origin if // `assetPreix` doesn't include one. Since we only care about the path, it doesn't matter what it is.) __rewriteFramesAssetPrefixPath__: assetPrefix ? new URL(assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '') : '', }; newConfig.module.rules.push( { test: /sentry\.(server|edge)\.config\.(jsx?|tsx?)/, use: [ { loader: path.resolve(__dirname, 'loaders/valueInjectionLoader.js'), options: { values: serverValues, }, }, ], }, { test: /sentry\.client\.config\.(jsx?|tsx?)/, use: [ { loader: path.resolve(__dirname, 'loaders/valueInjectionLoader.js'), options: { values: clientValues, }, }, ], }, ); } function resolveNextPackageDirFromDirectory(basedir) { try { return path.dirname(resolve.sync('next/package.json', { basedir })); } catch (e2) { // Should not happen in theory return undefined; } } const POTENTIAL_REQUEST_ASNYC_STORAGE_LOCATIONS = [ // Original location of RequestAsyncStorage // https://github.com/vercel/next.js/blob/46151dd68b417e7850146d00354f89930d10b43b/packages/next/src/client/components/request-async-storage.ts 'next/dist/client/components/request-async-storage.js', // Introduced in Next.js 13.4.20 // https://github.com/vercel/next.js/blob/e1bc270830f2fc2df3542d4ef4c61b916c802df3/packages/next/src/client/components/request-async-storage.external.ts 'next/dist/client/components/request-async-storage.external.js', ]; function getRequestAsyncStorageModuleLocation( webpackContextDir, webpackResolvableModuleLocations, ) { if (webpackResolvableModuleLocations === undefined) { return undefined; } const absoluteWebpackResolvableModuleLocations = webpackResolvableModuleLocations.map(loc => path.resolve(webpackContextDir, loc), ); for (const webpackResolvableLocation of absoluteWebpackResolvableModuleLocations) { const nextPackageDir = resolveNextPackageDirFromDirectory(webpackResolvableLocation); if (nextPackageDir) { const asyncLocalStorageLocation = POTENTIAL_REQUEST_ASNYC_STORAGE_LOCATIONS.find(loc => fs.existsSync(path.join(nextPackageDir, '..', loc)), ); if (asyncLocalStorageLocation) { return asyncLocalStorageLocation; } } } return undefined; } let downloadingCliAttempted = false; class SentryCliDownloadPlugin { apply(compiler) { compiler.hooks.beforeRun.tapAsync('SentryCliDownloadPlugin', (compiler, callback) => { const SentryWebpackPlugin = utils.loadModule('@sentry/webpack-plugin'); if (!SentryWebpackPlugin) { // Pretty much an invariant. return callback(); } // @ts-expect-error - this exists, the dynamic import just doesn't know it if (SentryWebpackPlugin.cliBinaryExists()) { return callback(); } if (!downloadingCliAttempted) { downloadingCliAttempted = true; // eslint-disable-next-line no-console utils.logger.info( `\n${chalk.cyan('info')} - ${chalk.bold( 'Sentry binary to upload source maps not found.', )} Package manager post-install scripts are likely disabled or there is a caching issue. Manually downloading instead...`, ); // @ts-expect-error - this exists, the dynamic import just doesn't know it const cliDownloadPromise = SentryWebpackPlugin.downloadCliBinary({ log: () => { // No logs from directly from CLI }, }); cliDownloadPromise.then( () => { // eslint-disable-next-line no-console utils.logger.info(`${chalk.cyan('info')} - Sentry binary was successfully downloaded.\n`); return callback(); }, e => { // eslint-disable-next-line no-console utils.logger.error(`${chalk.red('error')} - Sentry binary download failed:`, e); return callback(); }, ); } else { return callback(); } }); } } exports.constructWebpackConfigFunction = constructWebpackConfigFunction; exports.getUserConfigFile = getUserConfigFile; exports.getUserConfigFilePath = getUserConfigFilePath; exports.getWebpackPluginOptions = getWebpackPluginOptions; //# sourceMappingURL=webpack.js.map