Skip to content
Colin Wren
Twitter

Monitoring crashes in your React Native app with Sentry & Expo

DevOps, JavaScript, Software Development4 min read

sparkler
Photo by Matt Palmer on Unsplash

In December 2020 JiffyCV went into beta and I got to experience the highs of getting something I’d worked hard on into people’s hands and the lows of those people then reporting app crashing bugs that I hadn’t found during development.

The key to fixing a bug is knowing the exact steps to take to reproduce it and then how those steps cause a deviation in the intended behaviour, but when you’re not experiencing this first hand (and sometimes when you are) it’s incredibly hard to get the information you need to reproduce those steps.

The best way of giving yourself more information about the crash is to use a tool like Sentry that can capture exceptions thrown during the running of the app and log these to a central server.

Sentry then allows you to view the stack traces & additional contextual information needed to diagnose the crash and also allows you to manage crashes so that they only show again after the next release, to fix the issue when it has gone out for instance.

More importantly for me as I’m self-funding the app is that they give you 5000 events a month for free which means that I could roll out the tool for the beta test without having to pay anything.

Adding Sentry to your Expo app

The Expo team provide a package called sentry-expo which wraps the React Native version of the Sentry agent to work with Expo.

The setup is pretty minimal, just requiring the installation of the package and some configuration to ensure that Sentry is sending events to your instance.

Once the agent is set up, the next time you build a standalone app or publish a new set of assets using Expo it will create a new release in Sentry and once a user downloads the updated app you’ll start receiving crash information to the Sentry client.

Using breadcrumbs to build context around crashes

While Sentry will log the stack trace from the crashes it receives, these may not provide much information to work with if the code that caused the crash is common code shared across many features of your app.

In order to make it easier to debug a crash Sentry provides a means to build up a context of the actions the user was taking when the app crashed called ‘breadcrumbs’.

To use breadcrumbs you add calls to Sentry.Native.addBreadcrumb() to your code when key events in your user journeys happen such as user interactions or system processes that may result in a crash.

1import React, { useState } from 'react';
2import {loadAsync} from "expo-font";
3import * as Sentry from 'sentry-expo';
4import {AppLoading} from "expo";
5
6Sentry.init({
7 // Sentry config
8});
9
10async function loadState(setState) {
11 Sentry.Native.addBreadcrumb({
12 type: 'user',
13 category: 'openApp',
14 message: 'Loading resources',
15 });
16 await loadAsync({
17 // load some fonts
18 });
19 Sentry.Native.addBreadcrumb({
20 type: 'transaction',
21 category: 'sentry.transaction',
22 message: 'Loaded fonts',
23 });
24}
25
26export default function App() {
27 const [stateLoaded, setStateLoaded] = useState(false);
28 const [state, setState] = useState(null);
29 if (!stateLoaded) {
30 return (
31 <AppLoading
32 startAsync={() => loadState(setState)}
33 onFinish={() => setStateLoaded(true)}
34 onError={(error) => {
35 Sentry.Native.captureException(error);
36 }}
37 />
38 )
39 } else {
40 return (
41 // App
42 )
43 }
44}
Adding breadcrumbs to indicate when stages of the app’s loading process have completed

With the breadcrumbs in place, when the user is using your app and the app crashes the breadcrumbs, data is also sent to the Sentry server along with the stack trace allowing you to see those breadcrumbs in the stack trace timeline.

This context information means that you can, assuming you’ve put the breadcrumbs in the right places, see the actions the user took before the app crashed and use this information to build a set of steps to follow in order to reproduce the defect.

Using versioning and environments for greater context

As part of the Sentry.init() call you can define information on the version and the environment for the running application. This can be used in conjunction with the breadcrumbs to narrow down which code is causing issues, especially if you’re building multiple applications, such as for use with Expo’s release channel functionality.

We are using the release channels to run a beta version of JiffyCV while also releasing a production version to the app stores, so being able to set the environment to reflect which configuration of the app is running is key to understanding when the app crashes.

In order to implement this I moved the configuration parameters used to initialise Sentry into a JSON file that I can then amend using jq before running expo build on the CI as part of the build process.

This means that I don’t need to actively maintain the configuration for the two channels and if I want to add more channels I have a scalable means to support those too.

Apple’s privacy settings

When submitting your app to Apple you will need to declare that you’re capturing the user’s usage data for diagnostic reasons at the very least (As well as the need to declare you’re tracking the users device ID due to Expo’s inclusion of certain frameworks, even if you’re not using them).

If you opt to use the user identifying functionality of Sentry in your app you will have to declare more as part of the privacy questionnaire but I didn’t opt for this so I can’t say what these will need to be.

Personally I don’t see the logging of usage diagnostics as being a deal breaker for when I buy an app so I think the ability to fix bugs easily is worth the potential loss in revenue due to it’s inclusion.

What including Sentry in my Expo app has given me

During the beta, after I set up Sentry I was able to see a lot more information about the crashes happening for users which in turn allowed me to fix the defects that caused the crashes and keep those users engaged with the beta process.

I was also able to identify a number of crashes that no user had reported (potentially due to React Native showing the user a white screen for a second when a crash happens, so they may have not realised) which meant I was able to fix a whole new range of bugs I wouldn’t have been aware of.

Another side-effect of using Sentry was that I was able to build a picture of common user journeys and where in those journeys people were encountering issues. This meant I could focus my testing efforts in these areas as bugs tend to cluster.

Additionally the use of breadcrumbs allowed me to identify potentially performance-impacting issues with the way I had implemented a useEffect hook in one of the screens of the app. This showed up as a hook I expected to be called once, being called 20 times because the screen was still in the background listening to state changes.

Summary

If you’re using any tooling for monitoring crashes then I highly recommend you check out Sentry. I’ve used it for a number of projects across different environments such as server-side and in my React Native app — JiffyCV.

The issue reporting and contextual information that you can capture makes the bug fixing process far easier than relying on user feedback alone and you may even encounter bugs that the user has no idea are happening.

Even if you don’t proactively monitor bugs that happen in production you can still use Sentry as a means to monitor that bug fixes have actually fixed a bug using Sentry’s release-based error reporting.