Error reporting for React applications: Enhanced integration between Raygun4JS and React By Taylor | | 9 min. ( 1769 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:

Integrating React Router navigation with Pulse

Pulse is Raygun’s Real User Monitoring solution which 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 history instance accessible in your top level application component by wrapping it in a withRouter call.

Once you have the history instance accessible via props.history, all you need to do is attach a listener callback to it in your component’s constructor (or add it to an existing listener). This is as simple as adding the following line of code inside the listener callback

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

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

// App.js import { withRouter } from 'react-router' class App extends Component { constructor ( props ) { super ( props ); // The listener this . props . history . listen (( location , action ) => { rg4js ( 'trackEvent' , { type : 'pageView' , path : location . pathname }); }); } render () { return ( < div > < Route exact path = "/" Component = {...} /> < Route exact path = "/Home" Component = {...} /> </ div > ); } } export default withRouter ( App ); // index.js import React from 'react' ; import ReactDOM from 'react-dom' ; import App from './App' ; import { BrowserRouter , Route } from 'react-router-dom' ; ReactDOM . render ( < BrowserRouter > < App /> </ BrowserRouter >, document. getElementById ( 'root' ) );

Timing how long your application takes to render on page load with Pulse Custom Timings

While Pulse automatically tracks an extensive set of performance timings related to page and assets loading, you can also attach your own custom timing events with Pulse 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 componentDidMount event 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.

You can wrap a component with the below function to have its rendering time (and that of its children) reported to Raygun.

import React from 'react' ; // This has not been tested with a browser that doesn't support the window.performance api export default function LogRenderingTime ( Component ) { let boot = 0 ; class LogRenderTime extends React . Component { constructor ( props ) { super ( props ); boot = window. performance ? performance . now () : 0 ; } componentDidMount () { const renderTime = window. performance ? performance . now () - boot : 0 ; rg4js ( 'trackEvent' , { type : 'customTimings' , timings : { custom1 : renderTime } }); } render () { return < Component { ...this.props } />; } } return LogRenderTime ; }

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.

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 that should be done, but to show how both are implemented) can be seen here:

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 ;

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.

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.

Error reporting for React can help

And that’s everything. In conclusion, I hope this information was useful. If you have any tips regarding error reporting for React applications, wish to know more details about anything I’ve covered or something not covered here, let us know in the comments.