Skip to content

Commit

Permalink
feat(nextjs): Auto-include pages/_error.js (#140)
Browse files Browse the repository at this point in the history
This adds another file to be copied into the user's project when they run the nextjs version of the wizard, specifically the `_error.js` page which the vercel folks include in their Sentry example app[1] in order to catch more errors. As they say in the comments of that file:

```
  // Next.js will pass an err on the server if a page's data fetching methods
  // threw or returned a Promise that rejected
  //
  // Running on the client (browser), Next.js will provide an err if:
  //
  //  - a page's `getInitialProps` threw or returned a Promise that rejected
  //  - an exception was thrown somewhere in the React lifecycle (render,
  //    componentDidMount, etc) that was caught by Next.js's React Error
  //    Boundary. Read more about what types of exceptions are caught by Error
  //    Boundaries: https://reactjs.org/docs/error-boundaries.html
```

Given that `_error.js` doesn't go at the root level of the project the way the others do, and given that users can either store their page files in `pages` or `src/pages`, I had to add some logic for computing the destination of each copied file. Also, since `_error.js` already starts with an underscore, I felt like adding an underscore to the front of our copy of a file in cases where the file already exists wasn't a great scheme anymore, so I changed to adding `wizardcopy` just before the file's extension (so, for example, if `next.config.js` already exists, we'll now create `next.config.wizardcopy.js`). (This has the added advantage that the real file and our copy now alphabetize right next to one another, so it's dead simple to find them for merging.)

This change is added to the manual setup page in docs in getsentry/sentry-docs#4286.

[1] https://github.com/vercel/next.js/blob/canary/examples/with-sentry/pages/_error.js
  • Loading branch information
lobsterkatie authored Oct 20, 2021
1 parent 2cde774 commit 1e9faf0
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 19 deletions.
90 changes: 71 additions & 19 deletions lib/Steps/Integrations/NextJs.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-lines */
import * as fs from 'fs';
import { Answers, prompt } from 'inquirer';
import * as _ from 'lodash';
Expand All @@ -14,7 +15,16 @@ const PROPERTIES_FILENAME = 'sentry.properties';
const SENTRYCLIRC_FILENAME = '.sentryclirc';
const GITIGNORE_FILENAME = '.gitignore';
const CONFIG_DIR = 'configs/';
const MERGEABLE_CONFIG_PREFIX = '_';
const MERGEABLE_CONFIG_INFIX = 'wizardcopy';

// for those files which can go in more than one place, the list of places they
// could go (the first one which works will be used)
const TEMPLATE_DESTINATIONS: { [key: string]: string[] } = {
'_error.js': ['pages', 'src/pages'],
'next.config.js': ['.'],
'sentry.server.config.js': ['.'],
'sentry.client.config.js': ['.'],
};

let appPackage: any = {};

Expand Down Expand Up @@ -184,7 +194,12 @@ export class NextJs extends BaseIntegration {
private _createNextConfig(configDirectory: string, dsn: any): void {
const templates = fs.readdirSync(configDirectory);
for (const template of templates) {
this._setTemplate(configDirectory, template, dsn);
this._setTemplate(
configDirectory,
template,
TEMPLATE_DESTINATIONS[template],
dsn,
);
}
red(
'⚠ Performance monitoring is enabled capturing 100% of transactions.\n' +
Expand All @@ -195,27 +210,54 @@ export class NextJs extends BaseIntegration {

private _setTemplate(
configDirectory: string,
template: string,
templateFile: string,
destinationOptions: string[],
dsn: string,
): void {
const templatePath = path.join(configDirectory, template);
const mergeableFile = MERGEABLE_CONFIG_PREFIX + template;
if (!fs.existsSync(template)) {
this._fillAndCopyTemplate(templatePath, template, dsn);
} else if (!fs.existsSync(mergeableFile)) {
this._fillAndCopyTemplate(templatePath, mergeableFile, dsn);
red(
`File ${template} already exists, so created ${mergeableFile}.\n` +
`Please, merge those files.`,
);
nl();
} else {
red(
`File ${template} already exists, and ${mergeableFile} also exists.\n` +
'Please, merge those files.',
const templatePath = path.join(configDirectory, templateFile);

for (const destinationDir of destinationOptions) {
if (!fs.existsSync(destinationDir)) {
continue;
}

const destinationPath = path.join(destinationDir, templateFile);
// in case the file in question already exists, we'll make a copy with
// `MERGEABLE_CONFIG_INFIX` inserted just before the extension, so as not
// to overwrite the existing file
const mergeableFilePath = path.join(
destinationDir,
this._spliceInPlace(
templateFile.split('.'),
-1,
0,
MERGEABLE_CONFIG_INFIX,
).join('.'),
);
nl();

if (!fs.existsSync(destinationPath)) {
this._fillAndCopyTemplate(templatePath, destinationPath, dsn);
} else if (!fs.existsSync(mergeableFilePath)) {
this._fillAndCopyTemplate(templatePath, mergeableFilePath, dsn);
red(
`File \`${templateFile}\` already exists, so created \`${mergeableFilePath}\`.\n` +
'Please merge those files.',
);
nl();
} else {
red(
`Both \`${templateFile}\` and \`${mergeableFilePath}\` already exist.\n` +
'Please merge those files.',
);
nl();
}
return;
}

red(
`Could not find appropriate destination for \`${templateFile}\`. Tried: ${destinationOptions}.`,
);
nl();
}

private _fillAndCopyTemplate(
Expand Down Expand Up @@ -287,4 +329,14 @@ export class NextJs extends BaseIntegration {
}
return satisfies(minUserVersion, minVersionRange);
}

private _spliceInPlace(
arr: Array<any>,
start: number,
deleteCount: number,
...inserts: any[]
): Array<any> {
arr.splice(start, deleteCount, ...inserts);
return arr;
}
}
61 changes: 61 additions & 0 deletions scripts/NextJs/configs/_error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import NextErrorComponent from 'next/error';

import * as Sentry from '@sentry/nextjs';

const MyError = ({ statusCode, hasGetInitialPropsRun, err }) => {
if (!hasGetInitialPropsRun && err) {
// getInitialProps is not called in case of
// https://github.com/vercel/next.js/issues/8592. As a workaround, we pass
// err via _app.js so it can be captured
Sentry.captureException(err);
// Flushing is not required in this case as it only happens on the client
}

return <NextErrorComponent statusCode={statusCode} />;
};

MyError.getInitialProps = async ({ res, err, asPath }) => {
const errorInitialProps = await NextErrorComponent.getInitialProps({
res,
err,
});

// Workaround for https://github.com/vercel/next.js/issues/8592, mark when
// getInitialProps has run
errorInitialProps.hasGetInitialPropsRun = true;

// Running on the server, the response object (`res`) is available.
//
// Next.js will pass an err on the server if a page's data fetching methods
// threw or returned a Promise that rejected
//
// Running on the client (browser), Next.js will provide an err if:
//
// - a page's `getInitialProps` threw or returned a Promise that rejected
// - an exception was thrown somewhere in the React lifecycle (render,
// componentDidMount, etc) that was caught by Next.js's React Error
// Boundary. Read more about what types of exceptions are caught by Error
// Boundaries: https://reactjs.org/docs/error-boundaries.html

if (err) {
Sentry.captureException(err);

// Flushing before returning is necessary if deploying to Vercel, see
// https://vercel.com/docs/platform/limits#streaming-responses
await Sentry.flush(2000);

return errorInitialProps;
}

// If this point is reached, getInitialProps was called without any
// information about what the error might be. This is unexpected and may
// indicate a bug introduced in Next.js, so record it in Sentry
Sentry.captureException(
new Error(`_error.js getInitialProps missing data at path: ${asPath}`),
);
await Sentry.flush(2000);

return errorInitialProps;
};

export default MyError;

0 comments on commit 1e9faf0

Please sign in to comment.