diff --git a/packages/nextjs/rollup.npm.config.js b/packages/nextjs/rollup.npm.config.js index e6ddcbf97a50..32100ef278be 100644 --- a/packages/nextjs/rollup.npm.config.js +++ b/packages/nextjs/rollup.npm.config.js @@ -20,9 +20,6 @@ export default [ ...makeNPMConfigVariants( makeBaseNPMConfig({ entrypoints: [ - 'src/config/templates/serverRewriteFramesPrefixLoaderTemplate.ts', - 'src/config/templates/clientRewriteFramesPrefixLoaderTemplate.ts', - 'src/config/templates/releasePrefixLoaderTemplate.ts', 'src/config/templates/pageProxyLoaderTemplate.ts', 'src/config/templates/apiProxyLoaderTemplate.ts', ], diff --git a/packages/nextjs/src/config/loaders/index.ts b/packages/nextjs/src/config/loaders/index.ts index 00bf268fdc91..150e00bf1ca4 100644 --- a/packages/nextjs/src/config/loaders/index.ts +++ b/packages/nextjs/src/config/loaders/index.ts @@ -1,2 +1,3 @@ +export { default as valueInjectionLoader } from './valueInjectionLoader'; export { default as prefixLoader } from './prefixLoader'; export { default as proxyLoader } from './proxyLoader'; diff --git a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts new file mode 100644 index 000000000000..be8340dccdf3 --- /dev/null +++ b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts @@ -0,0 +1,26 @@ +import { LoaderThis } from './types'; + +type LoaderOptions = { + values: Record; +}; + +/** + * Set values on the global/window object at the start of a module. + * + * Options: + * - `values`: An object where the keys correspond to the keys of the global values to set and the values + * correspond to the values of the values on the global object. Values must be JSON serializable. + */ +export default function valueInjectionLoader(this: LoaderThis, userCode: string): string { + // We know one or the other will be defined, depending on the version of webpack being used + const { values } = 'getOptions' in this ? this.getOptions() : this.query; + + // Define some global proxy that works on server and on the browser. + let injectedCode = 'var _sentryCollisionFreeGlobalObject = typeof window === "undefined" ? global : window;\n'; + + Object.entries(values).forEach(([key, value]) => { + injectedCode += `_sentryCollisionFreeGlobalObject["${key}"] = ${JSON.stringify(value)};\n`; + }); + + return `${injectedCode}\n${userCode}`; +} diff --git a/packages/nextjs/src/config/templates/clientRewriteFramesPrefixLoaderTemplate.ts b/packages/nextjs/src/config/templates/clientRewriteFramesPrefixLoaderTemplate.ts deleted file mode 100644 index 50a304ae0586..000000000000 --- a/packages/nextjs/src/config/templates/clientRewriteFramesPrefixLoaderTemplate.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint-disable no-restricted-globals */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ - -(window as any).__rewriteFramesAssetPrefixPath__ = '__ASSET_PREFIX_PATH__'; - -// We need this to make this file an ESM module, which TS requires when using `isolatedModules`, but it doesn't affect -// the end result - Rollup recognizes that it's a no-op and doesn't include it when building our code. -export {}; diff --git a/packages/nextjs/src/config/templates/releasePrefixLoaderTemplate.ts b/packages/nextjs/src/config/templates/releasePrefixLoaderTemplate.ts deleted file mode 100644 index a41fe7f0d7c6..000000000000 --- a/packages/nextjs/src/config/templates/releasePrefixLoaderTemplate.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable no-constant-condition */ - -import { GLOBAL_OBJ } from '@sentry/utils'; - -import { EnhancedGlobal } from '../types'; - -const globalObj = GLOBAL_OBJ as EnhancedGlobal; - -globalObj.SENTRY_RELEASE = { id: '__RELEASE__' }; - -// Enable module federation support (see https://github.com/getsentry/sentry-webpack-plugin/pull/307) -if ('__PROJECT__') { - const key = '__ORG__' ? '__PROJECT__@__ORG__' : '__PROJECT__'; - globalObj.SENTRY_RELEASES = globalObj.SENTRY_RELEASES || {}; - globalObj.SENTRY_RELEASES[key] = { id: '__RELEASE__' }; -} diff --git a/packages/nextjs/src/config/templates/serverRewriteFramesPrefixLoaderTemplate.ts b/packages/nextjs/src/config/templates/serverRewriteFramesPrefixLoaderTemplate.ts deleted file mode 100644 index 9bce6accc6be..000000000000 --- a/packages/nextjs/src/config/templates/serverRewriteFramesPrefixLoaderTemplate.ts +++ /dev/null @@ -1,6 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any -(global as any).__rewriteFramesDistDir__ = '__DIST_DIR__'; - -// We need this to make this file an ESM module, which TS requires when using `isolatedModules`, but it doesn't affect -// the end result - Rollup recognizes that it's a no-op and doesn't include it when building our code. -export {}; diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index d2fec9898d27..22738dcfdc90 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -87,26 +87,8 @@ export function constructWebpackConfigFunction( // `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 for use by `RewriteFrames` - addRewriteFramesLoader(newConfig, isServer ? 'server' : 'client', userNextConfig); - - newConfig.module.rules.push({ - test: /sentry\.(server|client)\.config\.(jsx?|tsx?)/, - use: [ - { - // Inject the release value the same way the webpack plugin does. - loader: path.resolve(__dirname, 'loaders/prefixLoader.js'), - options: { - templatePrefix: 'release', - replacements: [ - ['__RELEASE__', webpackPluginOptions.release || process.env.SENTRY_RELEASE], - ['__ORG__', webpackPluginOptions.org || process.env.SENTRY_ORG], - ['__PROJECT__', webpackPluginOptions.project || process.env.SENTRY_PROJECT || ''], - ], - }, - }, - ], - }); + // Add a loader which will inject code that sets global values + addValueInjectionLoader(newConfig, userNextConfig, webpackPluginOptions); if (isServer) { if (userSentryOptions.autoInstrumentServerFunctions !== false) { @@ -667,49 +649,76 @@ function setUpModuleRules(newConfig: WebpackConfigObject): WebpackConfigObjectWi } /** - * Support the `distDir` and `assetPrefix` options by making their values (easy to get here at build-time) available at - * runtime (for use by `RewriteFrames`), by injecting code to attach their values to `global` or `window`. - * - * @param newConfig The webpack config object being constructed - * @param target Either 'server' or 'client' - * @param userNextConfig The user's nextjs config options + * Adds loaders to inject values on the global object based on user configuration. */ -function addRewriteFramesLoader( +function addValueInjectionLoader( newConfig: WebpackConfigObjectWithModuleRules, - target: 'server' | 'client', userNextConfig: NextConfigObject, + webpackPluginOptions: SentryWebpackPlugin.SentryCliPluginOptions, ): void { - // Nextjs will use `basePath` in place of `assetPrefix` if it's defined but `assetPrefix` is not const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || ''; - const replacements = { - server: [ - [ - '__DIST_DIR__', - // Make sure that if we have a windows path, the backslashes are interpreted as such (rather than as escape - // characters) - userNextConfig.distDir?.replace(/\\/g, '\\\\') || '.next', - ], - ], - client: [ - [ - '__ASSET_PREFIX_PATH__', - // 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.) - assetPrefix ? new URL(assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '') : '', - ], - ], + const releaseValue = webpackPluginOptions.release || process.env.SENTRY_RELEASE; + const orgValue = webpackPluginOptions.org || process.env.SENTRY_ORG; + const projectValue = webpackPluginOptions.project || process.env.SENTRY_PROJECT; + + const isomorphicValues = { + // Inject release into SDK + ...(releaseValue + ? { + SENTRY_RELEASE: { + id: releaseValue, + }, + } + : undefined), + + // Enable module federation support (see https://github.com/getsentry/sentry-webpack-plugin/pull/307) + ...(projectValue && releaseValue + ? { + SENTRY_RELEASES: { + [orgValue ? `${projectValue}@${orgValue}` : projectValue]: { id: releaseValue }, + }, + } + : undefined), }; - newConfig.module.rules.push({ - test: new RegExp(`sentry\\.${target}\\.config\\.(jsx?|tsx?)`), - use: [ - { - loader: path.resolve(__dirname, 'loaders/prefixLoader.js'), - options: { - templatePrefix: `${target}RewriteFrames`, - replacements: replacements[target], + const serverValues = { + ...isomorphicValues, + // Make sure that if we have a windows path, the backslashes are interpreted as such (rather than as escape + // characters) + __rewriteFramesDistDir__: userNextConfig.distDir?.replace(/\\/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\.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, + }, + }, + ], + }, + ); } diff --git a/packages/nextjs/src/index.client.ts b/packages/nextjs/src/index.client.ts index a28969824c92..7a8eaa1179db 100644 --- a/packages/nextjs/src/index.client.ts +++ b/packages/nextjs/src/index.client.ts @@ -35,7 +35,10 @@ declare const __SENTRY_TRACING__: boolean; // https://github.com/vercel/next.js/blob/166e5fb9b92f64c4b5d1f6560a05e2b9778c16fb/packages/next/build/webpack-config.ts#L206 declare const EdgeRuntime: string | undefined; -type GlobalWithAssetPrefixPath = typeof global & { __rewriteFramesAssetPrefixPath__: string }; +const globalWithInjectedValues = global as typeof global & { + __rewriteFramesAssetPrefixPath__: string; + __sentryRewritesTunnelPath__?: string; +}; /** Inits the Sentry NextJS SDK on the browser with the React SDK. */ export function init(options: NextjsOptions): void { @@ -67,7 +70,7 @@ function addClientIntegrations(options: NextjsOptions): void { // This value is injected at build time, based on the output directory specified in the build config. Though a default // is set there, we set it here as well, just in case something has gone wrong with the injection. - const assetPrefixPath = (global as GlobalWithAssetPrefixPath).__rewriteFramesAssetPrefixPath__ || ''; + const assetPrefixPath = globalWithInjectedValues.__rewriteFramesAssetPrefixPath__ || ''; const defaultRewriteFramesIntegration = new RewriteFrames({ // Turn `//_next/static/...` into `app:///_next/static/...` diff --git a/packages/nextjs/src/index.server.ts b/packages/nextjs/src/index.server.ts index 993abbd4cdb8..ca36e41a0427 100644 --- a/packages/nextjs/src/index.server.ts +++ b/packages/nextjs/src/index.server.ts @@ -19,7 +19,10 @@ export { captureUnderscoreErrorException } from './utils/_error'; // because or SSR of next.js we can only use this. export { ErrorBoundary, showReportDialog, withErrorBoundary } from '@sentry/react'; -type GlobalWithDistDir = typeof global & { __rewriteFramesDistDir__: string }; +const globalWithInjectedValues = global as typeof global & { + __rewriteFramesDistDir__: string; +}; + const domain = domainModule as typeof domainModule & { active: (domainModule.Domain & Carrier) | null }; // This is a variable that Next.js will string replace during build with a string if run in an edge runtime from Next.js @@ -114,7 +117,7 @@ function addServerIntegrations(options: NextjsOptions): void { // This value is injected at build time, based on the output directory specified in the build config. Though a default // is set there, we set it here as well, just in case something has gone wrong with the injection. - const distDirName = (global as GlobalWithDistDir).__rewriteFramesDistDir__ || '.next'; + const distDirName = globalWithInjectedValues.__rewriteFramesDistDir__ || '.next'; // nextjs always puts the build directory at the project root level, which is also where you run `next start` from, so // we can read in the project directory from the currently running process const distDirAbsPath = path.resolve(process.cwd(), distDirName); diff --git a/packages/nextjs/test/config/loaders.test.ts b/packages/nextjs/test/config/loaders.test.ts index 26b3d039ec15..714d3388782c 100644 --- a/packages/nextjs/test/config/loaders.test.ts +++ b/packages/nextjs/test/config/loaders.test.ts @@ -7,7 +7,6 @@ import { exportedNextConfig, serverBuildContext, serverWebpackConfig, - userSentryWebpackPluginConfig, } from './fixtures'; import { materializeFinalWebpackConfig } from './testUtils'; @@ -41,7 +40,7 @@ declare global { describe('webpack loaders', () => { describe('server loaders', () => { - it('adds server `RewriteFrames` loader to server config', async () => { + it('adds server `valueInjection` loader to server config', async () => { const finalWebpackConfig = await materializeFinalWebpackConfig({ exportedNextConfig, incomingWebpackConfig: serverWebpackConfig, @@ -52,34 +51,11 @@ describe('webpack loaders', () => { test: /sentry\.server\.config\.(jsx?|tsx?)/, use: [ { - loader: expect.stringEndingWith('prefixLoader.js'), - options: expect.objectContaining({ templatePrefix: 'serverRewriteFrames' }), - }, - ], - }); - }); - - it('adds release prefix loader to server config', async () => { - const finalWebpackConfig = await materializeFinalWebpackConfig({ - exportedNextConfig, - incomingWebpackConfig: serverWebpackConfig, - incomingWebpackBuildContext: serverBuildContext, - userSentryWebpackPluginConfig: userSentryWebpackPluginConfig, - }); - - expect(finalWebpackConfig.module.rules).toContainEqual({ - test: /sentry\.(server|client)\.config\.(jsx?|tsx?)/, - use: [ - { - loader: expect.stringEndingWith('prefixLoader.js'), - options: { - templatePrefix: 'release', - replacements: [ - ['__RELEASE__', 'doGsaREgReaT'], - ['__ORG__', 'squirrelChasers'], - ['__PROJECT__', 'simulator'], - ], - }, + loader: expect.stringEndingWith('valueInjectionLoader.js'), + // We use `expect.objectContaining({})` rather than `expect.any(Object)` to match any plain object because + // the latter will also match arrays, regexes, dates, sets, etc. - anything whose `typeof` value is + // `'object'`. + options: expect.objectContaining({ values: expect.objectContaining({}) }), }, ], }); @@ -87,7 +63,7 @@ describe('webpack loaders', () => { }); describe('client loaders', () => { - it('adds `RewriteFrames` loader to client config', async () => { + it('adds `valueInjection` loader to client config', async () => { const finalWebpackConfig = await materializeFinalWebpackConfig({ exportedNextConfig, incomingWebpackConfig: clientWebpackConfig, @@ -98,34 +74,11 @@ describe('webpack loaders', () => { test: /sentry\.client\.config\.(jsx?|tsx?)/, use: [ { - loader: expect.stringEndingWith('prefixLoader.js'), - options: expect.objectContaining({ templatePrefix: 'clientRewriteFrames' }), - }, - ], - }); - }); - - it('adds release prefix loader to client config', async () => { - const finalWebpackConfig = await materializeFinalWebpackConfig({ - exportedNextConfig, - incomingWebpackConfig: clientWebpackConfig, - incomingWebpackBuildContext: clientBuildContext, - userSentryWebpackPluginConfig: userSentryWebpackPluginConfig, - }); - - expect(finalWebpackConfig.module.rules).toContainEqual({ - test: /sentry\.(server|client)\.config\.(jsx?|tsx?)/, - use: [ - { - loader: expect.stringEndingWith('prefixLoader.js'), - options: { - templatePrefix: 'release', - replacements: [ - ['__RELEASE__', 'doGsaREgReaT'], - ['__ORG__', 'squirrelChasers'], - ['__PROJECT__', 'simulator'], - ], - }, + loader: expect.stringEndingWith('valueInjectionLoader.js'), + // We use `expect.objectContaining({})` rather than `expect.any(Object)` to match any plain object because + // the latter will also match arrays, regexes, dates, sets, etc. - anything whose `typeof` value is + // `'object'`. + options: expect.objectContaining({ values: expect.objectContaining({}) }), }, ], });