diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 3299f9167fbb..5c922aa84967 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -23,7 +23,7 @@ "@sentry/react": "6.10.0", "@sentry/tracing": "6.10.0", "@sentry/utils": "6.10.0", - "@sentry/webpack-plugin": "1.15.1", + "@sentry/webpack-plugin": "1.17.1", "tslib": "^1.9.3" }, "devDependencies": { diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 21017abec545..0a4adb8dad1c 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -1,4 +1,7 @@ -export { SentryCliPluginOptions as SentryWebpackPluginOptions } from '@sentry/webpack-plugin'; +import { SentryCliPluginOptions } from '@sentry/webpack-plugin'; + +export type SentryWebpackPluginOptions = SentryCliPluginOptions; +export type SentryWebpackPlugin = { options: SentryWebpackPluginOptions }; /** * Overall Nextjs config @@ -9,6 +12,8 @@ export type ExportedNextConfig = NextConfigObject | NextConfigFunction; export type NextConfigObject = { // custom webpack options webpack?: WebpackConfigFunction; + // whether to build serverless functions for all pages, not just API routes + target?: 'server' | 'experimental-serverless-trace'; sentry?: { disableServerWebpackPlugin?: boolean; disableClientWebpackPlugin?: boolean; @@ -43,7 +48,13 @@ export type WebpackConfigObject = { }; // Information about the current build environment -export type BuildContext = { dev: boolean; isServer: boolean; buildId: string; dir: string }; +export type BuildContext = { + dev: boolean; + isServer: boolean; + buildId: string; + dir: string; + config: Partial; +}; /** * Webpack `entry` config diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 5545d40b50c0..643b17623d39 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -21,18 +21,6 @@ export { SentryWebpackPlugin }; // TODO: merge default SentryWebpackPlugin include with their SentryWebpackPlugin include // TODO: drop merged keys from override check? `includeDefaults` option? -const defaultSentryWebpackPluginOptions = dropUndefinedKeys({ - url: process.env.SENTRY_URL, - org: process.env.SENTRY_ORG, - project: process.env.SENTRY_PROJECT, - authToken: process.env.SENTRY_AUTH_TOKEN, - configFile: 'sentry.properties', - stripPrefix: ['webpack://_N_E/'], - urlPrefix: `~/_next`, - include: '.next/', - ignore: ['.next/cache', 'server/ssr-module-cache.js', 'static/*/_ssgManifest.js', 'static/*/_buildManifest.js'], -}); - /** * Construct the function which will be used as the nextjs config's `webpack` value. * @@ -88,18 +76,11 @@ export function constructWebpackConfigFunction( newConfig.devtool = 'source-map'; } - checkWebpackPluginOverrides(userSentryWebpackPluginOptions); - newConfig.plugins = newConfig.plugins || []; newConfig.plugins.push( // @ts-ignore Our types for the plugin are messed up somehow - TS wants this to be `SentryWebpackPlugin.default`, // but that's not actually a thing - new SentryWebpackPlugin({ - dryRun: buildContext.dev, - release: getSentryRelease(buildContext.buildId), - ...defaultSentryWebpackPluginOptions, - ...userSentryWebpackPluginOptions, - }), + new SentryWebpackPlugin(getWebpackPluginOptions(buildContext, userSentryWebpackPluginOptions)), ); } @@ -136,7 +117,7 @@ async function addSentryToEntryProperty( : getUserConfigFile(buildContext.dir, 'client'); for (const entryPointName in newEntryProperty) { - if (entryPointName === 'pages/_app' || entryPointName.includes('pages/api')) { + if (shouldAddSentryToEntryPoint(entryPointName)) { // we need to turn the filename into a path so webpack can find it addFileToExistingEntryPoint(newEntryProperty, entryPointName, `./${userConfigFile}`); } @@ -221,13 +202,15 @@ function addFileToExistingEntryPoint( * our default options are getting overridden. (Note: If any of our default values is undefined, it won't be included in * the warning.) * - * @param userSentryWebpackPluginOptions The user's SentryWebpackPlugin options + * @param defaultOptions Default SentryWebpackPlugin options + * @param userOptions The user's SentryWebpackPlugin options */ -function checkWebpackPluginOverrides(userSentryWebpackPluginOptions: Partial): void { +function checkWebpackPluginOverrides( + defaultOptions: SentryWebpackPluginOptions, + userOptions: Partial, +): void { // warn if any of the default options for the webpack plugin are getting overridden - const sentryWebpackPluginOptionOverrides = Object.keys(defaultSentryWebpackPluginOptions) - .concat('dryrun') - .filter(key => key in userSentryWebpackPluginOptions); + const sentryWebpackPluginOptionOverrides = Object.keys(defaultOptions).filter(key => key in userOptions); if (sentryWebpackPluginOptionOverrides.length > 0) { logger.warn( '[Sentry] You are overriding the following automatically-set SentryWebpackPlugin config options:\n' + @@ -236,3 +219,58 @@ function checkWebpackPluginOverrides(userSentryWebpackPluginOptions: Partial, +): SentryWebpackPluginOptions { + const { isServer, dir: projectDir, buildId, dev: isDev, config: nextConfig } = buildContext; + + const isServerless = nextConfig.target === 'experimental-serverless-trace'; + const hasSentryProperties = fs.existsSync(path.resolve(projectDir, 'sentry.properties')); + + const serverInclude = isServerless + ? [{ paths: ['.next/serverless/'], urlPrefix: '~/_next/serverless' }] + : [ + { paths: ['.next/server/chunks/'], urlPrefix: '~/_next/server/chunks' }, + { paths: ['.next/server/pages/'], urlPrefix: '~/_next/server/pages' }, + ]; + const clientInclude = [{ paths: ['.next/static/chunks/pages'], urlPrefix: '~/_next/static/chunks/pages' }]; + + const defaultPluginOptions = dropUndefinedKeys({ + include: isServer ? serverInclude : clientInclude, + ignore: [], + 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/'], + urlPrefix: `~/_next`, + entries: shouldAddSentryToEntryPoint, + release: getSentryRelease(buildId), + dryRun: isDev, + }); + + checkWebpackPluginOverrides(defaultPluginOptions, userPluginOptions); + + return { ...defaultPluginOptions, ...userPluginOptions }; +} diff --git a/packages/nextjs/test/config.test.ts b/packages/nextjs/test/config.test.ts index 90e3cc5a6e47..636266cd404b 100644 --- a/packages/nextjs/test/config.test.ts +++ b/packages/nextjs/test/config.test.ts @@ -9,6 +9,7 @@ import { EntryPropertyFunction, ExportedNextConfig, NextConfigObject, + SentryWebpackPlugin as SentryWebpackPluginType, SentryWebpackPluginOptions, WebpackConfigObject, } from '../src/config/types'; @@ -44,7 +45,9 @@ const userNextConfig = { }), }), }; -const userSentryWebpackPluginConfig = { org: 'squirrelChasers', project: 'simulator', include: './thirdPartyMaps' }; +const userSentryWebpackPluginConfig = { org: 'squirrelChasers', project: 'simulator' }; +process.env.SENTRY_AUTH_TOKEN = 'dogsarebadatkeepingsecrets'; +process.env.SENTRY_RELEASE = 'doGsaREgReaT'; /** Mocks of the arguments passed to the result of `withSentryConfig` (when it's a function). */ const runtimePhase = 'ball-fetching'; @@ -80,10 +83,12 @@ const clientWebpackConfig = { target: 'web', context: '/Users/Maisey/projects/squirrelChasingSimulator', }; + const baseBuildContext = { dev: false, - buildId: 'doGsaREgReaT', + buildId: 'sItStAyLiEdOwN', dir: '/Users/Maisey/projects/squirrelChasingSimulator', + config: { target: 'server' as const }, }; const serverBuildContext = { isServer: true, ...baseBuildContext }; const clientBuildContext = { isServer: false, ...baseBuildContext }; @@ -100,7 +105,7 @@ const clientBuildContext = { isServer: false, ...baseBuildContext }; */ function materializeFinalNextConfig( userNextConfig: ExportedNextConfig, - userSentryWebpackPluginConfig?: SentryWebpackPluginOptions, + userSentryWebpackPluginConfig?: Partial, ): NextConfigObject { const sentrifiedConfig = withSentryConfig(userNextConfig, userSentryWebpackPluginConfig); let finalConfigValues = sentrifiedConfig; @@ -131,7 +136,7 @@ function materializeFinalNextConfig( */ async function materializeFinalWebpackConfig(options: { userNextConfig: ExportedNextConfig; - userSentryWebpackPluginConfig?: SentryWebpackPluginOptions; + userSentryWebpackPluginConfig?: Partial; incomingWebpackConfig: WebpackConfigObject; incomingWebpackBuildContext: BuildContext; }): Promise { @@ -311,12 +316,40 @@ describe('webpack config', () => { }); describe('Sentry webpack plugin config', () => { - it('includes expected properties', () => { - // TODO + it('includes expected properties', async () => { + // also, can pull from either env or user config (see notes on specific properties below) + const finalWebpackConfig = await materializeFinalWebpackConfig({ + userNextConfig, + userSentryWebpackPluginConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + }); + + expect(finalWebpackConfig.plugins?.[0].options).toEqual( + expect.objectContaining({ + include: expect.any(Array), // default, tested separately elsewhere + ignore: [], // default + org: 'squirrelChasers', // from user webpack plugin config + project: 'simulator', // from user webpack plugin config + authToken: 'dogsarebadatkeepingsecrets', // picked up from env + stripPrefix: ['webpack://_N_E/'], // default + urlPrefix: `~/_next`, // default + entries: expect.any(Function), // default, tested separately elsewhere + release: 'doGsaREgReaT', // picked up from env + dryRun: false, // based on buildContext.dev being false + }), + ); }); - it('preserves unrelated plugin config options', () => { - // TODO + it('preserves unrelated plugin config options', async () => { + const finalWebpackConfig = await materializeFinalWebpackConfig({ + userNextConfig, + userSentryWebpackPluginConfig: { ...userSentryWebpackPluginConfig, debug: true }, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + }); + + expect((finalWebpackConfig.plugins?.[0].options as SentryWebpackPluginOptions).debug).toEqual(true); }); it('warns when overriding certain default values', () => { @@ -327,6 +360,51 @@ describe('Sentry webpack plugin config', () => { // do we even want to do this? }); + describe('Sentry webpack plugin `include` option', () => { + it('has the correct value when building client bundles', async () => { + const finalWebpackConfig = await materializeFinalWebpackConfig({ + userNextConfig, + incomingWebpackConfig: clientWebpackConfig, + incomingWebpackBuildContext: clientBuildContext, + }); + + const sentryWebpackPlugin = finalWebpackConfig.plugins?.[0] as SentryWebpackPluginType; + + expect(sentryWebpackPlugin.options?.include).toEqual([ + { paths: ['.next/static/chunks/pages'], urlPrefix: '~/_next/static/chunks/pages' }, + ]); + }); + + it('has the correct value when building serverless server bundles', async () => { + const finalWebpackConfig = await materializeFinalWebpackConfig({ + userNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: { ...serverBuildContext, config: { target: 'experimental-serverless-trace' } }, + }); + + const sentryWebpackPlugin = finalWebpackConfig.plugins?.[0] as SentryWebpackPluginType; + + expect(sentryWebpackPlugin.options?.include).toEqual([ + { paths: ['.next/serverless/'], urlPrefix: '~/_next/serverless' }, + ]); + }); + + it('has the correct value when building serverful server bundles', async () => { + const finalWebpackConfig = await materializeFinalWebpackConfig({ + userNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + }); + + const sentryWebpackPlugin = finalWebpackConfig.plugins?.[0] as SentryWebpackPluginType; + + expect(sentryWebpackPlugin.options?.include).toEqual([ + { paths: ['.next/server/chunks/'], urlPrefix: '~/_next/server/chunks' }, + { paths: ['.next/server/pages/'], urlPrefix: '~/_next/server/pages' }, + ]); + }); + }); + it('allows SentryWebpackPlugin to be turned off for client code (independent of server code)', () => { const clientFinalNextConfig = materializeFinalNextConfig({ ...userNextConfig, diff --git a/yarn.lock b/yarn.lock index 916a3a592d33..3c7f2668e046 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2624,10 +2624,10 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= -"@sentry/cli@^1.64.1": - version "1.66.0" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.66.0.tgz#0526f1bc1c0570ce72ed817190af92f3b63a2e9a" - integrity sha512-2pZ+JHnvKqwyJWcGkKg/gCM/zURYronAnruBNllI+rH2g5IL0N90deMmjB1xcqXS66J222+MPTtWrGEK1Vl0/w== +"@sentry/cli@^1.68.0": + version "1.68.0" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.68.0.tgz#2ced8fac67ee01e746a45e8ee45a518d4526937e" + integrity sha512-zc7+cxKDqpHLREGJKRH6KwE8fZW8bnczg3OLibJ0czleXoWPdAuOK1Xm1BTMcOnaXfg3VKAh0rI7S1PTdj+SrQ== dependencies: https-proxy-agent "^5.0.0" mkdirp "^0.5.5" @@ -2636,12 +2636,12 @@ progress "^2.0.3" proxy-from-env "^1.1.0" -"@sentry/webpack-plugin@1.15.1": - version "1.15.1" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.15.1.tgz#deb014fce8c1b51811100f25ec9050dd03addd9b" - integrity sha512-/Z06MJDXyWcN2+CbeDTMDwVzySjgZWQajOke773TvpkgqdtkeT1eYBsA+pfsje+ZE1prEgrZdlH/L9HdM1odnQ== +"@sentry/webpack-plugin@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.17.1.tgz#1b3ebbe9991e4d77125ace2b24594059a088268a" + integrity sha512-L47a0hxano4a+9jbvQSBzHCT1Ph8fYAvGGUvFg8qc69yXS9si5lXRNIH/pavN6mqJjhQjAcEsEp+vxgvT4xZDQ== dependencies: - "@sentry/cli" "^1.64.1" + "@sentry/cli" "^1.68.0" "@simple-dom/interface@^1.4.0": version "1.4.0"