react-nativefirebasecrashlyticserror-boundary

Identifier les bugs dans votre application React Native avec Firebase Crashlytics

Intégrer crashlytics de firebase à votre application React Native.

Le “crash reporting” vous connaissez ? C’est une des notions les plus importantes afin d’assurer la bonne qualité de votre application.

Les bugs et crashes qui appairaissent dans votre application une fois déployée, peuvent être très difficile à taquer. Vous ne serez sûrement pas en contact avec les utilisateurs afins qu’ils puissent vous détailler ce qu’il s’est passé. Et encore pire, l’utilisateur vous laissera sûrement une mauvaise note.

Dans ce tutoriel, nous allons voir comment intégrer le module crashlytics à votre applicatin react native.

Ce tutoriel nécessite que vous ayez complété notre première partie sur l’initialisation de firebase avec React Native.

Installation

Le module @react-native-firebase/crashlytics propose des fonctionnalités utiles pour les développeurs.

Créer le projet dans la console Firebase

Allez sur la console Firebase, assurez-vous que votre projet est créé, sinon suivez l’article précédent.

Intégrer @react-native-firebase/crashlytics

Exécutez dans un terminal dans votre projet :

yarn add @react-native-firebase/crashlytics
cd ios/ && pod install

Pour la partie android, dans android/build.gradle :

// ..
buildscript {
  // ..
  dependencies {
    // ..
    classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'
  }
  // ..
}

Puis dans android/app/build.gradle :

apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services' // apply after this line
apply plugin: 'com.google.firebase.crashlytics'
// ..

Puis build à nouveau le projet avec :

npx react-native run-android

Tester votre configuration

Afin d’être sûr que le paramétrage fonctionne, créez d’abord un fichier firebase.json à la racine de votre projet, en y ajoutant :

{
  "react-native": {
    "crashlytics_auto_collection_enabled": true,
    "crashlytics_debug_enabled": true, // c'est temporaire, on va remettre à false plus tard 
    "crashlytics_javascript_exception_handler_chaining_enabled": false
  }
}

crashlytics_debug_enabled permet de dire à Crashlytics d’également tracker les bugs en mode debug. C’est important pour notre premier test.

Ensuite, soit lors d’un clic de bouton, soit au chargement d’un screen, insérez ce bout de code :

import crashlytics from '@react-native-firebase/crashlytics';

// ..

crashlytics().crash();

Allez ensuite sur la console Firebase, puis attendez 1 ou 2 minutes pour voir apparaître votre erreur :

Erreur sur la console Firebase

Testez la manip’ sur les 2 simulateurs iOS et android pour être sûr que tout fonctionne.

Maintenant, on peut passer aux cas pratiques !

Intégration d’un ErrorBoundary

Explication

Crashlytics va maintenant attraper les erreurs qui proviennent dans votre application, et vous les lister dans la console. Mais comme vous le savez, le code react native est compilé en natif, donc vous perdez votre code javascript lors de la release.

Donc vos stack d’erreurs ne seront pas très lisible.

Exemple de stack d’erreur reporté par crashlytics.

Fatal Exception: java.lang.ClassCastException: com.facebook.react.bridge.ReadableNativeMap cannot be cast to java.lang.String
       at com.facebook.react.bridge.ReadableNativeArray.getString(ReadableNativeArray.java:102)
       at com.facebook.react.bridge.JavaMethodWrapper$5.extractArgument(JavaMethodWrapper.java:73)
       at com.facebook.react.bridge.JavaMethodWrapper$5.extractArgument(JavaMethodWrapper.java:69)
       at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:356)
       at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:151)
       at com.facebook.react.bridge.queue.NativeRunnable.run(NativeRunnable.java)

La méthode pour récupérer l’erreur javascript est la suivante :

crashlytics().recordError(error);

ErrorBoundary

L’explication étant maintenant terminée, passons à la bonne pratique. Le composant ErrorBoundary permet de rattraper les erreurs qui se produisent dans le code javascript, et de montrer une vue plus propre à l’utilisateur, au lieu de faire cracher l’application.

Comme par exemple :

Exemple page erreur

Nous allons mettre en place ce composant, et envoyer les erreurs à Crashlytics à ce moment.

Dans component/ErrorBoundary.js :

import crashlytics from '@react-native-firebase/crashlytics';
import { node } from 'prop-types';
import React, { Component } from 'react';
import { SafeAreaView, Text, View } from 'react-native';
import RNRestart from 'react-native-restart'

import Button from '~/components/common/Button';
import t from '~/configs/i18n';

import styles from './styles';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);

    this.state = {};
  }

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {   
    if(errorInfo && errorInfo.componentStack) {
      crashlytics().log(errorInfo.componentStack);
    }
    crashlytics().recordError(error, error.message ? error.message : undefined);
  }

  onRestart() {
    RNRestart.Restart();
  };

  render() {
    const { hasError } = this.state;
    const { children } = this.props;

    if (hasError) {
      return (
        <SafeAreaView style={{ flex: 1 }}>
          <View style={styles.container}>
            <View style={styles.content}>
              <Text style={{ width: '100%', }}>
                500
              </Text>
              <Text style={{ fontSize: 32 }}>{t('label.error')}</Text>
              <Text style={{ marginVertical: 10, lineHeight: 23, fontWeight: '500' }}>
                {t('error.description')}
              </Text>
              <Button
                onPress={() => this.onRestart()}
              >
                {t('action.backToLogin')}
              </Button>
            </View>
          </View>
        </SafeAreaView>
      );
    }

    return children;
  }
}

ErrorBoundary.propTypes = {
  children: node,
};

ErrorBoundary.defaultProps = {
  children: null,
};

export default ErrorBoundary;

Puis dans App.js, entourez votre application avec ce composant :

return (
    <ErrorBoundary>
      <Provider store={store}>
        <PersistGate loading={null} persistor={persistor}>
          {storybookActive ? <StorybookUIRoot /> : <Router />}
        </PersistGate>
      </Provider>
  </ErrorBoundary>
  );

Voila ! Maintenant à chaque crash, nous allons envoyer les informations js à la console Crashlytics de Firebase. Ce que j’aime bien mettre en place, c’est le rechargement de l’application lors du clic sur le bouton, grâce à la dépendance react-native-restart. C’est optionnel bien sûr.

Loggez encore plus d’information

Grâce à l’étape précédente, vous aurez la stacktrace javascript. Mais c’est toujours mieux de rajouter plus d’info (bien sûr, avec parcimonie).

Pour cela 2 méthodes :

crashlytics().log(errorInfo.componentStack); //celle-là permet de logger une activité sur crashlytics
crashlytics().setAttribute(attribute, value); //celle-là permet de mettre un attribut

J’utilise la 2ème méthode pour savoir sur quelle page l’utilisateur se trouvait avant le grand crash.

Si vous utilisez @react-navigation/native :

<NavigationContainer
    theme={AppTheme}
    ref={navigationRef}
    onReady={() => {
      routeNameRef.current = navigationRef.current.getCurrentRoute().name;
      crashlytics().setAttribute("screen", routeNameRef.current);
    }}
    onStateChange={() => {
      const previousRouteName = routeNameRef.current;
      const currentRouteName = navigationRef.current.getCurrentRoute().name;

      if (previousRouteName !== currentRouteName) {
        crashlytics().setAttribute("screen", currentRouteName);
      }

      routeNameRef.current = currentRouteName;
    }}
  >

Conclusion

Et voila ! Ça fait pas mal d’information quand même. Maintenant j’espère que vous allez pouvoir débugger vos applications plus facilement. Ne tracker pas des informations susceptibles d’identifier vos utilisateurs (#rgpd).