Skip to content

Commit

Permalink
Improve with-sentry example (#5727)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sheerun authored and timneutkens committed Dec 10, 2018
1 parent c867b0c commit cd1d364
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 23 deletions.
4 changes: 4 additions & 0 deletions examples/with-sentry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)**.
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 changes: 23 additions & 0 deletions examples/with-sentry/next.config.js
Original file line number Diff line number Diff line change
@@ -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
}
})
16 changes: 11 additions & 5 deletions examples/with-sentry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@
"name": "with-sentry",
"version": "1.0.0",
"scripts": {
"dev": "next",
"dev": "node server.js",
"build": "next build",
"start": "next start"
"start": "NODE_ENV=production node server.js"
},
"dependencies": {
"next": "latest",
"@sentry/browser": "^4.0.4",
"@sentry/browser": "^4.3.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-dom": "^16.5.2"
"react-dom": "^16.5.2",
"uuid": "^3.3.2"
},
"license": "ISC"
}
37 changes: 23 additions & 14 deletions examples/with-sentry/pages/_app.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
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 {
constructor (...args) {
super(...args)
Sentry.init({dsn: SENTRY_PUBLIC_DSN})
let pageProps = {}

try {
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
}

return {
pageProps
}
}

// This reports errors thrown while rendering components
componentDidCatch (error, errorInfo) {
Sentry.configureScope(scope => {
Object.keys(errorInfo).forEach(key => {
scope.setExtra(key, errorInfo[key])
})
})
Sentry.captureException(error)

// This is needed to render errors correctly in development / production
captureException(error, { errorInfo })
super.componentDidCatch(error, errorInfo)
}
}

export default MyApp
33 changes: 29 additions & 4 deletions examples/with-sentry/pages/index.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,48 @@
import React from 'react'
import Link from 'next/link'

class Index extends React.Component {
static getInitialProps ({ query, req }) {
if (query.raiseError) {
throw new Error('Error in getInitialProps')
}
}

state = {
raiseError: false
}

componentDidUpdate () {
if (this.state.raiseError) {
throw new Error('Houston, we have a problem')
if (this.state.raiseErrorInUpdate) {
throw new Error('Error in componentDidUpdate')
}
}

raiseError = () => this.setState({ raiseError: true })
raiseErrorInUpdate = () => this.setState({ raiseErrorInUpdate: '1' })
raiseErrorInRender = () => this.setState({ raiseErrorInRender: '1' })

render () {
if (this.state.raiseErrorInRender) {
throw new Error('Error in render')
}

return (
<div>
<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>
)
}
Expand Down
66 changes: 66 additions & 0 deletions examples/with-sentry/server.js
Original file line number Diff line number Diff line change
@@ -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 changes: 63 additions & 0 deletions examples/with-sentry/utils/sentry.js
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit cd1d364

Please sign in to comment.