diff --git a/CHANGELOG.md b/CHANGELOG.md index 234f932c..b6dbae85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- feat(nextjs): Adjust Next.js wizard for usage with v8 SDK (#567) + ## 3.22.1 - fix(wizard): Handle missing auth token in wizard API endpoint response (#566) @@ -10,11 +14,14 @@ ## 3.21.0 -- feat(nextjs): Add comment to add spotlight in Sentry.init for Next.js server config (#545) +- feat(nextjs): Add comment to add spotlight in Sentry.init for Next.js server + config (#545) - feat(nextjs): Pin installed Next.js SDK version to version 7 (#550) - feat(remix): Add example page (#542) -- feat(sveltekit): Add comment for spotlight in Sentry.init for SvelteKit server hooks config (#546) -- ref(nextjs): Add note about `tunnelRoute` and Next.js middleware incompatibility (#544) +- feat(sveltekit): Add comment for spotlight in Sentry.init for SvelteKit server + hooks config (#546) +- ref(nextjs): Add note about `tunnelRoute` and Next.js middleware + incompatibility (#544) - ref(remix): Remove Vite dev-mode modification step (#543) ## 3.20.5 diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index 49809f00..2061e66c 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -12,25 +12,27 @@ import * as Sentry from '@sentry/node'; import { abort, abortIfCancelled, - addSentryCliConfig, + addDotEnvSentryBuildPluginFile, askShouldCreateExamplePage, confirmContinueIfNoOrDirtyGitRepo, + createNewConfigFile, ensurePackageIsInstalled, getOrAskForProjectData, getPackageDotJson, installPackage, isUsingTypeScript, printWelcome, + showCopyPasteInstructions, } from '../utils/clack-utils'; import { SentryProjectData, WizardOptions } from '../utils/types'; import { getFullUnderscoreErrorCopyPasteSnippet, getGlobalErrorCopyPasteSnippet, + getInstrumentationHookContent, + getInstrumentationHookCopyPasteSnippet, getNextjsConfigCjsAppendix, getNextjsConfigCjsTemplate, getNextjsConfigEsmCopyPasteSnippet, - getNextjsSentryBuildOptionsTemplate, - getNextjsWebpackPluginOptionsTemplate, getSentryConfigContents, getSentryDefaultGlobalErrorPage, getSentryDefaultUnderscoreErrorPage, @@ -38,6 +40,7 @@ import { getSentryExampleAppDirApiRoute, getSentryExamplePageContents, getSimpleUnderscoreErrorCopyPasteSnippet, + getWithSentryConfigOptionsTemplate, } from './templates'; import { traceStep, withTelemetry } from '../telemetry'; import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; @@ -82,7 +85,7 @@ export async function runNextjsWizardWithTelemetry( Sentry.setTag('sdk-already-installed', sdkAlreadyInstalled); await installPackage({ - packageName: '@sentry/nextjs@^7.105.0', + packageName: '@sentry/nextjs@^8', alreadyInstalled: !!packageJson?.dependencies?.['@sentry/nextjs'], }); @@ -248,7 +251,7 @@ export async function runNextjsWizardWithTelemetry( clack.log.info( `It seems like you already have a custom error page for your app directory.\n\nPlease add the following code to your custom error page\nat ${chalk.cyan( path.join(...appDirLocation, globalErrorPageFile), - )}:`, + )}:\n`, ); // eslint-disable-next-line no-console @@ -282,7 +285,7 @@ export async function runNextjsWizardWithTelemetry( ); } - await addSentryCliConfig({ authToken }); + await addDotEnvSentryBuildPluginFile(authToken); const mightBeUsingVercel = fs.existsSync( path.join(process.cwd(), 'vercel.json'), @@ -390,23 +393,75 @@ async function createOrMergeNextJsFiles( }); } - const sentryWebpackOptionsTemplate = getNextjsWebpackPluginOptionsTemplate( - selectedProject.organization.slug, - selectedProject.slug, - selfHosted, - sentryUrl, - ); + await traceStep('setup-instrumentation-hook', async () => { + const srcInstrumentationTsExists = fs.existsSync( + path.join(process.cwd(), 'src', 'instrumentation.ts'), + ); + const srcInstrumentationJsExists = fs.existsSync( + path.join(process.cwd(), 'src', 'instrumentation.js'), + ); + const instrumentationTsExists = fs.existsSync( + path.join(process.cwd(), 'instrumentation.ts'), + ); + const instrumentationJsExists = fs.existsSync( + path.join(process.cwd(), 'instrumentation.js'), + ); + + let instrumentationHookLocation: 'src' | 'root' | 'does-not-exist'; + if (srcInstrumentationTsExists || srcInstrumentationJsExists) { + instrumentationHookLocation = 'src'; + } else if (instrumentationTsExists || instrumentationJsExists) { + instrumentationHookLocation = 'root'; + } else { + instrumentationHookLocation = 'does-not-exist'; + } - const { tunnelRoute } = sdkConfigOptions; + if (instrumentationHookLocation === 'does-not-exist') { + const srcFolderExists = fs.existsSync(path.join(process.cwd(), 'src')); - const sentryBuildOptionsTemplate = getNextjsSentryBuildOptionsTemplate({ - tunnelRoute, - }); + const instrumentationHookPath = srcFolderExists + ? path.join(process.cwd(), 'src', 'instrumentation.ts') + : path.join(process.cwd(), 'instrumentation.ts'); - const nextConfigJs = 'next.config.js'; - const nextConfigMjs = 'next.config.mjs'; + const successfullyCreated = await createNewConfigFile( + instrumentationHookPath, + getInstrumentationHookContent(srcFolderExists ? 'src' : 'root'), + ); + + if (!successfullyCreated) { + await showCopyPasteInstructions( + 'instrumentation.ts', + getInstrumentationHookCopyPasteSnippet( + srcFolderExists ? 'src' : 'root', + ), + ); + } + } else { + await showCopyPasteInstructions( + srcInstrumentationTsExists + ? 'instrumentation.ts' + : srcInstrumentationJsExists + ? 'instrumentation.js' + : instrumentationTsExists + ? 'instrumentation.ts' + : 'instrumentation.js', + getInstrumentationHookCopyPasteSnippet(instrumentationHookLocation), + ); + } + }); await traceStep('setup-next-config', async () => { + const withSentryConfigOptionsTemplate = getWithSentryConfigOptionsTemplate({ + orgSlug: selectedProject.organization.slug, + projectSlug: selectedProject.slug, + selfHosted, + url: sentryUrl, + tunnelRoute: sdkConfigOptions.tunnelRoute, + }); + + const nextConfigJs = 'next.config.js'; + const nextConfigMjs = 'next.config.mjs'; + const nextConfigJsExists = fs.existsSync( path.join(process.cwd(), nextConfigJs), ); @@ -419,10 +474,7 @@ async function createOrMergeNextJsFiles( await fs.promises.writeFile( path.join(process.cwd(), nextConfigJs), - getNextjsConfigCjsTemplate( - sentryWebpackOptionsTemplate, - sentryBuildOptionsTemplate, - ), + getNextjsConfigCjsTemplate(withSentryConfigOptionsTemplate), { encoding: 'utf8', flag: 'w' }, ); @@ -460,10 +512,7 @@ async function createOrMergeNextJsFiles( if (shouldInject) { await fs.promises.appendFile( path.join(process.cwd(), nextConfigJs), - getNextjsConfigCjsAppendix( - sentryWebpackOptionsTemplate, - sentryBuildOptionsTemplate, - ), + getNextjsConfigCjsAppendix(withSentryConfigOptionsTemplate), 'utf8', ); @@ -514,8 +563,7 @@ async function createOrMergeNextJsFiles( // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment mod.exports.default = builders.raw(`withSentryConfig( ${expressionToWrap}, - ${sentryWebpackOptionsTemplate}, - ${sentryBuildOptionsTemplate} + ${withSentryConfigOptionsTemplate} )`); const newCode = mod.generate().code; @@ -550,10 +598,7 @@ async function createOrMergeNextJsFiles( // eslint-disable-next-line no-console console.log( - getNextjsConfigEsmCopyPasteSnippet( - sentryWebpackOptionsTemplate, - sentryBuildOptionsTemplate, - ), + getNextjsConfigEsmCopyPasteSnippet(withSentryConfigOptionsTemplate), ); const shouldContinue = await abortIfCancelled( diff --git a/src/nextjs/templates.ts b/src/nextjs/templates.ts index 8c8e142e..0cec70ec 100644 --- a/src/nextjs/templates.ts +++ b/src/nextjs/templates.ts @@ -1,30 +1,31 @@ import chalk from 'chalk'; +import { makeCodeSnippet } from '../utils/clack-utils'; -export function getNextjsWebpackPluginOptionsTemplate( - orgSlug: string, - projectSlug: string, - selfHosted: boolean, - url: string, -): string { +type WithSentryConfigOptions = { + orgSlug: string; + projectSlug: string; + selfHosted: boolean; + url: string; + tunnelRoute: boolean; +}; + +export function getWithSentryConfigOptionsTemplate({ + orgSlug, + projectSlug, + selfHosted, + tunnelRoute, + url, +}: WithSentryConfigOptions): string { return `{ // For all available options, see: // https://github.com/getsentry/sentry-webpack-plugin#options - // Suppresses source map uploading logs during build - silent: true, org: "${orgSlug}", project: "${projectSlug}",${selfHosted ? `\n url: "${url}"` : ''} - }`; -} -type SentryNextjsBuildOptions = { - tunnelRoute: boolean; -}; + // Only print logs for uploading source maps in CI + silent: !process.env.CI, -export function getNextjsSentryBuildOptionsTemplate({ - tunnelRoute, -}: SentryNextjsBuildOptions): string { - return `{ // For all available options, see: // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ @@ -48,7 +49,7 @@ export function getNextjsSentryBuildOptionsTemplate({ // Automatically tree-shake Sentry logger statements to reduce bundle size disableLogger: true, - // Enables automatic instrumentation of Vercel Cron Monitors. + // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) // See the following for more information: // https://docs.sentry.io/product/crons/ // https://vercel.com/docs/cron-jobs @@ -57,8 +58,7 @@ export function getNextjsSentryBuildOptionsTemplate({ } export function getNextjsConfigCjsTemplate( - sentryWebpackPluginOptionsTemplate: string, - sentryBuildOptionsTemplate: string, + withSentryConfigOptionsTemplate: string, ): string { return `const { withSentryConfig } = require("@sentry/nextjs"); @@ -67,15 +67,13 @@ const nextConfig = {}; module.exports = withSentryConfig( nextConfig, - ${sentryWebpackPluginOptionsTemplate}, - ${sentryBuildOptionsTemplate} + ${withSentryConfigOptionsTemplate} ); `; } export function getNextjsConfigCjsAppendix( - sentryWebpackPluginOptionsTemplate: string, - sentryBuildOptionsTemplate: string, + withSentryConfigOptionsTemplate: string, ): string { return ` @@ -85,15 +83,13 @@ const { withSentryConfig } = require("@sentry/nextjs"); module.exports = withSentryConfig( module.exports, - ${sentryWebpackPluginOptionsTemplate}, - ${sentryBuildOptionsTemplate} + ${withSentryConfigOptionsTemplate} ); `; } export function getNextjsConfigEsmCopyPasteSnippet( - sentryWebpackPluginOptionsTemplate: string, - sentryBuildOptionsTemplate: string, + withSentryConfigOptionsTemplate: string, ): string { return ` @@ -102,8 +98,7 @@ import { withSentryConfig } from "@sentry/nextjs"; export default withSentryConfig( yourNextConfig, - ${sentryWebpackPluginOptionsTemplate}, - ${sentryBuildOptionsTemplate} + ${withSentryConfigOptionsTemplate} ); `; } @@ -152,7 +147,7 @@ export function getSentryConfigContents( if (config === 'server') { spotlightOption = ` - // uncomment the line below to enable Spotlight (https://spotlightjs.com) + // Uncomment the line below to enable Spotlight (https://spotlightjs.com) // spotlight: process.env.NODE_ENV === 'development', `; } @@ -344,6 +339,45 @@ YourCustomErrorComponent.getInitialProps = async (contextData${ `; } +export function getInstrumentationHookContent( + instrumentationHookLocation: 'src' | 'root', +) { + return `export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + await import('${ + instrumentationHookLocation === 'root' ? '.' : '..' + }/sentry.server.config'); + } + + if (process.env.NEXT_RUNTIME === 'edge') { + await import('${ + instrumentationHookLocation === 'root' ? '.' : '..' + }/sentry.edge.config'); + } +} +`; +} + +export function getInstrumentationHookCopyPasteSnippet( + instrumentationHookLocation: 'src' | 'root', +) { + return makeCodeSnippet(true, (unchanged, plus) => { + return unchanged(`export ${plus('async')} function register() { + ${plus(`if (process.env.NEXT_RUNTIME === 'nodejs') { + await import('${ + instrumentationHookLocation === 'root' ? '.' : '..' + }/sentry.server.config'); + } + + if (process.env.NEXT_RUNTIME === 'edge') { + await import('${ + instrumentationHookLocation === 'root' ? '.' : '..' + }/sentry.edge.config'); + }`)} +}`); + }); +} + export function getSentryDefaultGlobalErrorPage() { return `"use client"; diff --git a/src/sourcemaps/tools/nextjs.ts b/src/sourcemaps/tools/nextjs.ts index cecb0044..bffb4101 100644 --- a/src/sourcemaps/tools/nextjs.ts +++ b/src/sourcemaps/tools/nextjs.ts @@ -3,7 +3,10 @@ import * as clack from '@clack/prompts'; import chalk from 'chalk'; import { runNextjsWizard } from '../../nextjs/nextjs-wizard'; import { traceStep } from '../../telemetry'; -import { abortIfCancelled, addSentryCliConfig } from '../../utils/clack-utils'; +import { + abortIfCancelled, + addDotEnvSentryBuildPluginFile, +} from '../../utils/clack-utils'; import { WizardOptions } from '../../utils/types'; import { SourceMapUploadToolConfigurationOptions } from './types'; @@ -99,7 +102,7 @@ In case you already tried the wizard, we can also show you how to configure your ); await traceStep('nextjs-manual-sentryclirc', () => - addSentryCliConfig({ authToken: options.authToken }), + addDotEnvSentryBuildPluginFile(options.authToken), ); } diff --git a/src/utils/clack-utils.ts b/src/utils/clack-utils.ts index 5ce191d3..1128b973 100644 --- a/src/utils/clack-utils.ts +++ b/src/utils/clack-utils.ts @@ -800,7 +800,7 @@ ${chalk.cyan('https://github.com/getsentry/sentry-wizard/issues')}`); clack.log.info(`In the meantime, we'll add a dummy auth token (${chalk.cyan( `"${DUMMY_AUTH_TOKEN}"`, )}) for you to replace later. -Create your auth token here: +Create your auth token here: ${chalk.cyan( selfHosted ? `${sentryUrl}organizations/${selectedProject.organization.slug}/settings/auth-tokens`