diff --git a/examples/with-sentry/README.md b/examples/with-sentry/README.md index 235ece556ca4f..28fecc990505d 100644 --- a/examples/with-sentry/README.md +++ b/examples/with-sentry/README.md @@ -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. diff --git a/examples/with-sentry/next.config.js b/examples/with-sentry/next.config.js new file mode 100644 index 0000000000000..61b2b70f4a014 --- /dev/null +++ b/examples/with-sentry/next.config.js @@ -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 + } +}) diff --git a/examples/with-sentry/package.json b/examples/with-sentry/package.json index b511f11b02db5..4e8b8aaee0ae2 100644 --- a/examples/with-sentry/package.json +++ b/examples/with-sentry/package.json @@ -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" } diff --git a/examples/with-sentry/pages/_app.js b/examples/with-sentry/pages/_app.js index 61d194dbfdc50..7d91e4ca514f3 100644 --- a/examples/with-sentry/pages/_app.js +++ b/examples/with-sentry/pages/_app.js @@ -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 diff --git a/examples/with-sentry/pages/index.js b/examples/with-sentry/pages/index.js index 826020e30c754..cde8a5ec9c983 100644 --- a/examples/with-sentry/pages/index.js +++ b/examples/with-sentry/pages/index.js @@ -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 (
) } diff --git a/examples/with-sentry/server.js b/examples/with-sentry/server.js new file mode 100644 index 0000000000000..7ade81e4f9a34 --- /dev/null +++ b/examples/with-sentry/server.js @@ -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}`) + }) + }) diff --git a/examples/with-sentry/utils/sentry.js b/examples/with-sentry/utils/sentry.js new file mode 100644 index 0000000000000..458c901936338 --- /dev/null +++ b/examples/with-sentry/utils/sentry.js @@ -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 +}