diff --git a/MIGRATION.md b/MIGRATION.md index 8b5d1bdf0c0e..b517c56e61e2 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -518,6 +518,45 @@ The following previously deprecated API has been removed from the `@sentry/nextj - `IS_BUILD` - `isBuild` +#### Removal of the `sentry` property in your Next.js options (next.config.js) + +With version 8 of the Sentry Next.js SDK, the SDK will no longer support passing Next.js options with a `sentry` +property to `withSentryConfig`. Please use the third argument of `withSentryConfig` to configure the SDK instead: + +```ts +// OLD +const nextConfig = { + // Your Next.js options... + + sentry: { + // Your Sentry SDK options... + }, +}; + +module.exports = withSentryConfig(nextConfig, { + // Your Sentry Webpack Plugin Options... +}); + +// NEW +const nextConfig = { + // Your Next.js options... +}; + +module.exports = withSentryConfig( + nextConfig, + { + // Your Sentry Webpack Plugin Options... + }, + { + // Your Sentry SDK options... + }, +); +``` + +The reason for this change is to have one consistent way of defining the SDK options. We hope that this change will +reduce confusion when setting up the SDK, with the upside that the explicit option is properly typed and will therefore +have code completion. + ### Astro SDK #### Removal of `trackHeaders` option for Astro middleware diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index c74990cb6a69..751969f17529 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -4,29 +4,15 @@ import type { DefinePlugin, WebpackPluginInstance } from 'webpack'; export type SentryWebpackPluginOptions = SentryCliPluginOptions; export type SentryWebpackPlugin = WebpackPluginInstance & { options: SentryWebpackPluginOptions }; + // Export this from here because importing something from Webpack (the library) in `webpack.ts` confuses the heck out of // madge, which we use for circular dependency checking. We've manually excluded this file from the check (which is // safe, since it only includes types), so we can import it here without causing madge to fail. See // https://github.com/pahen/madge/issues/306. export type { WebpackPluginInstance }; -/** - * Overall Nextjs config - */ - -// The first argument to `withSentryConfig` (which is the user's next config) may contain a `sentry` key, which we'll -// remove once we've captured it, in order to prevent nextjs from throwing warnings. Since it's only in there -// temporarily, we don't include it in the main `NextConfigObject` or `NextConfigFunction` types. -export type ExportedNextConfig = NextConfigObjectWithSentry | NextConfigFunctionWithSentry; - -export type NextConfigObjectWithSentry = NextConfigObject & { - sentry?: UserSentryOptions; -}; - -export type NextConfigFunctionWithSentry = ( - phase: string, - defaults: { defaultConfig: NextConfigObject }, -) => NextConfigObjectWithSentry | PromiseLike; +// The first argument to `withSentryConfig` (which is the user's next config). +export type ExportedNextConfig = NextConfigObject | NextConfigFunction; // Vendored from Next.js (this type is not complete - extend if necessary) type NextRewrite = { @@ -62,7 +48,7 @@ export type NextConfigObject = { >; }; -export type UserSentryOptions = { +export type SentryBuildtimeOptions = { /** * Override the SDK's default decision about whether or not to enable to the Sentry webpack plugin for server files. * Note that `false` forces the plugin to be enabled, even in situations where it's not recommended. diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index a6555c3b3343..5a1c9ede21b4 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -17,8 +17,8 @@ import type { BuildContext, EntryPropertyObject, NextConfigObject, + SentryBuildtimeOptions, SentryWebpackPluginOptions, - UserSentryOptions, WebpackConfigFunction, WebpackConfigObject, WebpackConfigObjectWithModuleRules, @@ -61,7 +61,7 @@ let showedMissingGlobalErrorWarningMsg = false; export function constructWebpackConfigFunction( userNextConfig: NextConfigObject = {}, userSentryWebpackPluginOptions: Partial = {}, - userSentryOptions: UserSentryOptions = {}, + userSentryOptions: SentryBuildtimeOptions = {}, ): WebpackConfigFunction { // 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 @@ -511,7 +511,7 @@ function findTranspilationRules(rules: WebpackModuleRule[] | undefined, projectD async function addSentryToEntryProperty( currentEntryProperty: WebpackEntryProperty, buildContext: BuildContext, - userSentryOptions: UserSentryOptions, + userSentryOptions: SentryBuildtimeOptions, ): Promise { // 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 @@ -725,7 +725,7 @@ function shouldAddSentryToEntryPoint(entryPointName: string, runtime: 'node' | ' export function getWebpackPluginOptions( buildContext: BuildContext, userPluginOptions: Partial, - userSentryOptions: UserSentryOptions, + userSentryOptions: SentryBuildtimeOptions, ): SentryWebpackPluginOptions { const { buildId, isServer, config, dir: projectDir } = buildContext; const userNextConfig = config as NextConfigObject; @@ -884,7 +884,7 @@ export function getWebpackPluginOptions( } /** Check various conditions to decide if we should run the plugin */ -function shouldEnableWebpackPlugin(buildContext: BuildContext, userSentryOptions: UserSentryOptions): boolean { +function shouldEnableWebpackPlugin(buildContext: BuildContext, userSentryOptions: SentryBuildtimeOptions): boolean { const { isServer } = buildContext; const { disableServerWebpackPlugin, disableClientWebpackPlugin } = userSentryOptions; @@ -900,7 +900,7 @@ function shouldEnableWebpackPlugin(buildContext: BuildContext, userSentryOptions /** 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: UserSentryOptions, isServer: boolean): void { +function handleSourcemapHidingOptionWarning(userSentryOptions: SentryBuildtimeOptions, isServer: boolean): void { // 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 @@ -969,7 +969,7 @@ function setUpModuleRules(newConfig: WebpackConfigObject): WebpackConfigObjectWi function addValueInjectionLoader( newConfig: WebpackConfigObjectWithModuleRules, userNextConfig: NextConfigObject, - userSentryOptions: UserSentryOptions, + userSentryOptions: SentryBuildtimeOptions, buildContext: BuildContext, sentryWebpackPluginOptions: Partial, ): void { diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index a665aa892c8c..90af76cca813 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -1,64 +1,62 @@ import { isThenable } from '@sentry/utils'; import type { - ExportedNextConfig, + ExportedNextConfig as NextConfig, NextConfigFunction, NextConfigObject, - NextConfigObjectWithSentry, + SentryBuildtimeOptions, SentryWebpackPluginOptions, - UserSentryOptions, } from './types'; import { constructWebpackConfigFunction } from './webpack'; let showedExportModeTunnelWarning = false; /** - * Add Sentry options to the config to be exported from the user's `next.config.js` file. + * Modifies the passed in Next.js configuration with automatic build-time instrumentation and source map upload. * - * @param exportedUserNextConfig The existing config to be exported prior to adding Sentry - * @param userSentryWebpackPluginOptions Configuration for SentryWebpackPlugin - * @param sentryOptions Optional additional options to add as alternative to `sentry` property of config + * @param nextConfig A Next.js configuration object, as usually exported in `next.config.js` or `next.config.mjs`. + * @param sentryWebpackPluginOptions Options to configure the automatically included Sentry Webpack Plugin for source maps and release management in Sentry. + * @param sentryBuildtimeOptions Additional options to configure instrumentation and * @returns The modified config to be exported */ export function withSentryConfig( - exportedUserNextConfig: ExportedNextConfig = {}, - userSentryWebpackPluginOptions: Partial = {}, - sentryOptions?: UserSentryOptions, + nextConfig: NextConfig = {}, + sentryWebpackPluginOptions: Partial = {}, + sentryBuildtimeOptions: SentryBuildtimeOptions = {}, ): NextConfigFunction | NextConfigObject { - if (typeof exportedUserNextConfig === 'function') { + if (typeof nextConfig === 'function') { return function (this: unknown, ...webpackConfigFunctionArgs: unknown[]): ReturnType { - const maybeUserNextConfigObject: NextConfigObjectWithSentry = exportedUserNextConfig.apply( - this, - webpackConfigFunctionArgs, - ); + const maybePromiseNextConfig: ReturnType = nextConfig.apply(this, webpackConfigFunctionArgs); - if (isThenable(maybeUserNextConfigObject)) { - return maybeUserNextConfigObject.then(function (userNextConfigObject: NextConfigObjectWithSentry) { - const userSentryOptions = { ...userNextConfigObject.sentry, ...sentryOptions }; - return getFinalConfigObject(userNextConfigObject, userSentryOptions, userSentryWebpackPluginOptions); + if (isThenable(maybePromiseNextConfig)) { + return maybePromiseNextConfig.then(promiseResultNextConfig => { + return getFinalConfigObject(promiseResultNextConfig, sentryBuildtimeOptions, sentryWebpackPluginOptions); }); } - // Reassign for naming-consistency sake. - const userNextConfigObject = maybeUserNextConfigObject; - const userSentryOptions = { ...userNextConfigObject.sentry, ...sentryOptions }; - return getFinalConfigObject(userNextConfigObject, userSentryOptions, userSentryWebpackPluginOptions); + return getFinalConfigObject(maybePromiseNextConfig, sentryBuildtimeOptions, sentryWebpackPluginOptions); }; } else { - const userSentryOptions = { ...exportedUserNextConfig.sentry, ...sentryOptions }; - return getFinalConfigObject(exportedUserNextConfig, userSentryOptions, userSentryWebpackPluginOptions); + return getFinalConfigObject(nextConfig, sentryBuildtimeOptions, sentryWebpackPluginOptions); } } // Modify the materialized object form of the user's next config by deleting the `sentry` property and wrapping the // `webpack` property function getFinalConfigObject( - incomingUserNextConfigObject: NextConfigObjectWithSentry, - userSentryOptions: UserSentryOptions, + incomingUserNextConfigObject: NextConfigObject, + userSentryOptions: SentryBuildtimeOptions, userSentryWebpackPluginOptions: Partial, ): NextConfigObject { - // Next 12.2.3+ warns about non-canonical properties on `userNextConfig`. - delete incomingUserNextConfigObject.sentry; + if ('sentry' in incomingUserNextConfigObject) { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/nextjs] Setting a `sentry` property on the Next.js config is no longer supported. Please use the `sentrySDKOptions` argument of `withSentryConfig` instead.', + ); + + // Next 12.2.3+ warns about non-canonical properties on `userNextConfig`. + delete incomingUserNextConfigObject.sentry; + } if (userSentryOptions?.tunnelRoute) { if (incomingUserNextConfigObject.output === 'export') { diff --git a/packages/nextjs/test/config/fixtures.ts b/packages/nextjs/test/config/fixtures.ts index 69f15a18f088..7da47e37be33 100644 --- a/packages/nextjs/test/config/fixtures.ts +++ b/packages/nextjs/test/config/fixtures.ts @@ -3,7 +3,6 @@ import type { EntryPropertyFunction, ExportedNextConfig, NextConfigObject, - NextConfigObjectWithSentry, WebpackConfigObject, } from '../../src/config/types'; @@ -26,7 +25,7 @@ export const userNextConfig: NextConfigObject = { }; /** Mocks of the arguments passed to `withSentryConfig` */ -export const exportedNextConfig = userNextConfig as NextConfigObjectWithSentry; +export const exportedNextConfig = userNextConfig; export const userSentryWebpackPluginConfig = { org: 'squirrelChasers', project: 'simulator' }; process.env.SENTRY_AUTH_TOKEN = 'dogsarebadatkeepingsecrets'; process.env.SENTRY_RELEASE = 'doGsaREgReaT'; diff --git a/packages/nextjs/test/config/testUtils.ts b/packages/nextjs/test/config/testUtils.ts index c9bed5dbe358..3b4062083f46 100644 --- a/packages/nextjs/test/config/testUtils.ts +++ b/packages/nextjs/test/config/testUtils.ts @@ -6,6 +6,7 @@ import type { EntryPropertyFunction, ExportedNextConfig, NextConfigObject, + SentryBuildtimeOptions, SentryWebpackPluginOptions, WebpackConfigObject, WebpackConfigObjectWithModuleRules, @@ -28,8 +29,9 @@ export function materializeFinalNextConfig( exportedNextConfig: ExportedNextConfig, userSentryWebpackPluginConfig?: Partial, runtimePhase?: string, + sentryBuildTimeOptions?: SentryBuildtimeOptions, ): NextConfigObject { - const sentrifiedConfig = withSentryConfig(exportedNextConfig, userSentryWebpackPluginConfig); + const sentrifiedConfig = withSentryConfig(exportedNextConfig, userSentryWebpackPluginConfig, sentryBuildTimeOptions); let finalConfigValues = sentrifiedConfig; if (typeof sentrifiedConfig === 'function') { @@ -59,6 +61,7 @@ export async function materializeFinalWebpackConfig(options: { userSentryWebpackPluginConfig?: Partial; incomingWebpackConfig: WebpackConfigObject; incomingWebpackBuildContext: BuildContext; + sentryBuildTimeOptions?: SentryBuildtimeOptions; }): Promise { const { exportedNextConfig, userSentryWebpackPluginConfig, incomingWebpackConfig, incomingWebpackBuildContext } = options; @@ -69,15 +72,11 @@ export async function materializeFinalWebpackConfig(options: { ? await exportedNextConfig('phase-production-build', defaultsObject) : exportedNextConfig; - // extract the `sentry` property as we do in `withSentryConfig` - const { sentry: sentryConfig } = materializedUserNextConfig; - delete materializedUserNextConfig.sentry; - // get the webpack config function we'd normally pass back to next const webpackConfigFunction = constructWebpackConfigFunction( materializedUserNextConfig, userSentryWebpackPluginConfig, - sentryConfig, + options.sentryBuildTimeOptions ?? {}, ); // call it to get concrete values for comparison diff --git a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts index b70e6d3d642e..ed101058a57e 100644 --- a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts +++ b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts @@ -48,36 +48,38 @@ describe('constructWebpackConfigFunction()', () => { }); it("doesn't set devtool if webpack plugin is disabled", () => { - const finalNextConfig = materializeFinalNextConfig({ - ...exportedNextConfig, - webpack: () => - ({ - ...serverWebpackConfig, - devtool: 'something-besides-source-map', - }) as any, - sentry: { disableServerWebpackPlugin: true }, - }); + const finalNextConfig = materializeFinalNextConfig( + { + ...exportedNextConfig, + webpack: () => + ({ + ...serverWebpackConfig, + devtool: 'something-besides-source-map', + }) as any, + }, + undefined, + undefined, + { disableServerWebpackPlugin: true }, + ); + const finalWebpackConfig = finalNextConfig.webpack?.(serverWebpackConfig, serverBuildContext); expect(finalWebpackConfig?.devtool).not.toEqual('source-map'); }); it('allows for the use of `hidden-source-map` as `devtool` value for client-side builds', async () => { - const exportedNextConfigHiddenSourceMaps = { - ...exportedNextConfig, - sentry: { ...exportedNextConfig.sentry, hideSourceMaps: true }, - }; - const finalClientWebpackConfig = await materializeFinalWebpackConfig({ - exportedNextConfig: exportedNextConfigHiddenSourceMaps, + exportedNextConfig: exportedNextConfig, incomingWebpackConfig: clientWebpackConfig, incomingWebpackBuildContext: clientBuildContext, + sentryBuildTimeOptions: { hideSourceMaps: true }, }); const finalServerWebpackConfig = await materializeFinalWebpackConfig({ - exportedNextConfig: exportedNextConfigHiddenSourceMaps, + exportedNextConfig: exportedNextConfig, incomingWebpackConfig: serverWebpackConfig, incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: { hideSourceMaps: true }, }); expect(finalClientWebpackConfig.devtool).toEqual('hidden-source-map'); diff --git a/packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts b/packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts index ddcc8965a6c1..f75aae23046b 100644 --- a/packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts +++ b/packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts @@ -3,7 +3,7 @@ import * as os from 'os'; import * as path from 'path'; import { default as SentryWebpackPlugin } from '@sentry/webpack-plugin'; -import type { BuildContext, ExportedNextConfig } from '../../../src/config/types'; +import type { BuildContext, ExportedNextConfig, SentryBuildtimeOptions } from '../../../src/config/types'; import { getUserConfigFile, getWebpackPluginOptions } from '../../../src/config/webpack'; import { clientBuildContext, @@ -83,13 +83,14 @@ describe('Sentry webpack plugin config', () => { }); it('has the correct value when building client bundles using `widenClientFileUpload` option', async () => { - const exportedNextConfigWithWidening = { ...exportedNextConfig, sentry: { widenClientFileUpload: true } }; + const exportedNextConfigWithWidening = { ...exportedNextConfig }; const buildContext = getBuildContext('client', exportedNextConfigWithWidening); const finalWebpackConfig = await materializeFinalWebpackConfig({ exportedNextConfig: exportedNextConfigWithWidening, incomingWebpackConfig: clientWebpackConfig, incomingWebpackBuildContext: buildContext, + sentryBuildTimeOptions: { widenClientFileUpload: true }, }); const sentryWebpackPluginInstance = findWebpackPlugin( @@ -180,11 +181,12 @@ describe('Sentry webpack plugin config', () => { }); it('has the correct value when building client bundles using `widenClientFileUpload` option', async () => { - const exportedNextConfigWithWidening = { ...exportedNextConfig, sentry: { widenClientFileUpload: true } }; + const exportedNextConfigWithWidening = { ...exportedNextConfig }; const finalWebpackConfig = await materializeFinalWebpackConfig({ exportedNextConfig: exportedNextConfigWithWidening, incomingWebpackConfig: clientWebpackConfig, incomingWebpackBuildContext: getBuildContext('client', exportedNextConfigWithWidening), + sentryBuildTimeOptions: { widenClientFileUpload: true }, }); const sentryWebpackPluginInstance = findWebpackPlugin( @@ -219,8 +221,8 @@ describe('Sentry webpack plugin config', () => { 'obeys `disableClientWebpackPlugin = true`', { ...exportedNextConfig, - sentry: { disableClientWebpackPlugin: true }, }, + { disableClientWebpackPlugin: true }, {}, true, false, @@ -230,8 +232,8 @@ describe('Sentry webpack plugin config', () => { 'obeys `disableServerWebpackPlugin = true`', { ...exportedNextConfig, - sentry: { disableServerWebpackPlugin: true }, }, + { disableServerWebpackPlugin: true }, {}, false, true, @@ -241,6 +243,7 @@ describe('Sentry webpack plugin config', () => { async ( _testName: string, exportedNextConfig: ExportedNextConfig, + buildTimeOptions: SentryBuildtimeOptions, extraEnvValues: Record, shouldFindServerPlugin: boolean, shouldFindClientPlugin: boolean, @@ -254,6 +257,7 @@ describe('Sentry webpack plugin config', () => { userSentryWebpackPluginConfig, incomingWebpackConfig: serverWebpackConfig, incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: buildTimeOptions, }); const clientFinalWebpackConfig = await materializeFinalWebpackConfig({ @@ -261,6 +265,7 @@ describe('Sentry webpack plugin config', () => { userSentryWebpackPluginConfig, incomingWebpackConfig: clientWebpackConfig, incomingWebpackBuildContext: clientBuildContext, + sentryBuildTimeOptions: buildTimeOptions, }); const genericSentryWebpackPluginInstance = expect.any(SentryWebpackPlugin); diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index 4ed6f2d40770..e457174f5fa9 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -49,14 +49,4 @@ describe('withSentryConfig', () => { expect(exportedNextConfigFunction).toHaveBeenCalledWith(defaultRuntimePhase, defaultsObject); }); - - it('removes `sentry` property', () => { - // It's unclear why we need this cast - - const finalConfig = materializeFinalNextConfig({ ...exportedNextConfig, sentry: {} }); - // const finalConfig = materializeFinalNextConfig({ ...exportedNextConfig, sentry: {} } as ExportedNextConfig); - - // We have to check using `in` because TS knows it shouldn't be there and throws a type error if we try to access it - // directly - expect('sentry' in finalConfig).toBe(false); - }); });