diff --git a/examples/with-sentry-simple/README.md b/examples/with-sentry-simple/README.md index 93418892fd2e2478d826ce5a758a2ca560758115..eaaa2671c9d27de931ab7b12b102dfd683594726 100644 --- a/examples/with-sentry-simple/README.md +++ b/examples/with-sentry-simple/README.md @@ -49,11 +49,19 @@ now This is a simple example showing how to use [Sentry](https://sentry.io) to catch & report errors on both client + server side. -- `_document.js` is _server-side only_ and is used to change the initial server-side rendered document markup. We listen at the node process level to capture exceptions. -- `_app.js` is client-side only and is used to initialize pages. We use the `componentDidCatch` lifecycle method to catch uncaught exceptions. +- `_app.js` renders on both the server and client. It initializes Sentry to catch any unhandled exceptions +- `_error.js` is rendered by Next.js while handling certain types of exceptions for you. It is overriden so those exceptions can be passed along to Sentry +- `next.config.js` enables source maps in production for Sentry and swaps out `@sentry/node` for `@sentry/browser` when building the client bundle -**Note**: Source maps will not be sent to Sentry when running locally. It's also possible you will see duplicate errors sent when testing -locally due to hot reloading. For a more accurate simulation, please deploy to Now. +**Note**: Source maps will not be sent to Sentry when running locally (because Sentry cannot access your `localhost`). To accurately test client-side source maps, please deploy to Now. + +**Note**: Server-side source maps will not work unless you [manually upload them to Sentry](https://docs.sentry.io/platforms/node/sourcemaps/#making-source-maps-available-to-sentry). + +**Note**: Error handling [works differently in production](https://nextjs.org/docs#custom-error-handling). Some exceptions will not be sent to Sentry in development mode (i.e. `npm run dev`). + +**Note**: The build output will contain warning about unhandled Promise rejections. This caused by the test pages, and is expected. + +**Note**: The version of `@zeit/next-source-maps` (`0.0.4-canary.1`) is important and must be specified since it is not yet the default. Otherwise [source maps will not be generated for the server](https://github.com/zeit/next-plugins/issues/377). ### Configuration @@ -67,4 +75,29 @@ Sentry.init({ }) ``` -_Note: Committing environment variables is not secure and is done here only for demonstration purposes. See the [`with-dotenv`](../with-dotenv) or [`with-now-env`](../with-now-env) for examples of how to set environment variables safely._ +### Disabling Sentry during development + +An easy way to disable Sentry while developing is to set its `enabled` flag based off of the `NODE_ENV` environment variable, which is [properly configured by the `next` subcommands](https://nextjs.org/docs#production-deployment). + +```js +Sentry.init({ + dsn: 'PUT_YOUR_SENTRY_DSN_HERE', + enabled: process.env.NODE_ENV === 'production' +}) +``` + +### Hosting source maps vs. uploading them to Sentry + +This example shows how to generate your own source maps, which are hosted alongside your JavaScript bundles in production. But that has the potential for innaccurate results in Sentry. + +Sentry will attempt to [fetch the source map](https://docs.sentry.io/platforms/javascript/sourcemaps/#hosting--uploading) when it is processing an exception, as long as the "Enable JavaScript source fetching" setting is turned on for your Sentry project. + +However, there are some disadvantages with this approach. Sentry has written a blog post about them here: https://blog.sentry.io/2018/07/17/source-code-fetching + +If you decide that uploading source maps to Sentry would be better, one approach is to define a custom `now-build` script in your `package.json`. Zeit Now's `@now/next` builder will [call this script](https://github.com/zeit/now/blob/canary/packages/now-next/src/index.ts#L270) for you. You can define what to do after a build there: + +``` +"now-build": "next build && node ./post-build.js" +``` + +In `./post-build.js` you can `require('@sentry/cli')` and go through the process of creating a Sentry release and [uploading source maps](https://docs.sentry.io/cli/releases/#sentry-cli-sourcemaps), and optionally deleting the `.js.map` files so they are not made public. diff --git a/examples/with-sentry-simple/next.config.js b/examples/with-sentry-simple/next.config.js index a033fcfdecf48a2077d4696b54191f95a5005852..1d920c7ce9e38b518f846fa1e710ac1d18ee0eb6 100644 --- a/examples/with-sentry-simple/next.config.js +++ b/examples/with-sentry-simple/next.config.js @@ -1,7 +1,25 @@ const withSourceMaps = require('@zeit/next-source-maps')() module.exports = withSourceMaps({ - webpack (config, _options) { + webpack: (config, options) => { + // In `pages/_app.js`, Sentry is imported from @sentry/node. While + // @sentry/browser will run in a Node.js environment, @sentry/node will use + // Node.js-only APIs to catch even more unhandled exceptions. + // + // This works well when Next.js is SSRing your page on a server with + // Node.js, but it is not what we want when your client-side bundle is being + // executed by a browser. + // + // Luckily, Next.js will call this webpack function twice, once for the + // server and once for the client. Read more: + // https://nextjs.org/docs#customizing-webpack-config + // + // So ask Webpack to replace @sentry/node imports with @sentry/browser when + // building the browser's bundle + if (!options.isServer) { + config.resolve.alias['@sentry/node'] = '@sentry/browser' + } + return config } }) diff --git a/examples/with-sentry-simple/package.json b/examples/with-sentry-simple/package.json index 664bbf79077759c0edd94a212dedffb063577443..ed32fcf41bbcc0473b1b2485b9c924fe7acc814a 100644 --- a/examples/with-sentry-simple/package.json +++ b/examples/with-sentry-simple/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@sentry/browser": "^5.1.0", + "@sentry/node": "^5.6.2", "next": "latest", "react": "^16.8.6", "react-dom": "^16.8.6" diff --git a/examples/with-sentry-simple/pages/_app.js b/examples/with-sentry-simple/pages/_app.js index aecbf00a48e99ee99858504d36c72c378a26f8a8..cb770b19ccb771ad87651c4910b5ee8e1abdf253 100644 --- a/examples/with-sentry-simple/pages/_app.js +++ b/examples/with-sentry-simple/pages/_app.js @@ -1,28 +1,21 @@ import React from 'react' import App from 'next/app' -import * as Sentry from '@sentry/browser' +import * as Sentry from '@sentry/node' Sentry.init({ - dsn: 'ENTER_YOUR_SENTRY_DSN_HERE' + // Replace with your project's Sentry DSN + dsn: 'https://00000000000000000000000000000000@sentry.io/1111111' }) class MyApp extends App { - componentDidCatch (error, errorInfo) { - Sentry.withScope(scope => { - Object.keys(errorInfo).forEach(key => { - scope.setExtra(key, errorInfo[key]) - }) - - Sentry.captureException(error) - }) - - super.componentDidCatch(error, errorInfo) - } - render () { const { Component, pageProps } = this.props - return + // Workaround for https://github.com/zeit/next.js/issues/8592 + const { err } = this.props + const modifiedPageProps = { ...pageProps, err } + + return } } diff --git a/examples/with-sentry-simple/pages/_document.js b/examples/with-sentry-simple/pages/_document.js deleted file mode 100644 index 001f0535a21904166095f3d6283cdb74029012dc..0000000000000000000000000000000000000000 --- a/examples/with-sentry-simple/pages/_document.js +++ /dev/null @@ -1,31 +0,0 @@ -import Document, { Html, Head, Main, NextScript } from 'next/document' -import * as Sentry from '@sentry/browser' - -process.on('unhandledRejection', err => { - Sentry.captureException(err) -}) - -process.on('uncaughtException', err => { - Sentry.captureException(err) -}) - -class MyDocument extends Document { - static async getInitialProps (ctx) { - const initialProps = await Document.getInitialProps(ctx) - return { ...initialProps } - } - - render () { - return ( - - - -
- - - - ) - } -} - -export default MyDocument diff --git a/examples/with-sentry-simple/pages/_error.js b/examples/with-sentry-simple/pages/_error.js new file mode 100644 index 0000000000000000000000000000000000000000..eec7942b439c63ed653fd6c1bcc722de3d6edb0d --- /dev/null +++ b/examples/with-sentry-simple/pages/_error.js @@ -0,0 +1,64 @@ +import React from 'react' +import Error from 'next/error' +import * as Sentry from '@sentry/node' + +const MyError = ({ statusCode, hasGetInitialPropsRun, err }) => { + if (!hasGetInitialPropsRun && err) { + // getInitialProps is not called in case of + // https://github.com/zeit/next.js/issues/8592. As a workaround, we pass + // err via _app.js so it can be captured + Sentry.captureException(err) + } + + return +} + +MyError.getInitialProps = async ({ res, err, asPath }) => { + const errorInitialProps = await Error.getInitialProps({ res, err }) + + // Workaround for https://github.com/zeit/next.js/issues/8592, mark when + // getInitialProps has run + errorInitialProps.hasGetInitialPropsRun = true + + if (res) { + // Running on the server, the response object is available. + // + // Next.js will pass an err on the server if a page's `getInitialProps` + // threw or returned a Promise that rejected + + if (res.statusCode === 404) { + // Opinionated: do not record an exception in Sentry for 404 + return { statusCode: 404 } + } + + if (err) { + Sentry.captureException(err) + + return errorInitialProps + } + } else { + // 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) + + 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}`)) + + return errorInitialProps +} + +export default MyError diff --git a/examples/with-sentry-simple/pages/client/test1.js b/examples/with-sentry-simple/pages/client/test1.js new file mode 100644 index 0000000000000000000000000000000000000000..460e43ec53d724b9f7ed3a18daadc4b69b44f26d --- /dev/null +++ b/examples/with-sentry-simple/pages/client/test1.js @@ -0,0 +1,9 @@ +import React from 'react' + +const Test1 = () =>

Client Test 1

+ +Test1.getInitialProps = () => { + throw new Error('Client Test 1') +} + +export default Test1 diff --git a/examples/with-sentry-simple/pages/client/test2.js b/examples/with-sentry-simple/pages/client/test2.js new file mode 100644 index 0000000000000000000000000000000000000000..d83f6f3e44e4e3ef787e49da24347896671a3697 --- /dev/null +++ b/examples/with-sentry-simple/pages/client/test2.js @@ -0,0 +1,7 @@ +import React from 'react' + +const Test2 = () =>

Client Test 2

+ +Test2.getInitialProps = () => Promise.reject(new Error('Client Test 2')) + +export default Test2 diff --git a/examples/with-sentry-simple/pages/client/test3.js b/examples/with-sentry-simple/pages/client/test3.js new file mode 100644 index 0000000000000000000000000000000000000000..4a5ed49eb956b01bba826d67f5458f647046ef16 --- /dev/null +++ b/examples/with-sentry-simple/pages/client/test3.js @@ -0,0 +1,13 @@ +import React from 'react' + +const Test3 = () =>

Client Test 3

+ +Test3.getInitialProps = () => { + const doAsyncWork = () => Promise.reject(new Error('Client Test 3')) + + doAsyncWork() + + return {} +} + +export default Test3 diff --git a/examples/with-sentry-simple/pages/client/test4.js b/examples/with-sentry-simple/pages/client/test4.js new file mode 100644 index 0000000000000000000000000000000000000000..b5d743dbf956d47cf8ebc382c4198527e97a2d20 --- /dev/null +++ b/examples/with-sentry-simple/pages/client/test4.js @@ -0,0 +1,8 @@ +import React from 'react' + +const doAsyncWork = () => Promise.reject(new Error('Client Test 4')) +doAsyncWork() + +const Test4 = () =>

Client Test 4

+ +export default Test4 diff --git a/examples/with-sentry-simple/pages/client/test5.js b/examples/with-sentry-simple/pages/client/test5.js new file mode 100644 index 0000000000000000000000000000000000000000..d385788d24a448192c76de789fc980885a933b62 --- /dev/null +++ b/examples/with-sentry-simple/pages/client/test5.js @@ -0,0 +1,19 @@ +import React from 'react' + +// This code will run just fine on the server in Node.js, but process will be +// undefined in a browser. Note that `isProd = process.env.NODE_ENV` would have +// worked because Webpack's DefinePlugin will replace it with a string at build +// time: https://nextjs.org/docs#build-time-configuration +const env = process.env +const isProd = env.NODE_ENV === 'production' + +const Test5 = () => ( + +

Client Test 5

+

+ isProd: {isProd} +

+
+) + +export default Test5 diff --git a/examples/with-sentry-simple/pages/client/test6.js b/examples/with-sentry-simple/pages/client/test6.js new file mode 100644 index 0000000000000000000000000000000000000000..9db472670853206611a2f909f7a271be05dfaa45 --- /dev/null +++ b/examples/with-sentry-simple/pages/client/test6.js @@ -0,0 +1,11 @@ +import React from 'react' + +const Test6 = () => { + React.useEffect(() => { + throw new Error('Client Test 6') + }, []) + + return

Client Test 6

+} + +export default Test6 diff --git a/examples/with-sentry-simple/pages/client/test7.js b/examples/with-sentry-simple/pages/client/test7.js new file mode 100644 index 0000000000000000000000000000000000000000..5bde35bd544ec9ebbd654b4ef8c0ad452df89488 --- /dev/null +++ b/examples/with-sentry-simple/pages/client/test7.js @@ -0,0 +1,13 @@ +import React from 'react' + +const Test7 = () => { + React.useEffect(async () => { + const doAsyncWork = () => Promise.reject(new Error('Client Test 7')) + const result = await doAsyncWork() + console.log(result) + }, []) + + return

Client Test 7

+} + +export default Test7 diff --git a/examples/with-sentry-simple/pages/client/test8.js b/examples/with-sentry-simple/pages/client/test8.js new file mode 100644 index 0000000000000000000000000000000000000000..07bfd9db51b623d1c20e7cff2e0811d0c956963e --- /dev/null +++ b/examples/with-sentry-simple/pages/client/test8.js @@ -0,0 +1,16 @@ +import React from 'react' + +const Test8 = () => ( + +

Client Test 8

+ +
+) + +export default Test8 diff --git a/examples/with-sentry-simple/pages/index.js b/examples/with-sentry-simple/pages/index.js index 049007c78fdb744a4086d9fb994cc846b716f913..1543981ed4b76a82c8745998565ff05aae9106b9 100644 --- a/examples/with-sentry-simple/pages/index.js +++ b/examples/with-sentry-simple/pages/index.js @@ -1,49 +1,183 @@ import React from 'react' +import Link from 'next/link' -class Index extends React.Component { - static getInitialProps ({ query }) { - if (query.raiseError) { - throw new Error('Error in getInitialProps') - } - } +const Index = () => ( +
+

Sentry Simple Example 🚨

+

+ This example demonstrates how to record unhandled exceptions in your code + with Sentry. There are several test pages below that result in various + kinds of unhandled exceptions. +

+

+ Important: exceptions in development mode take a + different path than in production. These tests should be run on a + production build (i.e. 'next build'). + {' '} + Read more +

+
    +
  • Server exceptions
  • +
      +
    • + getInitialProps throws an Error. This should cause _error.js to + render and record Error('Client Test 1') in Sentry. + {' '} + + Open in a new tab + +
    • +
    • + getInitialProps returns a Promise that rejects. This should cause + _error.js to render and record Error('Server Test 2') in Sentry. + {' '} + + Open in a new tab + +
    • +
    • + getInitialProps calls a Promise that rejects, but does not handle the + rejection or await its result (returning synchronously). Sentry should + record Error('Server Test 3'). + {' '} + + Open in a new tab + +
    • +
    • + There is a top-of-module Promise that rejects, but its result is not + awaited. Sentry should record Error('Server Test 4'). Note this will + also be recorded on the client side, once the page is hydrated. + {' '} + + Open in a new tab + +
    • +
    - state = { - raiseErrorInRender: false, - raiseErrorInUpdate: false - } - - componentDidUpdate () { - if (this.state.raiseErrorInUpdate) { - throw new Error('Error in componentDidUpdate') - } - } - - raiseErrorInUpdate = () => this.setState({ raiseErrorInUpdate: '1' }) - raiseErrorInRender = () => this.setState({ raiseErrorInRender: '1' }) - - render () { - if (this.state.raiseErrorInRender) { - throw new Error('Error in render') - } - - return ( - - ) - } -} +
  • Client exceptions
  • +
      +
    • + getInitialProps throws an Error. This should cause _error.js to render + and record Error('Client Test 1') in Sentry. Note Sentry will double + count this exception. Once from an unhandledrejection and again in + _error.js. Could be a bug in Next.js or Sentry, requires more + debugging. + {' '} + + + Perform client side navigation + + +
    • +
    • + getInitialProps returns a Promise that rejects. This should cause + _error.js to render and record Error('Client Test 2') in Sentry. As + above, Sentry will double count this exception. + {' '} + + + Perform client side navigation + + +
    • +
    • + getInitialProps calls a Promise that rejects, but does not handle the + rejection or await its result (returning synchronously). Sentry should + record Error('Client Test 3'). + {' '} + + + Perform client side navigation + + +
    • +
    • + There is a top-of-module Promise that rejects, but its result is not + awaited. Sentry should record Error('Client Test 4'). + {' '} + + + Perform client side navigation + + + {' '} + or + {' '} + + Open in a new tab + +
    • +
    • + There is a top-of-module exception. _error.js should render and record + ReferenceError('process is not defined') in Sentry. + {' '} + + + Perform client side navigation + + + {' '} + or + {' '} + + Open in a new tab + +
    • +
    • + There is an exception during React lifecycle that is caught by + Next.js's React Error Boundary. In this case, when the component + mounts. This should cause _error.js to render and record + Error('Client Test 6') in Sentry. + {' '} + + + Perform client side navigation + + + {' '} + or + {' '} + + Open in a new tab + +
    • +
    • + There is an unhandled Promise rejection during React lifecycle. In + this case, when the component mounts. Sentry should record + Error('Client Test 7'). + {' '} + + + Perform client side navigation + + + {' '} + or + {' '} + + Open in a new tab + +
    • +
    • + An Error is thrown from an event handler. Sentry should record + Error('Client Test 8'). + {' '} + + + Perform client side navigation + + + {' '} + or + {' '} + + Open in a new tab + +
    • +
    +
+
+) export default Index diff --git a/examples/with-sentry-simple/pages/server/test1.js b/examples/with-sentry-simple/pages/server/test1.js new file mode 100644 index 0000000000000000000000000000000000000000..60f8106e2bc5e3e2fb429022cf751a15b0075b26 --- /dev/null +++ b/examples/with-sentry-simple/pages/server/test1.js @@ -0,0 +1,9 @@ +import React from 'react' + +const Test1 = () =>

Server Test 1

+ +Test1.getInitialProps = () => { + throw new Error('Server Test 1') +} + +export default Test1 diff --git a/examples/with-sentry-simple/pages/server/test2.js b/examples/with-sentry-simple/pages/server/test2.js new file mode 100644 index 0000000000000000000000000000000000000000..dff9c3c76fad8c75c8f9038dd2a2b45b5cf043c4 --- /dev/null +++ b/examples/with-sentry-simple/pages/server/test2.js @@ -0,0 +1,7 @@ +import React from 'react' + +const Test2 = () =>

Server Test 2

+ +Test2.getInitialProps = () => Promise.reject(new Error('Server Test 2')) + +export default Test2 diff --git a/examples/with-sentry-simple/pages/server/test3.js b/examples/with-sentry-simple/pages/server/test3.js new file mode 100644 index 0000000000000000000000000000000000000000..cc7affe129847cda47ee4d6655e65cc8434d1b81 --- /dev/null +++ b/examples/with-sentry-simple/pages/server/test3.js @@ -0,0 +1,13 @@ +import React from 'react' + +const Test3 = () =>

Server Test 3

+ +Test3.getInitialProps = () => { + const doAsyncWork = () => Promise.reject(new Error('Server Test 3')) + + doAsyncWork() + + return {} +} + +export default Test3 diff --git a/examples/with-sentry-simple/pages/server/test4.js b/examples/with-sentry-simple/pages/server/test4.js new file mode 100644 index 0000000000000000000000000000000000000000..403297122a19fbfdb5ec90d104b75bcef9078413 --- /dev/null +++ b/examples/with-sentry-simple/pages/server/test4.js @@ -0,0 +1,14 @@ +import React from 'react' + +const doAsyncWork = () => Promise.reject(new Error('Server Test 4')) +doAsyncWork() + +const Test4 = () =>

Server Test 4

+ +// Define getInitialProps so that the page will be rendered on the server +// instead of statically +Test4.getInitialProps = () => { + return {} +} + +export default Test4