Exception tracking đŸš« with Bugsnag and Redwood

dac09

Daniel Choudhury

Posted on September 11, 2020

Exception tracking đŸš« with Bugsnag and Redwood

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.

Bugsnag UI preview

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:

Friction-free mobile & tvOS screen recording & sharing

💖 đŸ’Ș 🙅 đŸš©
dac09
Daniel Choudhury

Posted on September 11, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related