Crash Reporting & Real User Monitoring for React applications

| 10 min. (2106 words)

In this blog post, I’m going to talk about how to integrate Raygun4JS with React at a deeper level than what is provided out-of-the-box. None of these things are needed for Raygun4JS to do its primary job (reporting errors that happen on your website) but provide useful extra value for determining how your React application is performing and what is going wrong when an error occurs.

I will be covering the following topics:

Installing Raygun4JS

Installing Raygun to your React application only requires a couple lines of code.
Out of the box, Raygun’s JavaScript provider will provide Crash Reporting support to your React application.

Real User Monitoring requires some additional configuration to hook into your SPAs navigation solution.
Don’t stress, I cover this off in following sections so please read on.

To install the Raygun4JS provider you have two options:

  1. Install the Raygun4JS package from NPM and import it into your application
  2. Include the Raygun4JS script tag in your application

The NPM package is best suited for SPA applications built using frameworks like React, however, it does come with a caveat - Any issues that occur before the application has loaded will not be reported to Raygun. This is because the Raygun4JS package needs to be imported into your application before it can be used.

Alternatively, the provider can also be installed to your application using a script tag in the <head> of your website. This allows for Raygun to be loaded before your application has loaded. Depending on your use case you can choose the one that suits you best, or even use both (eg. load Crash Reporting using the <script> approach & Real User Monitoring using the NPM package).

Installation & Configuration via NPM

  1. Add the package to your application:
# Yarn
yarn add raygun4js

# NPM
npm install raygun4js
  1. Then configure it like so:
// App.js
import rg4js from 'raygun4js';

rg4js('apiKey', 'API_KEY');
rg4js('enableCrashReporting', true); // For Crash Reporting
rg4js('enablePulse', true); // For Real User Monitoring

Installation & Configuration via CDN

  1. Add the following snippet into the <head> of your document above every other script file.
<script type="text/javascript">
  !function(a,b,c,d,e,f,g,h){a.RaygunObject=e,a[e]=a[e]||function(){
  (a[e].o=a[e].o||[]).push(arguments)},f=b.createElement(c),g=b.getElementsByTagName(c)[0],
  f.async=1,f.src=d,g.parentNode.insertBefore(f,g),h=a.onerror,a.onerror=function(b,c,d,f,g){
  h&&h(b,c,d,f,g),g||(g=new Error(b)),a[e].q=a[e].q||[],a[e].q.push({
  e:g})}}(window,document,"script","//cdn.raygun.io/raygun4js/raygun.min.js","rg4js");
</script>
  1. Then configure it like so:
<script type="text/javascript">
  rg4js('apiKey', 'API_KEY');
  rg4js('enableCrashReporting', true); // For Crash Reporting
  rg4js('enablePulse', true); // For Real User Monitoring
</script>

More information on the installation process & advanced features can be found in our official React documentation.

If you’re new to React debugging, I’d also recommend checking out our Getting started with React debugging blog post.

Integrating React Router navigation with Real User Monitoring

Raygun’s Real User Monitoring provides you with actionable insights into why your users were affected by performance problems. For example, you can see:

  • Which pages they view
  • How they navigate through your website
  • How long pages and assets take to load and (if you have Crash Reporting as well), when and how often they experience problems

This provides invaluable insight into the experience of the users on your site and potential areas that could be optimized to improve your user experience.

For simpler websites that perform a full page refresh to transition from page to page Raygun4JS will automatically track each page view and collate them into a single session for that user. However, for more complicated, richer sites that rely on a Single Page navigation style Raygun4JS has no way of knowing what constitutes a different page. However, functionality is provided to inform Raygun4JS when something that constitutes a page transition occurs.

I will provide an example of how to track this information using React Router as it is React’s most common routing solution. If you use another router the general concept will be the same, you will just need to adapt it to whatever mechanism the router provides for hooking into transition events.

Tracking React Router navigation change events with Raygun4JS is very simple. The first step that you will need to do is, if you haven’t already, get the router’s location accessible using the useLocation API.

Once you have the location available stored as local variable, all you need to do is add a effect hook to your navigation component and pass the dependency to it. Inside this hook we can call the trackEvent option and wire up the appropriate payload.

let location = useLocation();

useEffect(function() {
  rg4js('trackEvent', { type: 'pageView', path: location.pathname });
}, [location]);

This will cause Raygun4JS to track every navigation change triggered from your application and attach it to the currently active Real User Monitoring session. See below for a complete example:

// App.js
import { useEffect } from "react";
import {
  Routes,
  Route,
  Link,
  useLocation 
} from "react-router-dom";
import rg4js from 'raygun4js';

function App() {

  let location = useLocation();

  useEffect(function() {
    rg4js('trackEvent', { type: 'pageView', path: location.pathname });
  }, [location]);

  return (
    <>
      <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
        </ul>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </>
  );
}

function Home() { return <p>Home</p> }
function About() { return <p>About</p> }

export default App;
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter as Router } from "react-router-dom";
import rg4js from 'raygun4js';
import App from './App';

rg4js('apiKey', process.env.REACT_APP_RAYGUN_API_KEY);
rg4js('enableCrashReporting', true);
rg4js('enablePulse', true);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Router>
      <App />
    </Router>
  </React.StrictMode>
);

Timing how long your application takes to render on page load with Real User Monitoring Custom Timings

While Real User Monitoring automatically tracks an extensive set of performance timings related to page and assets loading, you can also attach your own custom timing events with Real User Monitoring Custom Timings. Using this functionality you can very easily include your own timings for any unique timings to your application that you wish to be informed about on a per page basis. For this example, I will use a common one, how long it takes for your Application to render on the initial page load.

While React provides excellent performance tracking tools, these only work using the development build of React. Unfortunately, this means that you can’t plug these tools in on production to find out how long it takes for your React components to render.

However, using the performance.now() API you can get a good approximation of the time it took for your App to fully render. This can be achieved by taking the difference between the time that a component was constructed and the time that the useEffect hook fires (after the components render method has finished, which means all of its children have finished rendering as well).

While this isn’t as precise as the React instrumentation and could be affected by some other factors than just rendering it should be close to the true time. Once you have the duration that the render took you can use the Raygun4JS custom timings message to send the timing to Raygun.

Here’s an example of applying this technique to a componenet to have its rendering time (and that of its children) reported to Raygun.

// LoggedRenderComponent.js
import { useEffect, useState } from "react";
import rg4js from 'raygun4js';

function LoggedRenderComponent() {
  let boot = window.performance ? performance.now() : 0;

  useEffect(() => {
    const renderTime = window.performance ? performance.now() - boot : 0;
    console.log(`Component render time: ${renderTime}ms`);

    rg4js('trackEvent', {
      type: 'customTiming',
      name: 'render',
      duration: renderTime
    });
  }, [boot]);

  return (<p>LoggedRenderComponent example</p>);
}

export default LoggedRenderComponent;

Including Redux state in your errors

Along with all the default information about the browser environment, user information and breadcrumbs that Raygun4JS collects you can supply your own data with the crash reporting payload that can contain any unique information that you are interested in.

There are two different levels of integration you can do here depending on what errors you would like to have Redux data attached to them.

If there is a specific portion of the Redux state tree you would like attached to every error that happens you can configure rg4js using 'withCustomData' in the file that creates the store to extract the state out of the store and add it into the custom data object.

This can be achieved with the following snippet of code:

rg4js('withCustomData', () => {
  const state = store.getState();

  return { state };
});

With this, I am attaching the entire store state to the payload, but that is because my state is actually a primitive value. This also assumes you have your store saved to a variable called store.

Using this setup, any time an error occurs in your application it will pull the configured state out of the redux store and attach it to your error payload.

If you don’t want the store state included in all your errors you can go for a more targeted approach and only attach the Redux state to errors that occur when processing Redux actions using a simple middleware.

Including the following middleware in your store setup will cause it to only attach Redux state to errors that occur during processing of actions.

// errorReportingMiddleware.js
const errorReportingMiddleware = store => next => action => {
  try {
    return next(action);
  } catch(err) {
    console.error('Caught an exception!', err);

    rg4js('send', {
      error: err,
      customData: {
        action,
        state: store.getState()
      }
    });
  }
};

export default errorReportingMiddleware;

An example Redux store configuration with both of these techniques applied (not that it should be done, but to show how both are implemented) can be seen here:

// App.js
import { createStore, applyMiddleware } from 'redux';
import errorReportingMiddleware from './errorReportingMiddleware';

function App(state = 0, action) {
  // Simulate an error
  if (state === 5)
    null.bar

  switch(action.type) {
    case 'increment':
      return state + 1;
    case 'decrement':
      return state - 1;
  }

  return state;
}

// Targeted approach, only attach Redux state to errors that happen during Redux actions
// Create the store, including the crashReportingMiddleware
const store = createStore(App, applyMiddleware(errorReportingMiddleware));

// Global approach, attach Redux state to any error that occurs on the page
// Attach a global Raygun4JS custom data handler
rg4js('withCustomData', () => {
  const state = store.getState();

  return { state };
});

export default store;

Including Redux actions in the Breadcrumbs trail

Breadcrumbs are a useful feature that can provide you with context about the actions performed in your application leading up to what caused the error. Raygun4JS will automatically create breadcrumbs for things like XHR requests, element clicks, navigation events etc but you can also manually include your own Breadcrumbs. In a Redux application, it can be quite useful to include a breadcrumb for each action executed so you can see how the state of the store changed leading up to an error.

Logging a trail of all the actions that flow through your Redux reducers is easily done by a small bit of middleware. All the middleware does is create a breadcrumb after each action has executed with the resulting change.

// breadcrumbMiddleware.js
const breadcrumbMiddleware = store => next => action => {
  const previousState = store.getState();
  const nextAction = next(action);
  const state = store.getState();

  rg4js('recordBreadcrumb', {
    message: `Executing Redux action '${action.type}'`,
    metadata: {
      action,
      state,
      previousState
    },
    level: 'info'
  });

  return nextAction;
};

export default breadcrumbMiddleware;

One thing to be mindful of here is that each error payload to Raygun is limited to 128KB so if your store state is large it is best not to include the entire store state in the Breadcrumb, twice in this case, as it may lead to your error payload being rejected. Instead, try to remove sections of it that wouldn’t be useful for reproducing errors. A maximum of 32 Breadcrumbs is kept at any point in time.

.catch() swallowing errors in Promise chains

Something that has caught me out before and can be a potential source of confusion around missing errors is the fact that adding a .catch call to the end of a promise chain swallows all the errors inside of it. This includes errors thrown during the rendering of a component if at some point in the Promise chain a component rerender is triggered, say by dispatching a Redux action.

This issue comment by @gaeron provides further information and the correct method of handling Promise rejections.

If you do use a .catch call and wish to be informed about the error caught by .catch you will either need to manually log it to Raygun4JS with rg4js('send', ...) or rethrow the error after handling it to cause the global exception handler to pick up on it.

Thanks for reading!