/* eslint-env node */ /* eslint import/no-nodejs-modules:0 */ import fs from 'fs'; import path from 'path'; import CompressionPlugin from 'compression-webpack-plugin'; import CopyPlugin from 'copy-webpack-plugin'; import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import webpack from 'webpack'; import {Configuration as DevServerConfig} from 'webpack-dev-server'; import FixStyleOnlyEntriesPlugin from 'webpack-remove-empty-scripts'; const {StatsWriterPlugin} = require('webpack-stats-plugin'); import IntegrationDocsFetchPlugin from './build-utils/integration-docs-fetch-plugin'; import LastBuiltPlugin from './build-utils/last-built-plugin'; import SentryInstrumentation from './build-utils/sentry-instrumentation'; import {extractIOSDeviceNames} from './scripts/extract-ios-device-names'; import babelConfig from './babel.config'; // Runs as part of prebuild step to generate a list of identifier -> name mappings for iOS (async () => { await extractIOSDeviceNames(); })(); /** * Merges the devServer config into the webpack config * * See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43232 */ interface Configuration extends webpack.Configuration { devServer?: DevServerConfig; } const {env} = process; // Environment configuration env.NODE_ENV = env.NODE_ENV ?? 'development'; const IS_PRODUCTION = env.NODE_ENV === 'production'; const IS_TEST = env.NODE_ENV === 'test' || !!env.TEST_SUITE; // This is used to stop rendering dynamic content for tests/snapshots // We want it in the case where we are running tests and it is in CI, // this should not happen in local const IS_CI = !!env.CI; // We intentionally build in production mode for acceptance tests, so we explicitly use an env var to // say that the bundle will be used in acceptance tests. This affects webpack plugins and components // with dynamic data that render differently statically in tests. // // Note, cannot assume it is an acceptance test if `IS_CI` is true, as our image builds has the // `CI` env var set. const IS_ACCEPTANCE_TEST = !!env.IS_ACCEPTANCE_TEST; const IS_DEPLOY_PREVIEW = !!env.NOW_GITHUB_DEPLOYMENT; const IS_UI_DEV_ONLY = !!env.SENTRY_UI_DEV_ONLY; const DEV_MODE = !(IS_PRODUCTION || IS_CI); const WEBPACK_MODE: Configuration['mode'] = IS_PRODUCTION ? 'production' : 'development'; const CONTROL_SILO_PORT = env.SENTRY_CONTROL_SILO_PORT; // Environment variables that are used by other tooling and should // not be user configurable. // // Ports used by webpack dev server to proxy to backend and webpack const SENTRY_BACKEND_PORT = env.SENTRY_BACKEND_PORT; const SENTRY_WEBPACK_PROXY_HOST = env.SENTRY_WEBPACK_PROXY_HOST; const SENTRY_WEBPACK_PROXY_PORT = env.SENTRY_WEBPACK_PROXY_PORT; const SENTRY_RELEASE_VERSION = env.SENTRY_RELEASE_VERSION; // Used by sentry devserver runner to force using webpack-dev-server const FORCE_WEBPACK_DEV_SERVER = !!env.FORCE_WEBPACK_DEV_SERVER; const HAS_WEBPACK_DEV_SERVER_CONFIG = !!SENTRY_BACKEND_PORT && !!SENTRY_WEBPACK_PROXY_PORT; // User/tooling configurable environment variables const NO_DEV_SERVER = !!env.NO_DEV_SERVER; // Do not run webpack dev server const SHOULD_FORK_TS = DEV_MODE && !env.NO_TS_FORK; // Do not run fork-ts plugin (or if not dev env) const SHOULD_HOT_MODULE_RELOAD = DEV_MODE && !!env.SENTRY_UI_HOT_RELOAD; const SHOULD_LAZY_LOAD = DEV_MODE && !!env.SENTRY_UI_LAZY_LOAD; // Deploy previews are built using vercel. We can check if we're in vercel's // build process by checking the existence of the PULL_REQUEST env var. const DEPLOY_PREVIEW_CONFIG = IS_DEPLOY_PREVIEW && { branch: env.NOW_GITHUB_COMMIT_REF, commitSha: env.NOW_GITHUB_COMMIT_SHA, githubOrg: env.NOW_GITHUB_COMMIT_ORG, githubRepo: env.NOW_GITHUB_COMMIT_REPO, }; // When deploy previews are enabled always enable experimental SPA mode -- // deploy previews are served standalone. Otherwise fallback to the environment // configuration. const SENTRY_EXPERIMENTAL_SPA = !DEPLOY_PREVIEW_CONFIG && !IS_UI_DEV_ONLY ? !!env.SENTRY_EXPERIMENTAL_SPA : true; // We should only read from the SENTRY_SPA_DSN env variable if SENTRY_EXPERIMENTAL_SPA // is true. This is to make sure we can validate that the experimental SPA mode is // working properly. const SENTRY_SPA_DSN = SENTRY_EXPERIMENTAL_SPA ? env.SENTRY_SPA_DSN : undefined; // this is the path to the django "sentry" app, we output the webpack build here to `dist` // so that `django collectstatic` and so that we can serve the post-webpack bundles const sentryDjangoAppPath = path.join(__dirname, 'src/sentry/static/sentry'); const distPath = env.SENTRY_STATIC_DIST_PATH || path.join(sentryDjangoAppPath, 'dist'); const staticPrefix = path.join(__dirname, 'static'); // Locale file extraction build step if (env.SENTRY_EXTRACT_TRANSLATIONS === '1') { babelConfig.plugins?.push([ 'module:babel-gettext-extractor', { fileName: 'build/javascript.po', baseDirectory: path.join(__dirname), functionNames: { gettext: ['msgid'], ngettext: ['msgid', 'msgid_plural', 'count'], gettextComponentTemplate: ['msgid'], t: ['msgid'], tn: ['msgid', 'msgid_plural', 'count'], tct: ['msgid'], }, }, ]); } // Locale compilation and optimizations. // // Locales are code-split from the app and vendor chunk into separate chunks // that will be loaded by layout.html depending on the users configured locale. // // Code splitting happens using the splitChunks plugin, configured under the // `optimization` key of the webpack module. We create chunk (cache) groups for // each of our supported locales and extract the PO files and moment.js locale // files into each chunk. // // A plugin is used to remove the locale chunks from the app entry's chunk // dependency list, so that our compiled bundle does not expect that *all* // locale chunks must be loaded const localeCatalogPath = path.join( __dirname, 'src', 'sentry', 'locale', 'catalogs.json' ); type LocaleCatalog = { supported_locales: string[]; }; const localeCatalog: LocaleCatalog = JSON.parse( fs.readFileSync(localeCatalogPath, 'utf8') ); // Translates a locale name to a language code. // // * po files are kept in a directory represented by the locale name [0] // * moment.js locales are stored as language code files // // [0] https://docs.djangoproject.com/en/2.1/topics/i18n/#term-locale-name const localeToLanguage = (locale: string) => locale.toLowerCase().replace('_', '-'); const supportedLocales = localeCatalog.supported_locales; const supportedLanguages = supportedLocales.map(localeToLanguage); type CacheGroups = Exclude< NonNullable['splitChunks'], false | undefined >['cacheGroups']; type CacheGroupTest = ( module: webpack.Module, context: Parameters[1] ) => boolean; // A mapping of chunk groups used for locale code splitting const localeChunkGroups: CacheGroups = {}; supportedLocales // No need to split the english locale out as it will be completely empty and // is not included in the django layout.html. .filter(l => l !== 'en') .forEach(locale => { const language = localeToLanguage(locale); const group = `locale/${language}`; // List of module path tests to group into locale chunks const localeGroupTests = [ new RegExp(`locale\\/${locale}\\/.*\\.po$`), new RegExp(`moment\\/locale\\/${language}\\.js$`), ]; // module test taken from [0] and modified to support testing against // multiple expressions. // // [0] https://github.com/webpack/webpack/blob/7a6a71f1e9349f86833de12a673805621f0fc6f6/lib/optimize/SplitChunksPlugin.js#L309-L320 const groupTest: CacheGroupTest = (module, {chunkGraph}) => localeGroupTests.some(pattern => pattern.test(module?.nameForCondition?.() ?? '') ? true : chunkGraph.getModuleChunks(module).some(c => c.name && pattern.test(c.name)) ); // We are defining a chunk that combines the django language files with // moment's locales as if you want one, you will want the other. // // In the application code you will still need to import via their module // paths and not the chunk name localeChunkGroups[group] = { chunks: 'async', name: group, test: groupTest, enforce: true, }; }); const babelOptions = {...babelConfig, cacheDirectory: true}; const babelLoaderConfig = { loader: 'babel-loader', options: babelOptions, }; /** * Main Webpack config for Sentry React SPA. */ const appConfig: Configuration = { mode: WEBPACK_MODE, entry: { /** * Main Sentry SPA * * The order here matters for `getsentry` */ app: ['sentry/utils/statics-setup', 'sentry'], /** * Pipeline View for integrations */ pipeline: ['sentry/utils/statics-setup', 'sentry/views/integrationPipeline'], /** * Legacy CSS Webpack appConfig for Django-powered views. * This generates a single "sentry.css" file that imports ALL component styles * for use on Django-powered pages. */ sentry: 'less/sentry.less', }, context: staticPrefix, module: { /** * XXX: Modifying the order/contents of these rules may break `getsentry` * Please remember to test it. */ rules: [ { test: /\.[tj]sx?$/, include: [staticPrefix], exclude: /(vendor|node_modules|dist)/, use: babelLoaderConfig, }, { test: /\.po$/, use: { loader: 'po-catalog-loader', options: { referenceExtensions: ['.js', '.jsx', '.tsx'], domain: 'sentry', }, }, }, { test: /\.pegjs/, use: {loader: 'pegjs-loader'}, }, { test: /\.css/, use: [ 'style-loader', { loader: 'css-loader', options: { importLoaders: 1, modules: true, }, }, ], }, { test: /\.less$/, include: [staticPrefix], use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: 'auto', }, }, 'css-loader', 'less-loader', ], }, { test: /\.(woff|woff2|ttf|eot|svg|png|gif|ico|jpg|mp4)($|\?)/, type: 'asset', }, ], noParse: [ // don't parse known, pre-built javascript files (improves webpack perf) /jed\/jed\.js/, /marked\/lib\/marked\.js/, /terser\/dist\/bundle\.min\.js/, ], }, plugins: [ /** * Adds build time measurement instrumentation, which will be reported back * to sentry */ new SentryInstrumentation(), // Do not bundle moment's locale files as we will lazy load them using // dynamic imports in the application code new webpack.IgnorePlugin({ contextRegExp: /moment$/, resourceRegExp: /^\.\/locale$/, }), /** * TODO(epurkhiser): Figure out if we still need these */ new webpack.ProvidePlugin({ process: 'process/browser', Buffer: ['buffer', 'Buffer'], }), /** * Extract CSS into separate files. */ new MiniCssExtractPlugin({ // We want the sentry css file to be unversioned for frontend-only deploys // We will cache using `Cache-Control` headers filename: 'entrypoints/[name].css', }), /** * Defines environment specific flags. */ new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify(env.NODE_ENV), IS_ACCEPTANCE_TEST: JSON.stringify(IS_ACCEPTANCE_TEST), DEPLOY_PREVIEW_CONFIG: JSON.stringify(DEPLOY_PREVIEW_CONFIG), EXPERIMENTAL_SPA: JSON.stringify(SENTRY_EXPERIMENTAL_SPA), SPA_DSN: JSON.stringify(SENTRY_SPA_DSN), SENTRY_RELEASE_VERSION: JSON.stringify(SENTRY_RELEASE_VERSION), }, }), /** * This removes empty js files for style only entries (e.g. sentry.less) */ new FixStyleOnlyEntriesPlugin({verbose: false}), ...(SHOULD_FORK_TS ? [ new ForkTsCheckerWebpackPlugin({ typescript: { configFile: path.resolve(__dirname, './config/tsconfig.build.json'), configOverwrite: { compilerOptions: {incremental: true}, }, }, devServer: false, // memorylimit is configured in package.json }), ] : []), /** * Restrict translation files that are pulled in through app/translations.jsx * and through moment/locale/* to only those which we create bundles for via * locale/catalogs.json. * * Without this, webpack will still output all of the unused locale files despite * the application never loading any of them. */ new webpack.ContextReplacementPlugin( /sentry-locale$/, path.join(__dirname, 'src', 'sentry', 'locale', path.sep), true, new RegExp(`(${supportedLocales.join('|')})/.*\\.po$`) ), new webpack.ContextReplacementPlugin( /moment\/locale/, new RegExp(`(${supportedLanguages.join('|')})\\.js$`) ), /** * Copies file logo-sentry.svg to the dist/entrypoints directory so that it can be accessed by * the backend */ new CopyPlugin({ patterns: [ { from: path.join(staticPrefix, 'images/logo-sentry.svg'), to: 'entrypoints/logo-sentry.svg', toType: 'file', }, // Add robots.txt when deploying in preview mode so public previews do // not get indexed by bots. ...(IS_DEPLOY_PREVIEW ? [ { from: path.join(staticPrefix, 'robots-dev.txt'), to: 'robots.txt', toType: 'file' as const, }, ] : []), ], }), // Generates stats to src/sentry/static/sentry/dist/stats.json new StatsWriterPlugin({ stats: { hash: true, assets: true, chunks: true, modules: true, env: true, entrypoints: true, }, transform(data: webpack.StatsCompilation) { data.namedChunkGroups; const finalData: Record = { version: data.version, hash: data.hash, entrypoints: data.entrypoints, env: data.env, assetsByChunkName: data.assetsByChunkName, }; // Based on https://github.com/relative-ci/bundle-stats/tree/master/packages/plugin-webpack-filter // # The MIT License // Copyright 2019 Viorel Cojocaru, contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the 'Software'), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies // of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following // conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. // { // "chunkNames": [], // The chunks this asset contains // "chunks": [10, 6], // The chunk IDs this asset contains // "comparedForEmit": false, // Indicates whether or not the asset was compared with the same file on the output file system // "emitted": true, // Indicates whether or not the asset made it to the `output` directory // "name": "10.web.js", // The `output` filename // "size": 1058, // The size of the file in bytes // "info": { // "immutable": true, // A flag telling whether the asset can be long term cached (contains a hash) // "size": 1058, // The size in bytes, only becomes available after asset has been emitted // "development": true, // A flag telling whether the asset is only used for development and doesn't count towards user-facing assets // "hotModuleReplacement": true, // A flag telling whether the asset ships data for updating an existing application (HMR) // "sourceFilename": "originalfile.js", // sourceFilename when asset was created from a source file (potentially transformed) // "javascriptModule": true // true, when asset is javascript and an ESM // } // } if (data.assets) { finalData.assets = data.assets.reduce((acc, asset) => { if (asset.name && !/.map$/.test(asset.name)) { acc.push({ name: asset.name, size: asset.size, info: asset.info, ...(asset.chunks && {chunks: asset.chunks}), }); } return acc; }, [] as Array>); } // { // "entry": true, // Indicates whether or not the chunk contains the webpack runtime // "files": [ // // An array of filename strings that contain this chunk // ], // "filteredModules": 0, // See the description in the [top-level structure](#structure) above // "id": 0, // The ID of this chunk // "initial": true, // Indicates whether this chunk is loaded on initial page load or [on demand](/guides/lazy-loading) // "modules": [ // // A list of [module objects](#module-objects) // "web.js?h=11593e3b3ac85436984a" // ], // "names": [ // // An list of chunk names contained within this chunk // ], // "origins": [ // { // "loc": "", // Lines of code that generated this chunk // "module": "(webpack)\\test\\browsertest\\lib\\index.web.js", // Path to the module // "moduleId": 0, // The ID of the module // "moduleIdentifier": "(webpack)\\test\\browsertest\\lib\\index.web.js", // Path to the module // "moduleName": "./lib/index.web.js", // Relative path to the module // "name": "main", // The name of the chunk // "reasons": [ // // A list of the same `reasons` found in [module objects](#module-objects) // ] // } // ], // "parents": [], // Parent chunk IDs // "rendered": true, // Indicates whether or not the chunk went through Code Generation // "size": 188057 // Chunk size in bytes // } if (data.chunks) { finalData.chunks = data.chunks.reduce((acc, chunk) => { if (!chunk.id) { return acc; } acc.push({ entry: chunk.entry, id: chunk.id, size: chunk.size, initial: chunk.initial, files: chunk.files, names: chunk.names, modules: chunk.modules, }); return acc; }, [] as Array>); } // { // "assets": [ // // A list of [asset objects](#asset-objects) // ], // "built": true, // Indicates that the module went through [Loaders](/concepts/loaders), Parsing, and Code Generation // "cacheable": true, // Whether or not this module is cacheable // "chunks": [ // // IDs of chunks that contain this module // ], // "errors": 0, // Number of errors when resolving or processing the module // "failed": false, // Whether or not compilation failed on this module // "id": 0, // The ID of the module (analogous to [`module.id`](/api/module-variables/#moduleid-commonjs)) // "identifier": "(webpack)\\test\\browsertest\\lib\\index.web.js", // A unique ID used internally // "name": "./lib/index.web.js", // Path to the actual file // "optional": false, // All requests to this module are with `try... catch` blocks (irrelevant with ESM) // "prefetched": false, // Indicates whether or not the module was [prefetched](/plugins/prefetch-plugin) // "profile": { // // Module specific compilation stats corresponding to the [`--profile` flag](/api/cli/#profiling) (in milliseconds) // "building": 73, // Loading and parsing // "dependencies": 242, // Building dependencies // "factory": 11 // Resolving dependencies // }, // "reasons": [ // { // "loc": "33:24-93", // Lines of code that caused the module to be included // "module": "./lib/index.web.js", // Relative path to the module based on [context](/configuration/entry-context/#context) // "moduleId": 0, // The ID of the module // "moduleIdentifier": "(webpack)\\test\\browsertest\\lib\\index.web.js", // Path to the module // "moduleName": "./lib/index.web.js", // A more readable name for the module (used for "pretty-printing") // "type": "require.context", // The [type of request](/api/module-methods) used // "userRequest": "../../cases" // Raw string used for the `import` or `require` request // } // ], // "size": 3593, // Estimated size of the module in bytes // "source": "// Should not break it...\r\nif(typeof...", // The stringified raw source // "warnings": 0 // Number of warnings when resolving or processing the module // } if (data.modules) { finalData.modules = data.modules.reduce((acc, module) => { if (!module.name) { return acc; } const seenReasonModule = new Set(); acc.push({ name: module.name, size: module.size, moduleType: module.moduleType, ...(module.chunks && {chunks: module.chunks}), ...(module.reasons && { reasons: module.reasons.reduce((reasonAcc, reason) => { if (seenReasonModule.has(reason.module)) { return reasonAcc; } reasonAcc.push({ module: reason.module, }); seenReasonModule.add(reason.module); return reasonAcc; }, [] as Array>), }), }); return acc; }, [] as Array>); } return JSON.stringify(finalData, null, 2); }, }), ], resolve: { alias: { 'react-dom$': 'react-dom/profiling', 'scheduler/tracing': 'scheduler/tracing-profiling', sentry: path.join(staticPrefix, 'app'), 'sentry-images': path.join(staticPrefix, 'images'), 'sentry-logos': path.join(sentryDjangoAppPath, 'images', 'logos'), 'sentry-fonts': path.join(staticPrefix, 'fonts'), // Aliasing this for getsentry's build, otherwise `less/select2` will not be able // to be resolved less: path.join(staticPrefix, 'less'), 'sentry-test': path.join(__dirname, 'tests', 'js', 'sentry-test'), 'sentry-locale': path.join(__dirname, 'src', 'sentry', 'locale'), 'ios-device-list': path.join( __dirname, 'node_modules', 'ios-device-list', 'dist', 'ios-device-list.min.js' ), }, fallback: { vm: false, stream: false, crypto: require.resolve('crypto-browserify'), // `yarn why` says this is only needed in dev deps string_decoder: false, }, modules: ['node_modules'], extensions: ['.jsx', '.js', '.json', '.ts', '.tsx', '.less'], }, output: { clean: true, // Clean the output directory before emit. path: distPath, publicPath: '', filename: 'entrypoints/[name].js', chunkFilename: 'chunks/[name].[contenthash].js', sourceMapFilename: 'sourcemaps/[name].[contenthash].js.map', assetModuleFilename: 'assets/[name].[contenthash][ext]', }, optimization: { chunkIds: 'named', moduleIds: 'named', splitChunks: { // Only affect async chunks, otherwise webpack could potentially split our initial chunks // Which means the app will not load because we'd need these additional chunks to be loaded in our // django template. chunks: 'async', maxInitialRequests: 10, // (default: 30) maxAsyncRequests: 10, // (default: 30) cacheGroups: { ...localeChunkGroups, }, }, // This only runs in production mode // Grabbed this example from https://github.com/webpack-contrib/css-minimizer-webpack-plugin minimizer: ['...', new CssMinimizerPlugin()], }, devtool: IS_PRODUCTION ? 'source-map' : 'eval-cheap-module-source-map', }; if (IS_TEST || IS_ACCEPTANCE_TEST) { appConfig.resolve!.alias!['integration-docs-platforms'] = path.join( __dirname, 'fixtures/integration-docs/_platforms.json' ); } else { const plugin = new IntegrationDocsFetchPlugin({basePath: __dirname}); appConfig.plugins?.push(plugin); appConfig.resolve!.alias!['integration-docs-platforms'] = plugin.modulePath; } if (IS_ACCEPTANCE_TEST) { appConfig.plugins?.push(new LastBuiltPlugin({basePath: __dirname})); } // Dev only! Hot module reloading if ( FORCE_WEBPACK_DEV_SERVER || (HAS_WEBPACK_DEV_SERVER_CONFIG && !NO_DEV_SERVER) || IS_UI_DEV_ONLY ) { if (SHOULD_HOT_MODULE_RELOAD) { // Hot reload react components on save // We include the library here as to not break docker/google cloud builds // since we do not install devDeps there. const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); appConfig.plugins?.push(new ReactRefreshWebpackPlugin()); // TODO: figure out why defining output breaks hot reloading if (IS_UI_DEV_ONLY) { appConfig.output = {}; } if (SHOULD_LAZY_LOAD) { appConfig.experiments = { lazyCompilation: { // enable lazy compilation for dynamic imports imports: true, // disable lazy compilation for entries entries: false, }, }; } } appConfig.devServer = { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true', 'Document-Policy': 'js-profiling', }, // Cover the various environments we use (vercel, getsentry-dev, localhost) allowedHosts: [ '.sentry.dev', '.dev.getsentry.net', '.localhost', '127.0.0.1', '.docker.internal', ], static: { directory: './src/sentry/static/sentry', watch: true, }, host: SENTRY_WEBPACK_PROXY_HOST, // Don't reload on errors hot: 'only', port: Number(SENTRY_WEBPACK_PROXY_PORT), devMiddleware: { stats: 'errors-only', }, client: { overlay: false, }, }; if (!IS_UI_DEV_ONLY) { // This proxies to local backend server const backendAddress = `http://127.0.0.1:${SENTRY_BACKEND_PORT}/`; const relayAddress = 'http://127.0.0.1:7899'; // If we're running siloed servers we also need to proxy // those requests to the right server. let controlSiloProxy = {}; if (CONTROL_SILO_PORT) { // TODO(hybridcloud) We also need to use this URL pattern // list to select contro/region when making API requests in non-proxied // environments (like production). We'll likely need a way to consolidate this // with the configuration api.Client uses. const controlSiloAddress = `http://127.0.0.1:${CONTROL_SILO_PORT}`; controlSiloProxy = { '/auth/**': controlSiloAddress, '/account/**': controlSiloAddress, '/api/0/users/**': controlSiloAddress, '/api/0/api-tokens/**': controlSiloAddress, '/api/0/sentry-apps/**': controlSiloAddress, '/api/0/organizations/*/audit-logs/**': controlSiloAddress, '/api/0/organizations/*/broadcasts/**': controlSiloAddress, '/api/0/organizations/*/integrations/**': controlSiloAddress, '/api/0/organizations/*/config/integrations/**': controlSiloAddress, '/api/0/organizations/*/sentry-apps/**': controlSiloAddress, '/api/0/organizations/*/sentry-app-installations/**': controlSiloAddress, '/api/0/api-authorizations/**': controlSiloAddress, '/api/0/api-applications/**': controlSiloAddress, '/api/0/doc-integrations/**': controlSiloAddress, '/api/0/assistant/**': controlSiloAddress, }; } appConfig.devServer = { ...appConfig.devServer, static: { ...(appConfig.devServer.static as object), publicPath: '/_static/dist/sentry', }, // syntax for matching is using https://www.npmjs.com/package/micromatch proxy: { ...controlSiloProxy, '/api/store/**': relayAddress, '/api/{1..9}*({0..9})/**': relayAddress, '/api/0/relays/outcomes/': relayAddress, '!/_static/dist/sentry/**': backendAddress, }, }; appConfig.output!.publicPath = '/_static/dist/sentry/'; } } // XXX(epurkhiser): Sentry (development) can be run in an experimental // pure-SPA mode, where ONLY /api* requests are proxied directly to the API // backend (in this case, sentry.io), otherwise ALL requests are rewritten // to a development index.html -- thus, completely separating the frontend // from serving any pages through the backend. // // THIS IS EXPERIMENTAL and has limitations (e.g. you can't use SSO) // // Various sentry pages still rely on django to serve html views. if (IS_UI_DEV_ONLY) { // XXX: If you change this also change its sibiling in: // - static/index.ejs // - static/app/utils/extractSlug.tsx const KNOWN_DOMAINS = /(?:\.?)((?:localhost|dev\.getsentry\.net|sentry\.dev)(?:\:\d*)?)$/; const extractSlug = (hostname: string) => { const match = hostname.match(KNOWN_DOMAINS); if (!match) { return null; } const [ matchedExpression, // Expression includes optional leading `.` ] = match; const [slug] = hostname.replace(matchedExpression, '').split('.'); return slug; }; // Try and load certificates from mkcert if available. Use $ yarn mkcert-localhost const certPath = path.join(__dirname, 'config'); const httpsOptions = !fs.existsSync(path.join(certPath, 'localhost.pem')) ? {} : { key: fs.readFileSync(path.join(certPath, 'localhost-key.pem')), cert: fs.readFileSync(path.join(certPath, 'localhost.pem')), }; appConfig.devServer = { ...appConfig.devServer, compress: true, server: { type: 'https', options: httpsOptions, }, headers: { 'Document-Policy': 'js-profiling', }, static: { publicPath: '/_assets/', }, proxy: [ { context: ['/api/', '/avatar/', '/organization-avatar/', '/extensions/'], target: 'https://sentry.io', secure: false, changeOrigin: true, headers: { Referer: 'https://sentry.io/', 'Document-Policy': 'js-profiling', }, cookieDomainRewrite: {'.sentry.io': 'localhost'}, router: ({hostname}) => { const orgSlug = extractSlug(hostname); return orgSlug ? `https://${orgSlug}.sentry.io` : 'https://sentry.io'; }, }, ], historyApiFallback: { rewrites: [{from: /^\/.*$/, to: '/_assets/index.html'}], }, }; appConfig.optimization = { runtimeChunk: 'single', }; } if (IS_UI_DEV_ONLY || SENTRY_EXPERIMENTAL_SPA) { appConfig.output!.publicPath = '/_assets/'; /** * Generate a index.html file used for running the app in pure client mode. * This is currently used for PR deploy previews, where only the frontend * is deployed. */ const HtmlWebpackPlugin = require('html-webpack-plugin'); appConfig.plugins?.push( new HtmlWebpackPlugin({ // Local dev vs vercel slightly differs... ...(IS_UI_DEV_ONLY ? {devServer: `https://127.0.0.1:${SENTRY_WEBPACK_PROXY_PORT}`} : {}), favicon: path.resolve(sentryDjangoAppPath, 'images', 'favicon_dev.png'), template: path.resolve(staticPrefix, 'index.ejs'), mobile: true, excludeChunks: ['pipeline'], title: 'Sentry', window: { __SENTRY_DEV_UI: true, }, }) ); } const minificationPlugins = [ // This compression-webpack-plugin generates pre-compressed files // ending in .gz, to be picked up and served by our internal static media // server as well as nginx when paired with the gzip_static module. // // TODO(ts): The current @types/compression-webpack-plugin is still targeting // webpack@4, for now we just as any it. new CompressionPlugin({ algorithm: 'gzip', test: /\.(js|map|css|svg|html|txt|ico|eot|ttf)$/, }) as any, // NOTE: In production mode webpack will automatically minify javascript // using the TerserWebpackPlugin. ]; if (IS_PRODUCTION) { // NOTE: can't do plugins.push(Array) because webpack/webpack#2217 minificationPlugins.forEach(plugin => appConfig.plugins?.push(plugin)); } // Cache webpack builds if (env.WEBPACK_CACHE_PATH) { appConfig.cache = { type: 'filesystem', cacheLocation: path.resolve(__dirname, env.WEBPACK_CACHE_PATH), buildDependencies: { // This makes all dependencies of this file - build dependencies config: [__filename], // By default webpack and loaders are build dependencies }, }; } export default appConfig;