提交 dc28e5b7 编写于 作者: W Weston Thayer 提交者: Tim Neutkens

Example update: with-sentry-simple (#8684)

* 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
上级 aa98323e
......@@ -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.
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
}
})
......@@ -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"
......
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} />
}
}
......
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 (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
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
import React from 'react'
const Test1 = () => <h1>Client Test 1</h1>
Test1.getInitialProps = () => {
throw new Error('Client Test 1')
}
export default Test1
import React from 'react'
const Test2 = () => <h1>Client Test 2</h1>
Test2.getInitialProps = () => Promise.reject(new Error('Client Test 2'))
export default Test2
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
import React from 'react'
const doAsyncWork = () => Promise.reject(new Error('Client Test 4'))
doAsyncWork()
const Test4 = () => <h1>Client Test 4</h1>
export default Test4
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
import React from 'react'
const Test6 = () => {
React.useEffect(() => {
throw new Error('Client Test 6')
}, [])
return <h1>Client Test 6</h1>
}
export default Test6
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
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
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 = () => (
<div style={{ maxWidth: 700, margin: '0 auto' }}>
<h2>Sentry Simple Example 🚨</h2>
<p>
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.
</p>
<p>
<strong>Important:</strong> exceptions in development mode take a
different path than in production. These tests should be run on a
production build (i.e. 'next build').
{' '}
<a href='https://nextjs.org/docs#custom-error-handling'>Read more</a>
</p>
<ul>
<li>Server exceptions</li>
<ul>
<li>
getInitialProps throws an Error. This should cause _error.js to
render and record Error('Client Test 1') in Sentry.
{' '}
<a href='/server/test1' target='_blank'>
Open in a new tab
</a>
</li>
<li>
getInitialProps returns a Promise that rejects. This should cause
_error.js to render and record Error('Server Test 2') in Sentry.
{' '}
<a href='/server/test2' target='_blank'>
Open in a new tab
</a>
</li>
<li>
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').
{' '}
<a href='/server/test3' target='_blank'>
Open in a new tab
</a>
</li>
<li>
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.
{' '}
<a href='/server/test4' target='_blank'>
Open in a new tab
</a>
</li>
</ul>
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 (
<div>
<h2>Sentry Example 🚨</h2>
<ul>
<li>
<a href='#' onClick={this.raiseErrorInRender}>
Raise the error in render
</a>
</li>
<li>
<a href='#' onClick={this.raiseErrorInUpdate}>
Raise the error in componentDidUpdate
</a>
</li>
</ul>
</div>
)
}
}
<li>Client exceptions</li>
<ul>
<li>
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.
{' '}
<Link href='/client/test1'>
<a>
Perform client side navigation
</a>
</Link>
</li>
<li>
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.
{' '}
<Link href='/client/test2'>
<a>
Perform client side navigation
</a>
</Link>
</li>
<li>
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').
{' '}
<Link href='/client/test3'>
<a>
Perform client side navigation
</a>
</Link>
</li>
<li>
There is a top-of-module Promise that rejects, but its result is not
awaited. Sentry should record Error('Client Test 4').
{' '}
<Link href='/client/test4'>
<a>
Perform client side navigation
</a>
</Link>
{' '}
or
{' '}
<a href='/client/test4' target='_blank'>
Open in a new tab
</a>
</li>
<li>
There is a top-of-module exception. _error.js should render and record
ReferenceError('process is not defined') in Sentry.
{' '}
<Link href='/client/test5'>
<a>
Perform client side navigation
</a>
</Link>
{' '}
or
{' '}
<a href='/client/test5' target='_blank'>
Open in a new tab
</a>
</li>
<li>
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.
{' '}
<Link href='/client/test6'>
<a>
Perform client side navigation
</a>
</Link>
{' '}
or
{' '}
<a href='/client/test6' target='_blank'>
Open in a new tab
</a>
</li>
<li>
There is an unhandled Promise rejection during React lifecycle. In
this case, when the component mounts. Sentry should record
Error('Client Test 7').
{' '}
<Link href='/client/test7'>
<a>
Perform client side navigation
</a>
</Link>
{' '}
or
{' '}
<a href='/client/test7' target='_blank'>
Open in a new tab
</a>
</li>
<li>
An Error is thrown from an event handler. Sentry should record
Error('Client Test 8').
{' '}
<Link href='/client/test8'>
<a>
Perform client side navigation
</a>
</Link>
{' '}
or
{' '}
<a href='/client/test8' target='_blank'>
Open in a new tab
</a>
</li>
</ul>
</ul>
</div>
)
export default Index
import React from 'react'
const Test1 = () => <h1>Server Test 1</h1>
Test1.getInitialProps = () => {
throw new Error('Server Test 1')
}
export default Test1
import React from 'react'
const Test2 = () => <h1>Server Test 2</h1>
Test2.getInitialProps = () => Promise.reject(new Error('Server Test 2'))
export default Test2
import React from 'react'
const Test3 = () => <h1>Server Test 3</h1>
Test3.getInitialProps = () => {
const doAsyncWork = () => Promise.reject(new Error('Server Test 3'))
doAsyncWork()
return {}
}
export default Test3
import React from 'react'
const doAsyncWork = () => Promise.reject(new Error('Server Test 4'))
doAsyncWork()
const Test4 = () => <h1>Server Test 4</h1>
// Define getInitialProps so that the page will be rendered on the server
// instead of statically
Test4.getInitialProps = () => {
return {}
}
export default Test4
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册