Skip to content

Commit

Permalink
Example update: with-sentry-simple (#8684)
Browse files Browse the repository at this point in the history
* Update to capture server exceptions and more

- Adds test cases for several server and client-side exceptions
- Allows capturing more server-side exceptions by overriding _error.js and using Sentry.captureException() within
- Use @sentry/node on the server
- Rely on Next.js's React Error Boundary instead of creating our own in _app.js

* Update test notes

Found some differences while testing in production

* Remove accidental mount throw on test 8

* Add note about server-side source maps

* Linting fixes
  • Loading branch information
Weston Thayer authored and timneutkens committed Sep 17, 2019
1 parent aa98323 commit dc28e5b
Show file tree
Hide file tree
Showing 19 changed files with 447 additions and 96 deletions.
43 changes: 38 additions & 5 deletions examples/with-sentry-simple/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
20 changes: 19 additions & 1 deletion examples/with-sentry-simple/next.config.js
Original file line number Diff line number Diff line change
@@ -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
}
})
1 change: 1 addition & 0 deletions examples/with-sentry-simple/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
23 changes: 8 additions & 15 deletions examples/with-sentry-simple/pages/_app.js
Original file line number Diff line number Diff line change
@@ -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 <Component {...pageProps} />
// Workaround for https://github.com/zeit/next.js/issues/8592
const { err } = this.props
const modifiedPageProps = { ...pageProps, err }

return <Component {...modifiedPageProps} />
}
}

Expand Down
31 changes: 0 additions & 31 deletions examples/with-sentry-simple/pages/_document.js

This file was deleted.

64 changes: 64 additions & 0 deletions examples/with-sentry-simple/pages/_error.js
Original file line number Diff line number Diff line change
@@ -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 <Error statusCode={statusCode} />
}

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
9 changes: 9 additions & 0 deletions examples/with-sentry-simple/pages/client/test1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'

const Test1 = () => <h1>Client Test 1</h1>

Test1.getInitialProps = () => {
throw new Error('Client Test 1')
}

export default Test1
7 changes: 7 additions & 0 deletions examples/with-sentry-simple/pages/client/test2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react'

const Test2 = () => <h1>Client Test 2</h1>

Test2.getInitialProps = () => Promise.reject(new Error('Client Test 2'))

export default Test2
13 changes: 13 additions & 0 deletions examples/with-sentry-simple/pages/client/test3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react'

const Test3 = () => <h1>Client Test 3</h1>

Test3.getInitialProps = () => {
const doAsyncWork = () => Promise.reject(new Error('Client Test 3'))

doAsyncWork()

return {}
}

export default Test3
8 changes: 8 additions & 0 deletions examples/with-sentry-simple/pages/client/test4.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react'

const doAsyncWork = () => Promise.reject(new Error('Client Test 4'))
doAsyncWork()

const Test4 = () => <h1>Client Test 4</h1>

export default Test4
19 changes: 19 additions & 0 deletions examples/with-sentry-simple/pages/client/test5.js
Original file line number Diff line number Diff line change
@@ -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 = () => (
<React.Fragment>
<h1>Client Test 5</h1>
<p>
isProd: {isProd}
</p>
</React.Fragment>
)

export default Test5
11 changes: 11 additions & 0 deletions examples/with-sentry-simple/pages/client/test6.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react'

const Test6 = () => {
React.useEffect(() => {
throw new Error('Client Test 6')
}, [])

return <h1>Client Test 6</h1>
}

export default Test6
13 changes: 13 additions & 0 deletions examples/with-sentry-simple/pages/client/test7.js
Original file line number Diff line number Diff line change
@@ -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 <h1>Client Test 7</h1>
}

export default Test7
16 changes: 16 additions & 0 deletions examples/with-sentry-simple/pages/client/test8.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react'

const Test8 = () => (
<React.Fragment>
<h1>Client Test 8</h1>
<button
onClick={() => {
throw new Error('Client Test 8')
}}
>
Click me to throw an Error
</button>
</React.Fragment>
)

export default Test8
Loading

0 comments on commit dc28e5b

Please sign in to comment.