From a1f5368ad4e2c123eab2b334445eff25f7f6cb86 Mon Sep 17 00:00:00 2001
From: Luca Forstner <luca.forstner@sentry.io>
Date: Mon, 13 Nov 2023 16:25:35 +0100
Subject: [PATCH] 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 <Error statusCode={props.statusCode} />;
+};
+
+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);
+};
+`;
+}