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 = () =>
+
+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 = () => (
+
+
+
+
+)
+
+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
+
+
+ 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 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
+
+
+
+// Define getInitialProps so that the page will be rendered on the server
+// instead of statically
+Test4.getInitialProps = () => {
+ return {}
+}
+
+export default Test4