diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts index d2bca652..b3508317 100644 --- a/src/nextjs/nextjs-wizard.ts +++ b/src/nextjs/nextjs-wizard.ts @@ -23,15 +23,18 @@ import { } from '../utils/clack-utils'; import { SentryProjectData, WizardOptions } from '../utils/types'; import { + getFullUnderscoreErrorCopyPasteSnippet, getNextjsConfigCjsAppendix, getNextjsConfigCjsTemplate, getNextjsConfigEsmCopyPasteSnippet, getNextjsSentryBuildOptionsTemplate, getNextjsWebpackPluginOptionsTemplate, getSentryConfigContents, + getSentryDefaultUnderscoreErrorPage, getSentryExampleApiRoute, getSentryExampleAppDirApiRoute, getSentryExamplePageContents, + getSimpleUnderscoreErrorCopyPasteSnippet, } from './templates'; import { traceStep, withTelemetry } from '../telemetry'; import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; @@ -83,6 +86,109 @@ export async function runNextjsWizardWithTelemetry( createOrMergeNextJsFiles(selectedProject, selfHosted, sentryUrl), ); + await traceStep('create-underscoreerror-page', async () => { + const srcDir = path.join(process.cwd(), 'src'); + const maybePagesDirPath = path.join(process.cwd(), 'pages'); + const maybeSrcPagesDirPath = path.join(srcDir, 'pages'); + + const pagesLocation = + fs.existsSync(maybePagesDirPath) && + fs.lstatSync(maybePagesDirPath).isDirectory() + ? ['pages'] + : fs.existsSync(maybeSrcPagesDirPath) && + fs.lstatSync(maybeSrcPagesDirPath).isDirectory() + ? ['src', 'pages'] + : undefined; + + if (!pagesLocation) { + return; + } + + const underscoreErrorPageFile = fs.existsSync( + path.join(process.cwd(), ...pagesLocation, '_error.tsx'), + ) + ? '_error.tsx' + : fs.existsSync(path.join(process.cwd(), ...pagesLocation, '_error.ts')) + ? '_error.ts' + : fs.existsSync(path.join(process.cwd(), ...pagesLocation, '_error.jsx')) + ? '_error.jsx' + : fs.existsSync(path.join(process.cwd(), ...pagesLocation, '_error.js')) + ? '_error.js' + : undefined; + + if (!underscoreErrorPageFile) { + await fs.promises.writeFile( + path.join(process.cwd(), ...pagesLocation, '_error.jsx'), + getSentryDefaultUnderscoreErrorPage(), + { encoding: 'utf8', flag: 'w' }, + ); + + clack.log.success( + `Created ${chalk.bold(path.join(...pagesLocation, '_error.jsx'))}.`, + ); + } else if ( + fs + .readFileSync( + path.join(process.cwd(), ...pagesLocation, underscoreErrorPageFile), + 'utf8', + ) + .includes('getInitialProps') + ) { + clack.log.info( + `It seems like you already have a custom error page.\n\nPlease put the following function call in the ${chalk.bold( + 'getInitialProps', + )}\nmethod of your custom error page at ${chalk.bold( + path.join(...pagesLocation, underscoreErrorPageFile), + )}:`, + ); + + // eslint-disable-next-line no-console + console.log(getSimpleUnderscoreErrorCopyPasteSnippet()); + + const shouldContinue = await abortIfCancelled( + clack.confirm({ + message: `Did you modify your ${chalk.bold( + path.join(...pagesLocation, underscoreErrorPageFile), + )} file as described above?`, + active: 'Yes', + inactive: 'No, get me out of here', + }), + ); + + if (!shouldContinue) { + await abort(); + } + } else { + clack.log.info( + `It seems like you already have a custom error page.\n\nPlease add the following code to your custom error page\nat ${chalk.bold( + path.join(...pagesLocation, underscoreErrorPageFile), + )}:`, + ); + + // eslint-disable-next-line no-console + console.log( + getFullUnderscoreErrorCopyPasteSnippet( + underscoreErrorPageFile === '_error.ts' || + underscoreErrorPageFile === '_error.tsx', + ), + ); + + const shouldContinue = await abortIfCancelled( + clack.confirm({ + message: `Did add the code to your ${chalk.bold( + path.join(...pagesLocation, underscoreErrorPageFile), + )} file as described above?`, + active: 'Yes', + inactive: 'No, get me out of here', + }), + ); + + if (!shouldContinue) { + await abort(); + } + } + }); + await traceStep('create-example-page', async () => createExamplePage(selfHosted, selectedProject, sentryUrl), ); diff --git a/src/nextjs/templates.ts b/src/nextjs/templates.ts index aa6196b4..2d1b1138 100644 --- a/src/nextjs/templates.ts +++ b/src/nextjs/templates.ts @@ -1,3 +1,5 @@ +import chalk from 'chalk'; + export function getNextjsWebpackPluginOptionsTemplate( orgSlug: string, projectSlug: string, @@ -267,3 +269,58 @@ export function GET() { } `; } + +export function getSentryDefaultUnderscoreErrorPage() { + return `import * as Sentry from "@sentry/nextjs"; +import Error from "next/error"; + +const CustomErrorComponent = (props) => { + return ; +}; + +CustomErrorComponent.getInitialProps = async (contextData) => { + // In case this is running in a serverless function, await this in order to give Sentry + // time to send the error before the lambda exits + await Sentry.captureUnderscoreErrorException(contextData); + + // This will contain the status code of the response + return Error.getInitialProps(contextData); +}; + +export default CustomErrorComponent; +`; +} + +export function getSimpleUnderscoreErrorCopyPasteSnippet() { + return ` +${chalk.green(`import * as Sentry from '@sentry/nextjs';`)} + +${chalk.dim( + '// Replace "YourCustomErrorComponent" with your custom error component!', +)} +YourCustomErrorComponent.getInitialProps = async (${chalk.green( + `contextData`, + )}) => { + ${chalk.green('await Sentry.captureUnderscoreErrorException(contextData);')} + + ${chalk.dim('// ...other getInitialProps code')} +}; +`; +} + +export function getFullUnderscoreErrorCopyPasteSnippet(isTs: boolean) { + return ` +import * as Sentry from '@sentry/nextjs';${ + isTs ? '\nimport type { NextPageContext } from "next";' : '' + } + +${chalk.dim( + '// Replace "YourCustomErrorComponent" with your custom error component!', +)} +YourCustomErrorComponent.getInitialProps = async (contextData${ + isTs ? ': NextPageContext' : '' + }) => { + await Sentry.captureUnderscoreErrorException(contextData); +}; +`; +}