React : Suivi des erreurs avec Bugsnag

bugsnag-react

Vendredi 18h30, après une bonne semaine de livraison continue et de revue de code, quoi de tel qu'un product manager qui entre paniqué dans le bureau en vous demandant ce que signifie ce "Uncaught ReferenceError" ?

Bien qu'il ne permette pas d'éviter cette erreur (mais TypeScript peut aider 😁), l'outil Bugsnag sait (entre autres) remonter automatiquement les erreurs non-gérées de votre application.


bugsnag-breadcrumb Breadcrumbs: L'outil trace la succession d'actions ayant provoqué l'erreur

Les avantages sont multiples :

  • On n'attend pas après les retours utilisateurs pour découvrir qu'une fonctionnalité provoque des runtime errors (#réactivité #blagueDrôle)
  • Chaque erreur rapportée peut entraîner le déclenchement d'un Webhook (e.g. créer un issue sur Github, envoyer un message sur Slack...)
  • On peut tracer manuellement des cas critiques avec un maximum de metadata, facilitant la reproduction d'un bug ou usage (state, historique des actions Redux, saisies, ID, etc)

Disclaimer :

  • Sentry est une excellente alternative à Bugsnag
  • Les exemples donnés ci-après nécessitent React 16+ et ses Error Boundaries pour fonctionner.

Après avoir créé un nouveau projet sur Bugsnag (Browser > React), on obtient une clé API permettant d'émettre de nouveaux "événements" (erreur gérée, erreur non-gérée, rapport personnalisé, etc).

nouveau-projet-bugsnag

Il ne reste qu'à intégrer le client au projet !

yarn add @bugsnag/js @bugsnag/plugin-react  
# ou npm install @bugsnag/js @bugsnag/plugin-react

💡 Les paquets sont fournis avec leurs définitions 😌 #typeSafety

On crée un client bugsnag :

// src/<whatever/bugsnag-client.ts
import bugsnag from '@bugsnag/js';

const bugsnagClient = bugsnag({  
  apiKey: BUGSNAG_API_KEY,
  beforeSend: (report) => {
    // les rapports sont ignorés si l'app n'est pas en production
    if (process.env.NODE_ENV !== 'production') {
      report.ignore();
    }

    // le state est ajouté aux données meta du rapport (debugging facilité)
    report.metadata.state = store.getState();
  },
});

export default bugsnagClient;  

Deux options sont utilisées :

  • apiKey, la clé API fournie par Bugsnag
  • beforeSend, un callback appelé avant chaque émission

On peut ajouter toute sorte de données au rapport remonté, facilitant le travail de debug/analyse.

Les erreurs non-gérées : Error Boundary

Pour rapporter automatiquement les erreurs non gérées, on crée un component Error Boundary avec le client :

// src/<whatever>/BugsnagBoundary.ts
import React, { ReactNode }  from 'react';  
import bugsnagReact from '@bugsnag/plugin-react';

import bugsnagClient from './bugsnag-client.ts';

bugsnagClient.use(bugsnagReact, React);

const BugsnagBoundary: FunctionComponent<{ FallbackComponent?: ReactNode }> = bugsnagClient.getPlugin('react');

export default BugsnagBoundary;  

BugsnagBoundary est directement utilisable pour envelopper notre component de plus haut niveau :

<BugsnagBoundary>  
  <App/>
</BugsnagBoundary>  

Cependant, cette solution présente deux défauts :

  • Aucun contenu n'est affiché à l'utilisateur si c'est l'étape de rendu (rendering) qui provoque une erreur. Bien que ladite erreur soit remontée à Bugsnag. Un component de remplacement peut être affiché, cas échéant, en le passant à la prop FallbackComponent.
  • Les erreurs provoquées lors des phases de conception ne sont plus remontées visuellement par les outils de développement (react-error-overlay, react-hot-loader, etc).

On peut wrapper BugsnagBoundary dans un nouveau composant pour y remédier :

// src/<whatever>/ErrorBoundary.ts
import React, { FunctionComponent } from 'react';

const BugsnagBoundary: FunctionComponent<{ FallbackComponent?: ReactNode }> = bugsnagClient.getPlugin('react');

const ErrorBoundary: FunctionComponent = ({ children }) => {  
  if (process.env.NODE_ENV === 'production') {
    return <BugsnagBoundary FallbackComponent={FallbackComponent}>{children}</BugsnagBoundary>;
  }

  return children;
};

export default ErrorBoundary;  

BugsnagBoundary n'est rendu qu'en production et affiche FallbackComponent si children ne peut pas l'être. Autrement, seul children est rendu et l'arbre des components reste inchangé.

💡 FallbackComponent reçoit les props suivantes :

type FallbackComponentProps = {  
  error: Error;
  info: { componentStack: string };
};

On wrap ensuite notre <App/> avec ErrorBoundary.

Les erreurs gérées : notify

Les erreurs gérées (et les rapports personnalisés) peuvent être émis avec notify :

// dans un processus critique (en cas d'erreur), ou dans un flux sensible :
bugsnagClient.notify('Unexpected error during Sign Up', {  
  metaData: { validation: { errors, signUpForm } },
  severity: 'info',
});

Ici aussi, on ajoute un maximum d'informations facilitant la compréhension du contexte (en prenant garde de ne jamais aller à l'encontre de la RGPD 😬).

 Suivi 🧐

Depuis l'inbox (dans le dashboard), on peut suivre en temps quasi-réel les rapports, leur fréquence, la date de dernière occurrence, les assigner à un développeur, les marquer comme Fixed, etc.

bugsnag-react Dans le détail de chaque rapport, on trouve, en plus des Breadcrumbs : la stacktrace, un extrait du code ayant provoqué l'erreur (ou émis le rapport), les données meta rapportées, des données sur l'appareil de l'utilisateur, etc.

📜 Les source maps doivent être hébergées sur Bugsnag, pour permettre le bon usage de cette fonctionnalité (sinon, c'est la portion de code minifiée/transpilée qui est affichée).

Enjoy coding!


Merci à mon très inspirant collègue (et super lead frontend dev 💓) @Timur, pour son indéfectible soutien 🔥.