cra
mr

Instrumenting GatsbyJS with Sentry

June 11, 2020

I recently kicked off an initiative to port our developer documentation into a public-facing website. Previously this was split between Sentry’s official docs (rarely updated), and our internal knowledge base. Making this public was a good way to ensure it was kept up to date, but also an important step in ensuring Sentry’s open source origins stay true. As part of this, we also wanted to give Gatsby a spin, as working on our Jekyll-based documentation has had a pretty slow iteration speed due to the way it builds pages.

I’m not going to go into the details of the Gatsby build itself, but the project is open source on GitHub so take a look if you’re interested. Instead I’m going to talk a bit about developing the @sentry/gatsby integration, what worked out of the box, and where we still need to improve our new products at Sentry. To follow along take a look at the gatsby package in sentry-javascript.

A Gatsby Trace in Sentry
A Gatsby Trace in Sentry

When I started the integration, I had a rough idea on how to approach it due to an existing plugin by octalmage. There were a couple things I wanted to accomplish though which werent present here:

  • plugin-based approach (the same as the existing plugin)
  • frictionless setup - generally config can be inferred from the environment
  • support for Sentry’s new performance monitoring

I already had the approach for the plugin. It’s fairly straight forward, and just requires a gatsby-browser.js to be defined where you’re passed plugin options and can inject the SDK:

exports.onClientEntry = function (_, pluginParams) {
  require.ensure(["@sentry/react"], function (require) {
    const Sentry = require("@sentry/browser");
    Sentry.init({
      environment: process.env.NODE_ENV || "development",
      release: process.env.SENTRY_RELEASE,
      dsn: process.env.SENTRY_DSN,
      ...pluginParams,
    });
    window.Sentry = Sentry;
  });
};

There’s a couple of tricky bits in here that won’t actually do what you’d expect:

  • Passing integrations is actually operating outside of a browser context, meaning we cant initialize @sentry/apm from pluginParams
  • While NODE_ENV is available, Gatsby wont expose other environment variables to the browser unless prefixed with GATSBY_. This means our SENTRY_ vars won’t exist.

The first one was easy to work around. We don’t actually need a ton of integrations, so we just created detection for tracing configuration:

exports.onClientEntry = function(_, pluginParams) {
  require.ensure(['@sentry/react', '@sentry/apm'], function(require) {
    // ...
    const TracingIntegration = require('@sentry/apm').Integrations.Tracing;
    const tracesSampleRate = pluginParams.tracesSampleRate !== undefined ? pluginParams.tracesSampleRate : 0;
    const integrations = [...(pluginParams.integrations || [])];
    if (tracesSampleRate) {
      integrations.push(new TracingIntegration());
    }
    Sentry.init({
      // ...
      ...pluginParams,
      tracesSampleRate,
      integrations,
    });
    // ...
  });

This will only inject @sentry/apm when you’ve defined tracesSampleRate > 0 in your configuration. Great, we’ve solved our first problem!

The next issue we had was more complex. How do we automatically detect the DSN and release values from the environment? The simplest way I found to do this was to use the onCreateWebpackConfig hook via gatsby-node.js. This allows us to Webpack’s DefinePlugin. The simplest way to understand this plugin is it does a literal find-and-replace on build (not to be confused with templating). Using this we’re able to detect the environment variables at build time, and inject our __SENTRY_DSN__ style placeholders:

exports.onCreateWebpackConfig = ({ plugins, actions }) => {
  actions.setWebpackConfig({
    plugins: [
      plugins.define({
        __SENTRY_RELEASE__: JSON.stringify(process.env.SENTRY_RELEASE || ""),
        __SENTRY_DSN__: JSON.stringify(process.env.SENTRY_DSN || ""),
      }),
    ],
  });
};

Once I got to this point, I decided to take the environment configuration one step further, and added automatic detection for some of the popular cloud services, including GitHub Actions, Netlify, and Vercel:

{
  // ...
  // There's more to this list than whats visible here, but you get the idea.
  __SENTRY_RELEASE__: JSON.stringify(
    // GitHub Actions - https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables
    process.env.GITHUB_SHA ||
      // Netlify - https://docs.netlify.com/configure-builds/environment-variables/#build-metadata
      process.env.COMMIT_REF ||
      // Vercel - https://vercel.com/docs/v2/build-step#system-environment-variables
      process.env.VERCEL_GITHUB_COMMIT_SHA ||
      // Zeit (now known as Vercel)
      process.env.ZEIT_GITHUB_COMMIT_SHA ||
      ""
  ),
}

That’s it! The @sentry/gatsby integration will be available as part of the next release of sentry-javascript. Configuration is as easy as dropping the plugin in your gatsby-config.js:

plugins: [
  {
    resolve: "@sentry/gatsby",
    options: {
      tracesSampleRate: 1,
    },
  },
  // ...
];

In general I was pretty happy with how well our tracing is working out of the box here, but its not without its warts. We’ve got some improvements to make around how navigation events are captured (they’re a fairly empty trace right now), and we need to provide some hooks so you can indicate the status of the transaction (such as telling Sentry about your 404 handler).

The one major nit that I have is and I’d love to see Gatsby support better is some degree of integration tests. Right now we’re shipping with next to nothing (we’re only testing exports), but having a way to know that our usage of the APIs works in different versions of Gatsby is fairly critical. We’ll likely explore just bundling a full Gatsby site in the future so at the very least QA is easy.