Exception tracking đ« with Bugsnag and Redwood
Daniel Choudhury
Posted on September 11, 2020
So youâve built your amazing redwood app, tested it thoroughly and are ready to go live. You, with your hipster beard and oat milk flat whites âïž, youâre an awesome dev, so thereâs no bugs in your code...... or are there?
Of course there are hotshot đ! It might be an edge case you hadnât considered, maybe itâs something in your infrastructure, or that third party service that isnât super reliable or maybe just that users will do what you least expect and they hit an unhandled error!
For a production app, and for your app to delight users, you need to be able to diagnose issues quickly, and push up a fix as soon as you can. You need good logs to be able to see what the issue is, and you want your tooling to minimise this friction. In enterprise circles this is measured using mean time to recovery - MTTR - in simple terms, you want to fix and ship as quickly as possible.
Introduction
This tutorial will walk through setting up Bugsnag, which is an exception tracking tool (primarily used by mobile devs) but also supports web and Node.js, with Redwood - both on the frontend and the api side.
Since we first launched Tape.sh, a screen recording tool for mobile devs, we needed this visibility to fix very hard to reproduce issues. It even helped us contribute back to Redwood!
We really like Bugsnag, but you can follow the exact same process, and use a tool of your own choice - I donât imagine it being too different.
Tldr;
For the frontend, wrap the Redwood app with the bugsnag exception handler component and make sure you upload sourcemaps (with the webpack plugin or otherwise).
For the backend, create a custom apollo server plugin, and pass through exceptions to bugsnag. Make sure you have a method available to report errors to use in your custom functions too.
Part 1: Frontend
First sign in to your bugsnag dashboard, and create your project.
New Project > Browser > React
Grab your API key, and we'll use it in a little bit.
Setting up exception handler
Let's add the Bugsnag library and react plugin
# -W because we'll use it in both web and api
yarn add -W @bugsnag/js
yarn workspace web add @bugsnag/plugin-react
Now we have to wrap our entire frontend react app, with the exception handler. In web/src/index.js
+ import Bugsnag from '@bugsnag/js'
+ import BugsnagPluginReact from '@bugsnag/plugin-react'
+ Bugsnag.start({
+ apiKey: process.env.BUGSNAG_NOTIFIER_API_KEY,
+ plugins: [new BugsnagPluginReact()],
+ releaseStage: process.env.CONTEXT || process.env.NODE_ENV,
+ appVersion: process.env.DEPLOY_ID,
+ })
+ const BugsnagBoundary = Bugsnag.getPlugin('react').createErrorBoundary(React)
ReactDOM.render(
- <FatalErrorBoundary page={FatalErrorPage}>
+ <BugsnagBoundary
+ FallbackComponent={<FatalErrorBoundary page={FatalErrorPage} />}
+ >
<RedwoodProvider>
<Routes />
</RedwoodProvider>
+ </BugsnagBoundar>
- </FatalErrorBoundary>,
document.getElementById('redwood-app')
)
The fallback component I'm using is the default Redwood FatalErrorBoundary, but you can use your own here.
Notice how we're using process.env
variables. By default Redwood (rightly!) doesn't expose env variables to the frontend. So lets modify redwood.toml to include these variables
[web]
port = 8910
apiProxyPath = "/.netlify/functions"
+ includeEnvironmentVariables = ['BUGSNAG_NOTIFIER_API_KEY', 'CONTEXT', 'NODE_ENV', 'DEPLOY_ID']
[api]
port = 8911
[browser]
REMEMBER!
And finally, remember to add BUGSNAG_NOTIFIER_API_KEY
to your .env file
Done âš! Now youâll get notified when your userâs hit an exception. But the logs youâll see wonât be that helpful yet, because your javascript is minified. So far we know what is happening, now letâs setup the why
Webpack setup & uploading source maps
We're going to use the Bugsnag's webpack plugins to set this up. Let's install them:
yarn workspace web add webpack-bugsnag-plugins
To customise your webpack config for Redwood, you need to create a file at web/config/webpack.config.js
. If you already have it, great just add to it.
/* ==== web/config/webpack.config.js ==== */
// Important, so webpack can use the environment variables
require('dotenv-defaults').config()
const {
BugsnagSourceMapUploaderPlugin,
BugsnagBuildReporterPlugin,
} = require('webpack-bugsnag-plugins')
module.exports = (config) => {
// Check if its building in netlify
// No need to upload source maps when building locally
const netlifyBuild = !!process.env.NETLIFY
const bugsnagPlugins = netlifyBuild
? [
new BugsnagBuildReporterPlugin({
apiKey: process.env.BUGSNAG_NOTIFIER_API_KEY,
appVersion: process.env.DEPLOY_ID,
releaseStage: process.env.CONTEXT || process.env.NODE_ENV,
sourceControl: {
provider: 'github',
repository: process.env.REPOSITORY_URL,
revision: process.env.COMMIT_REF,
},
}),
new BugsnagSourceMapUploaderPlugin({
apiKey: process.env.BUGSNAG_NOTIFIER_API_KEY,
appVersion: process.env.DEPLOY_ID,
}),
]
: []
config.plugins = [...config.plugins, ...bugsnagPlugins]
return config
}
Notice that we're using the process.env.NETLIFY
environment variable. This is so that we don't upload source maps for local builds. The environment variables REPOSITORY_URL
, COMMIT_REF
, DEPLOY_ID
and CONTEXT
come from Netlify, so modify according to where you're going to deploy your code.
Validate your setup
So letâs validate our setup now. Just add
throw new Error('Catch me bugsnag!')
anywhere in your frontend code, and when it getâs triggered you should see it come through on your dashboard (and email). Youâll be able to see what happened, why it happened and also, how it happened through the Breadcrumbs tab.
A note on sourcemaps with Netlify. If you have "optimise JS bundles" enabled on Netlify, they move your bundles to the Cloudfront CDN, and Bugsnag isn't able to match up the sourcemap to the js file.
If you switch off the setting in the Netlify UI, you'll be able to see the stacktrace fully. I haven't looked into making it work with Cloudfront yet, because unfortunately Netlify doesn't expose what they rename the files to and what the hostname is going to be, before uploading.
Dev & Prod configuration
Great, now that you're seeing errors come through, you want to disable it for dev. So let's create <EnvironmentAwareErrorBoundary>
const EnvironmentAwareErrorBoundary = React.memo(({ children, ...otherProps }) => {
if (process.env.NODE_ENV === 'development') {
return (
<FatalErrorBoundary page={FatalErrorBoundary} {...otherProps}>
{children}
</FatalErrorBoundary>
)
} else {
Bugsnag.start({
apiKey: process.env.BUGSNAG_NOTIFIER_API_KEY,
plugins: [new BugsnagPluginReact()],
releaseStage: process.env.CONTEXT || process.env.NODE_ENV,
appVersion: process.env.DEPLOY_ID,
})
const BugsnagBoundary = Bugsnag.getPlugin('react').createErrorBoundary(
React
)
return (
<BugsnagBoundary
FallbackComponent={<FatalErrorBoundary page={FatalErrorPage} />}
{...otherProps}
>
{children}
</BugsnagBoundary>
)
}
})
What this does is use the default Redwood FatalErrorBoundary on development, but reports the exception to Bugsnag in production.
You can then wrap your app in this component like this:
ReactDOM.render(
+ <EnvironmentAwareErrorBoundary>
{*/ your other stuff */}
<RedwoodProvider>
<Routes />
</RedwoodProvider>
+ </EnvironmentAwareErrorBoundary>,
document.getElementById('redwood-app')
)
Part 2: API and Graphql
Create custom plugin for graphql
For the backend, we want to capture errors from graphql. So let's start with create a util module to house the Bugsnag code.
api/src/lib/bugsnag.js
import Bugsnag from '@bugsnag/js'
import { isEmpty } from 'lodash'
Bugsnag.start({
apiKey: process.env.BUGSNAG_SERVER_API_KEY,
releaseStage: process.env.CONTEXT || process.env.NODE_ENV,
appVersion: process.env.DEPLOY_ID,
})
export const reportErrorFromContext = (requestContext) => {
const { errors, metrics, request, context } = requestContext
// Call bugsnag here
// But you could easily use something else here
Bugsnag.notify(new Error(errors), function (event) {
event.severity = 'error'
event.addMetadata('metrics', metrics)
event.addMetadata('errors', errors)
event.addMetadata('query', request)
})
}
export const reportError = (error) => {
Bugsnag.notify(error)
}
We expose the reportReportErrorFromContext
to use in our custom apollo server plugin, but leave the reportError
for use elsewhere.
Now let's create the plugin and add it to our server setup
// === api/src/functions/graphql.js ===
+ import { reportErrorFromContext } from 'src/lib/bugsnag'
+ const bugSnagExceptionPlugin = {
+ requestDidStart() {
+ return {
+ didEncounterErrors(requestContext) {
+ reportErrorFromContext(requestContext)
+ },
+ }
+ },
+ }
export const handler = createGraphQLHandler({
getCurrentUser,
+ plugins: [bugSnagExceptionPlugin],
schema: makeMergedSchema({
schemas,
services: makeServices({ services }),
// ....rest of the file omitted for brevity
Custom functions
Remember how we created the reportError
method? You can now use this in your custom functions
Prod configuration
Quick headsup here, Netlifyâs functions annoyingly donât set the NODE_ENV value at runtime by default. So if you wanted to check if youâre running on prod, youâll have to make sure you set the value in the Netlify UI, but then in your netlify.toml set NODE_ENV to development for the build env (otherwise the build will fail).
Same as the frontend, we want to disable logging exceptions during dev. So let's wrap the code in some if statements and we're done! In our case we're using the process.env.LOG_EXCEPTIONS
variable.
api/src/lib/bugsnag.js
+ if (!isEmpty(process.env.LOG_EXCEPTIONS)) {
Bugsnag.start({
apiKey: process.env.BUGSNAG_SERVER_API_KEY,
releaseStage: process.env.CONTEXT || process.env.NODE_ENV,
appVersion: process.env.DEPLOY_ID,
})
+ }
export const reportReportErrorFromContext = (requestContext) => {
const { errors, metrics, request, context } = requestContext
// Note that netlify doesn't set node_env at runtime in functions
+ if (isEmpty(process.env.LOG_EXCEPTIONS)) {
+ return
+ }
That's all folks! đ You're now ready to launch your app, with the confidence that you can find, trace and fix exceptions if they happen!
đđœ PS Here's what we're working on with Redwood:
Posted on September 11, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.