123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119 |
- 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<string> | { [key:string]: any, import: string | Array<string> }\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
|