mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Improve with-sentry example (#5727)
* Improve with-sentry example * remove nonexisting keys from request and update errorInfo handling * readd query and pathname * read query and params and add pathname and query to client
This commit is contained in:
parent
c867b0ce9c
commit
cd1d3640a9
|
@ -47,3 +47,7 @@ This example show you how to add Sentry to catch errors in next.js
|
||||||
|
|
||||||
You will need a Sentry DSN for your project. You can get it from the Settings of your Project, in **Client Keys (DSN)**, and copy the string labeled **DSN (Public)**.
|
You will need a Sentry DSN for your project. You can get it from the Settings of your Project, in **Client Keys (DSN)**, and copy the string labeled **DSN (Public)**.
|
||||||
Note that if you are using a custom server, there is logging available for common platforms: https://docs.sentry.io/platforms/javascript/express/?platform=node
|
Note that if you are using a custom server, there is logging available for common platforms: https://docs.sentry.io/platforms/javascript/express/?platform=node
|
||||||
|
|
||||||
|
You can set SENTRY_DSN in next.config.js
|
||||||
|
|
||||||
|
If you want sentry to show non-minified sources you need to set SENTRY_TOKEN environment variable when starting server. You can find it in project settings under "Security Token" section.
|
||||||
|
|
23
examples/with-sentry/next.config.js
Normal file
23
examples/with-sentry/next.config.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
const webpack = require('webpack')
|
||||||
|
const nextSourceMaps = require('@zeit/next-source-maps')()
|
||||||
|
|
||||||
|
const SENTRY_DSN = ''
|
||||||
|
|
||||||
|
module.exports = nextSourceMaps({
|
||||||
|
webpack: (config, { dev, isServer, buildId }) => {
|
||||||
|
if (!dev) {
|
||||||
|
config.plugins.push(
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env.SENTRY_DSN': JSON.stringify(SENTRY_DSN),
|
||||||
|
'process.env.SENTRY_RELEASE': JSON.stringify(buildId)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isServer) {
|
||||||
|
config.resolve.alias['@sentry/node'] = '@sentry/browser'
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
})
|
|
@ -2,15 +2,21 @@
|
||||||
"name": "with-sentry",
|
"name": "with-sentry",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next",
|
"dev": "node server.js",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start"
|
"start": "NODE_ENV=production node server.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "latest",
|
"@sentry/browser": "^4.3.4",
|
||||||
"@sentry/browser": "^4.0.4",
|
"@sentry/node": "^4.3.4",
|
||||||
|
"@zeit/next-source-maps": "0.0.4-canary.0",
|
||||||
|
"cookie-parser": "^1.4.3",
|
||||||
|
"express": "^4.16.4",
|
||||||
|
"js-cookie": "^2.2.0",
|
||||||
|
"next": "7.0.2",
|
||||||
"react": "^16.5.2",
|
"react": "^16.5.2",
|
||||||
"react-dom": "^16.5.2"
|
"react-dom": "^16.5.2",
|
||||||
|
"uuid": "^3.3.2"
|
||||||
},
|
},
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,32 @@
|
||||||
import App from 'next/app'
|
import App from 'next/app'
|
||||||
import * as Sentry from '@sentry/browser'
|
import { captureException } from '../utils/sentry'
|
||||||
|
|
||||||
const SENTRY_PUBLIC_DSN = ''
|
class MyApp extends App {
|
||||||
|
// This reports errors before rendering, when fetching initial props
|
||||||
|
static async getInitialProps (appContext) {
|
||||||
|
const { Component, ctx } = appContext
|
||||||
|
|
||||||
export default class MyApp extends App {
|
let pageProps = {}
|
||||||
constructor (...args) {
|
|
||||||
super(...args)
|
try {
|
||||||
Sentry.init({dsn: SENTRY_PUBLIC_DSN})
|
if (Component.getInitialProps) {
|
||||||
|
pageProps = await Component.getInitialProps(ctx)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
captureException(e, ctx)
|
||||||
|
throw e // you can also skip re-throwing and set property on pageProps
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch (error, errorInfo) {
|
return {
|
||||||
Sentry.configureScope(scope => {
|
pageProps
|
||||||
Object.keys(errorInfo).forEach(key => {
|
}
|
||||||
scope.setExtra(key, errorInfo[key])
|
}
|
||||||
})
|
|
||||||
})
|
|
||||||
Sentry.captureException(error)
|
|
||||||
|
|
||||||
// This is needed to render errors correctly in development / production
|
// This reports errors thrown while rendering components
|
||||||
|
componentDidCatch (error, errorInfo) {
|
||||||
|
captureException(error, { errorInfo })
|
||||||
super.componentDidCatch(error, errorInfo)
|
super.componentDidCatch(error, errorInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default MyApp
|
||||||
|
|
|
@ -1,23 +1,48 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
class Index extends React.Component {
|
class Index extends React.Component {
|
||||||
|
static getInitialProps ({ query, req }) {
|
||||||
|
if (query.raiseError) {
|
||||||
|
throw new Error('Error in getInitialProps')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
raiseError: false
|
raiseError: false
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate () {
|
componentDidUpdate () {
|
||||||
if (this.state.raiseError) {
|
if (this.state.raiseErrorInUpdate) {
|
||||||
throw new Error('Houston, we have a problem')
|
throw new Error('Error in componentDidUpdate')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
raiseError = () => this.setState({ raiseError: true })
|
raiseErrorInUpdate = () => this.setState({ raiseErrorInUpdate: '1' })
|
||||||
|
raiseErrorInRender = () => this.setState({ raiseErrorInRender: '1' })
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
if (this.state.raiseErrorInRender) {
|
||||||
|
throw new Error('Error in render')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>Index page</h2>
|
<h2>Index page</h2>
|
||||||
<button onClick={this.raiseError}>Click to raise the error</button>
|
<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>
|
||||||
|
<li>
|
||||||
|
<Link href={{ pathname: '/', query: { raiseError: '1' } }}>
|
||||||
|
<a>Raise error in getInitialProps of client-loaded page</a>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='/?raiseError=1'>
|
||||||
|
Raise error in getInitialProps of server-loaded page
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
66
examples/with-sentry/server.js
Normal file
66
examples/with-sentry/server.js
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
const next = require('next')
|
||||||
|
const express = require('express')
|
||||||
|
const cookieParser = require('cookie-parser')
|
||||||
|
const Sentry = require('@sentry/node')
|
||||||
|
const uuidv4 = require('uuid/v4')
|
||||||
|
const port = parseInt(process.env.PORT, 10) || 3000
|
||||||
|
const dev = process.env.NODE_ENV !== 'production'
|
||||||
|
const app = next({ dev })
|
||||||
|
const handle = app.getRequestHandler()
|
||||||
|
|
||||||
|
require('./utils/sentry')
|
||||||
|
|
||||||
|
app.prepare()
|
||||||
|
.then(() => {
|
||||||
|
const server = express()
|
||||||
|
|
||||||
|
// This attaches request information to sentry errors
|
||||||
|
server.use(Sentry.Handlers.requestHandler())
|
||||||
|
|
||||||
|
server.use(cookieParser())
|
||||||
|
|
||||||
|
server.use((req, res, next) => {
|
||||||
|
const htmlPage =
|
||||||
|
!req.path.match(/^\/(_next|static)/) &&
|
||||||
|
!req.path.match(/\.(js|map)$/) &&
|
||||||
|
req.accepts('text/html', 'text/css', 'image/png') === 'text/html'
|
||||||
|
|
||||||
|
if (!htmlPage) {
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.cookies.sid || req.cookies.sid.length === 0) {
|
||||||
|
req.cookies.sid = uuidv4()
|
||||||
|
res.cookie('sid', req.cookies.sid)
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// In production we don't want to serve sourcemaps for anyone
|
||||||
|
if (!dev) {
|
||||||
|
const hasSentryToken = !!process.env.SENTRY_TOKEN
|
||||||
|
server.get(/\.map$/, (req, res, next) => {
|
||||||
|
if (hasSentryToken && req.headers['x-sentry-token'] !== process.env.SENTRY_TOKEN) {
|
||||||
|
res
|
||||||
|
.status(401)
|
||||||
|
.send(
|
||||||
|
'Authentication access token is required to access the source map.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
server.get('*', (req, res) => handle(req, res))
|
||||||
|
|
||||||
|
// This handles errors if they are thrown before raching the app
|
||||||
|
server.use(Sentry.Handlers.errorHandler())
|
||||||
|
|
||||||
|
server.listen(port, err => {
|
||||||
|
if (err) throw err
|
||||||
|
console.log(`> Ready on http://localhost:${port}`)
|
||||||
|
})
|
||||||
|
})
|
63
examples/with-sentry/utils/sentry.js
Normal file
63
examples/with-sentry/utils/sentry.js
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
const Sentry = require('@sentry/node')
|
||||||
|
const Cookie = require('js-cookie')
|
||||||
|
|
||||||
|
if (process.env.SENTRY_DSN) {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.SENTRY_DSN,
|
||||||
|
release: process.env.SENTRY_RELEASE,
|
||||||
|
maxBreadcrumbs: 50,
|
||||||
|
attachStacktrace: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureException (err, { req, res, errorInfo, query, pathname }) {
|
||||||
|
Sentry.configureScope(scope => {
|
||||||
|
if (err.message) {
|
||||||
|
// De-duplication currently doesn't work correctly for SSR / browser errors
|
||||||
|
// so we force deduplication by error message if it is present
|
||||||
|
scope.setFingerprint([err.message])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.statusCode) {
|
||||||
|
scope.setExtra('statusCode', err.statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res && res.statusCode) {
|
||||||
|
scope.setExtra('statusCode', res.statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.browser) {
|
||||||
|
scope.setTag('ssr', false)
|
||||||
|
scope.setExtra('query', query)
|
||||||
|
scope.setExtra('pathname', pathname)
|
||||||
|
|
||||||
|
// On client-side we use js-cookie package to fetch it
|
||||||
|
const sessionId = Cookie.get('sid')
|
||||||
|
if (sessionId) {
|
||||||
|
scope.setUser({ id: sessionId })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scope.setTag('ssr', true)
|
||||||
|
scope.setExtra('url', req.url)
|
||||||
|
scope.setExtra('method', req.method)
|
||||||
|
scope.setExtra('headers', req.headers)
|
||||||
|
scope.setExtra('params', req.params)
|
||||||
|
scope.setExtra('query', req.query)
|
||||||
|
|
||||||
|
// On server-side we take session cookie directly from request
|
||||||
|
if (req.cookies.sid) {
|
||||||
|
scope.setUser({ id: req.cookies.sid })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorInfo) {
|
||||||
|
scope.setExtra('componentStack', errorInfo.componentStack)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Sentry.captureException(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
captureException
|
||||||
|
}
|
Loading…
Reference in a new issue