From a1f5368ad4e2c123eab2b334445eff25f7f6cb86 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 13 Nov 2023 16:25:35 +0100 Subject: [PATCH 1/2] feat(nextjs): Add instructions for custom _error page --- src/nextjs/nextjs-wizard.ts | 106 ++++++++++++++++++++++++++++++++++++ src/nextjs/templates.ts | 57 +++++++++++++++++++ 2 files changed, 163 insertions(+) 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); +}; +`; +} From 964ea87e59ff2663d5e74b4b026dc0f08ed93085 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 13 Nov 2023 16:27:45 +0100 Subject: [PATCH 2/2] changelog --- CHANGELOG.md | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b25d958..b50aed7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ # Changelog +## Unreleased + +- feat(nextjs): Add instructions for custom \_error page (#496) + ## 3.16.3 -- fix(sourcemaps): Re-read package.json when modifying build command #493 +- fix(sourcemaps): Re-read package.json when modifying build command (#493) ## 3.16.2 @@ -15,10 +19,13 @@ ## 3.16.0 -- ref(reactnative): Use clack prompts and share common code (dirty repo, login) (#473) +- ref(reactnative): Use clack prompts and share common code (dirty repo, login) + (#473) - feat(reactnative): Add telemetry (#477) -- feat(reactnative): Improve `build.gradle` patch so that it's more likely to work without changes in monorepos (#478) -- fix(reactnative): Save Sentry URL, Organization and Project to `sentry.properties` (#479) +- feat(reactnative): Improve `build.gradle` patch so that it's more likely to + work without changes in monorepos (#478) +- fix(reactnative): Save Sentry URL, Organization and Project to + `sentry.properties` (#479) ## 3.15.0 @@ -42,14 +49,17 @@ - enh(android): Show link to issues page after setup is complete (#448) - feat(remix): Pass `org`, `project`, `url` to `upload-sourcemaps` script (#434) -- feat(sourcemaps): Automatically enable source maps generation in `tsconfig.json` (#449) +- feat(sourcemaps): Automatically enable source maps generation in + `tsconfig.json` (#449) - feat(sveltekit): Add telemetry collection (#455) - fix(nextjs): Add selfhosted url in `next.config.js` (#438) - fix(nextjs): Create necessary directories in app router (#439) -- fix(sourcemaps): Write package manager command instead of object to package.json (#453) +- fix(sourcemaps): Write package manager command instead of object to + package.json (#453) - ref(sveltekit): Check for minimum supported SvelteKit version (#456) -Work in this release contributed by @andreysam. Thank you for your contributions! +Work in this release contributed by @andreysam. Thank you for your +contributions! ## 3.12.0 @@ -81,16 +91,19 @@ brew install getsentry/tools/sentry-wizard ``` - feat: Add Bun package manager support (#417) -- feat(apple): Add option to choose between cocoapods when available and SPM (#423) +- feat(apple): Add option to choose between cocoapods when available and SPM + (#423) - feat(apple): Search App entry point by build files not directories (#420) - feat(apple): Use ".sentryclirc" for auth instead of hard coding it (#422) - feat(nextjs): Add support for Next.js 13 app router (#385) -- feat(sourcemaps): Provide exit path if there's no need to upload sourcemaps (#415) +- feat(sourcemaps): Provide exit path if there's no need to upload sourcemaps + (#415) - fix: Handle no projects available (#412) - fix: Remove picocolor usage (#426) - fix: Support org auth tokens in old wizards (#409) - fix: Treat user-entered DSN as a public DSN (#410) -- fix(sourcemaps): Enable source map generation when modifying Vite config (#421) +- fix(sourcemaps): Enable source map generation when modifying Vite config + (#421) ## 3.10.0