Cours JavaScript et React · Module 16: Gestion des erreurs et Suspense

Journalisation et monitoring des erreurs

5 min de lecture
Dans cette leçon5

Sur un vaisseau spatial, chaque panne est enregistrée dans la boîte noire. Sans elle, l'équipage ne pourrait pas analyser après coup les causes des problèmes. Dans les applications React, nous avons exactement le même problème : si une erreur survient chez un utilisateur en production et que nous ne la journalisons pas, nous n'en saurons jamais rien. La journalisation et le monitoring des erreurs, c'est ta boîte noire de production.

Stratégies de journalisation des erreurs

Dans un environnement de production, il nous faut plusieurs couches de journalisation :

1. console.error - uniquement pour le développement

console.error est utile pendant le développement local, mais en production l'utilisateur ne le voit pas, et toi non plus tu n'y as pas accès.

1// Bon pour le développement, inutile en production
2componentDidCatch(error, errorInfo) {
3  console.error('Erreur attrapée :', error);
4  console.error('Pile des composants :', errorInfo.componentStack);
5}

2. Journalisation vers un service externe

En production, les erreurs doivent être envoyées à un service de monitoring :

1class ErrorBoundary extends React.Component {
2  componentDidCatch(error, errorInfo) {
3    // Envoi au service de monitoring
4    logErrorToService({
5      message: error.message,
6      stack: error.stack,
7      componentStack: errorInfo.componentStack,
8      timestamp: new Date().toISOString(),
9      url: window.location.href,
10      userAgent: navigator.userAgent,
11    });
12  }
13
14  // ...render avec fallback
15}
16
17async function logErrorToService(errorData) {
18  try {
19    await fetch('/api/errors', {
20      method: 'POST',
21      headers: { 'Content-Type': 'application/json' },
22      body: JSON.stringify(errorData),
23    });
24  } catch (e) {
25    // Repli - ne bloque pas l'appli si la journalisation échoue
26    console.error('Échec de la journalisation de l\'erreur :', e);
27  }
28}

Sentry et les services de suivi d'erreurs

Sentry est le service le plus populaire pour le monitoring des erreurs dans les applications JavaScript et React. Parmi les autres outils répandus : LogRocket, Bugsnag, Datadog et New Relic.

Intégration de base de Sentry

1import * as Sentry from '@sentry/react';
2
3// Initialisation dans main/index.js
4Sentry.init({
5  dsn: 'https://key@sentry.io/project',
6  environment: 'production',
7  // Pourcentage de sessions à enregistrer (performance)
8  tracesSampleRate: 0.2,
9});
10
11// Sentry capture automatiquement :
12// - Les exceptions non gérées
13// - Les rejets de Promise non gérés
14// - Les erreurs réseau

Sentry avec Error Boundary

1import * as Sentry from '@sentry/react';
2
3// Sentry fournit une Error Boundary prête à l'emploi
4function App() {
5  return (
6    <Sentry.ErrorBoundary
7      fallback={({ error }) => (
8        <div className="error-page">
9          <h2>Quelque chose s'est mal passé</h2>
10          <p>L'erreur a été signalée automatiquement</p>
11          <button onClick={() => window.location.reload()}>
12            Rafraîchir la page
13          </button>
14        </div>
15      )}
16    >
17      <SpaceStation />
18    </Sentry.ErrorBoundary>
19  );
20}

Contexte et breadcrumbs dans Sentry

Sentry permet d'ajouter du contexte aux erreurs : informations sur l'utilisateur, actions récentes (breadcrumbs) et données supplémentaires :

1// Définir le contexte utilisateur
2Sentry.setUser({
3  id: user.id,
4  email: user.email,
5  username: user.callsign,
6});
7
8// Ajouter manuellement des breadcrumbs (trace des actions de l'utilisateur)
9Sentry.addBreadcrumb({
10  category: 'navigation',
11  message: 'L\'utilisateur est allé sur le panneau de mission',
12  level: 'info',
13});
14
15// Signaler manuellement une erreur avec du contexte supplémentaire
16Sentry.captureException(error, {
17  extra: {
18    missionId: currentMission.id,
19    moduleState: moduleStatus,
20  },
21});

Hook personnalisé de journalisation des erreurs

Tu peux créer ton propre hook qui centralise la logique de journalisation :

1function useErrorLogger() {
2  const logError = useCallback((error, context = {}) => {
3    const errorReport = {
4      message: error.message,
5      stack: error.stack,
6      timestamp: new Date().toISOString(),
7      url: window.location.href,
8      ...context,
9    };
10
11    // En production, envoie au service
12    if (process.env.NODE_ENV === 'production') {
13      sendToErrorService(errorReport);
14    }
15
16    // En développement, journalise dans la console
17    console.error('[ErrorLogger]', errorReport);
18  }, []);
19
20  return { logError };
21}
22
23// Utilisation dans un composant
24function MissionControl() {
25  const { logError } = useErrorLogger();
26
27  const handleDataFetch = async () => {
28    try {
29      const data = await fetchMissionData();
30      setMissions(data);
31    } catch (error) {
32      logError(error, {
33        component: 'MissionControl',
34        action: 'fetchMissionData',
35      });
36      setError('Échec de la récupération des données de mission');
37    }
38  };
39}

Le signalement des erreurs en production

Gestionnaires d'erreurs globaux

En plus des Error Boundaries et du try-catch, il vaut la peine d'ajouter des gestionnaires globaux qui attrapent les erreurs échappant aux autres mécanismes :

1// Capture globale des erreurs non attrapées
2window.addEventListener('error', (event) => {
3  logErrorToService({
4    type: 'uncaught_error',
5    message: event.message,
6    filename: event.filename,
7    lineno: event.lineno,
8    colno: event.colno,
9  });
10});
11
12// Capture globale des rejets de Promise non gérés
13window.addEventListener('unhandledrejection', (event) => {
14  logErrorToService({
15    type: 'unhandled_rejection',
16    message: event.reason?.message || 'Unknown rejection',
17    stack: event.reason?.stack,
18  });
19});

Niveaux de gravité des erreurs

Toutes les erreurs n'ont pas la même importance. Les services de suivi d'erreurs permettent une classification :

1// Erreur critique - l'application est inutilisable
2Sentry.captureException(error, { level: 'fatal' });
3
4// Erreur - une fonctionnalité est cassée, mais l'appli fonctionne
5Sentry.captureException(error, { level: 'error' });
6
7// Avertissement - quelque chose pourrait clocher
8Sentry.captureMessage('API responding slowly', 'warning');
9
10// Info - utile pour le débogage
11Sentry.captureMessage('User changed language', 'info');

Les source maps pour le débogage en production

Le code de production est minifié : la pile d'appels est illisible sans les source maps. Les source maps sont comme une carte de décodage : elles permettent de retraduire les positions du code minifié vers les fichiers source d'origine.

1// Sans source maps, la pile d'appels ressemble à :
2// Error at e.render (main.a1b2c3.js:1:45678)
3
4// Avec source maps :
5// Error at MissionControl.render (MissionControl.jsx:42:15)

Configuration des source maps

1// webpack.config.js
2module.exports = {
3  devtool: 'source-map', // Génère les source maps
4  // ...
5};
6
7// Important : les source maps NE doivent PAS être publiques !
8// Téléverse-les vers Sentry, mais ne les sers pas aux utilisateurs :
9// sentry-cli sourcemaps upload ./build

Les source maps te permettent de voir exactement quelle ligne du code d'origine a provoqué l'erreur, même si le bundle de production est minifié sur une seule ligne. C'est comme avoir les plans techniques d'un vaisseau spatial : au lieu de regarder un panneau minifié incompréhensible, tu vois exactement quel module a lâché et pourquoi. Souviens-toi toutefois de ne jamais rendre les source maps accessibles publiquement : ce serait comme partager les plans du vaisseau avec d'éventuels envahisseurs.

Code de cette leçon : App.jsx
1import React, { useState, useCallback, useEffect } from 'react';
2import './styles.css';
3
4// Hook personnalisé pour journaliser les erreurs
5function useErrorLogger() {
6  const [errorLog, setErrorLog] = useState([]);
7
8  const logError = useCallback((error, context = {}) => {
9    const report = {
10      id: Date.now(),
11      message: error.message || String(error),
12      stack: error.stack ? error.stack.split('\n').slice(0, 3).join('\n') : 'aucune',
13      timestamp: new Date().toLocaleTimeString(),
14      url: window.location.href,
15      ...context,
16    };
17
18    setErrorLog(prev => [report, ...prev].slice(0, 10));
19
20    // En production, on enverrait au service :
21    // sendToSentry(report);
22    console.error('[ErrorLogger]', report);
23  }, []);
24
25  return { logError, errorLog };
26}
27
28// Error Boundary avec journalisation
29class LoggingErrorBoundary extends React.Component {
30  constructor(props) {
31    super(props);
32    this.state = { hasError: false, error: null };
33  }
34
35  static getDerivedStateFromError(error) {
36    return { hasError: true, error };
37  }
38
39  componentDidCatch(error, errorInfo) {
40    // Journalisation vers le "service"
41    if (this.props.onError) {
42      this.props.onError(error, {
43        component: this.props.label || 'Unknown',
44        type: 'render_error',
45        componentStack: errorInfo.componentStack,
46      });
47    }
48  }
49
50  render() {
51    if (this.state.hasError) {
52      return (
53        <div className="error-card">
54          <strong>{this.props.label} : Panne</strong>
55          <p>{this.state.error?.message}</p>
56          <button onClick={() => this.setState({ hasError: false })}>
57            Réessayer
58          </button>
59        </div>
60      );
61    }
62    return this.props.children;
63  }
64}
65
66// Module qui peut lever une erreur
67function UnstableModule({ name, failRate = 0.3 }) {
68  if (Math.random() < failRate) {
69    throw new Error(name + ' : panne critique du système !');
70  }
71  return (
72    <div className="module-ok">
73      <span className="dot green" /> {name} : En ligne
74    </div>
75  );
76}
77
78// Composant avec une erreur async
79function DataPanel({ logError }) {
80  const [data, setData] = useState(null);
81  const [loading, setLoading] = useState(false);
82
83  const fetchData = async () => {
84    setLoading(true);
85    try {
86      await new Promise(r => setTimeout(r, 800));
87      if (Math.random() > 0.5) {
88        throw new Error('Délai dépassé pour la connexion à la base de données');
89      }
90      setData({ missions: 42, crew: 7, fuel: '87%' });
91    } catch (error) {
92      logError(error, {
93        component: 'DataPanel',
94        action: 'fetchData',
95        type: 'async_error',
96      });
97      setData(null);
98    } finally {
99      setLoading(false);
100    }
101  };
102
103  return (
104    <div className="data-panel">
105      <h3>Données de la station</h3>
106      <button onClick={fetchData} className="fetch-btn">
107        {loading ? 'Chargement...' : 'Récupérer les données'}
108      </button>
109      {data && (
110        <ul>
111          <li>Missions : {data.missions}</li>
112          <li>Équipage : {data.crew}</li>
113          <li>Carburant : {data.fuel}</li>
114        </ul>
115      )}
116    </div>
117  );
118}
119
120export default function App() {
121  const { logError, errorLog } = useErrorLogger();
122  const [key, setKey] = useState(0);
123
124  // Gestionnaire global (simulation)
125  useEffect(() => {
126    const handler = (event) => {
127      logError(new Error(event.reason?.message || 'Unhandled rejection'), {
128        type: 'unhandled_rejection',
129      });
130    };
131    window.addEventListener('unhandledrejection', handler);
132    return () => window.removeEventListener('unhandledrejection', handler);
133  }, [logError]);
134
135  return (
136    <div className="app">
137      <h1>Error Logging & Monitoring</h1>
138      <p className="subtitle">
139        Chaque erreur est enregistrée dans le journal (la boîte noire)
140      </p>
141      <button onClick={() => setKey(k => k + 1)} className="reset-btn">
142        Réinitialiser les modules
143      </button>
144
145      <div className="modules" key={key}>
146        <LoggingErrorBoundary label="Navigation" onError={logError}>
147          <UnstableModule name="Navigation" />
148        </LoggingErrorBoundary>
149
150        <LoggingErrorBoundary label="Communication" onError={logError}>
151          <UnstableModule name="Communication" />
152        </LoggingErrorBoundary>
153
154        <DataPanel logError={logError} />
155      </div>
156
157      <div className="log-panel">
158        <h3>Journal des erreurs ({errorLog.length})</h3>
159        {errorLog.length === 0 ? (
160          <p className="empty-log">Aucune erreur enregistrée</p>
161        ) : (
162          errorLog.map(entry => (
163            <div key={entry.id} className="log-entry">
164              <span className="log-time">{entry.timestamp}</span>
165              <span className="log-type">[{entry.type || 'error'}]</span>
166              <span className="log-msg">{entry.message}</span>
167              {entry.component && (
168                <span className="log-comp">({entry.component})</span>
169              )}
170            </div>
171          ))
172        )}
173      </div>
174    </div>
175  );
176}

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Pourquoi console.error est-il insuffisant pour monitorer les erreurs en production ?

  2. 2. Comment Sentry s'intègre-t-il aux Error Boundaries dans React ?

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonne les niveaux de gravité des erreurs du plus élevé au plus bas :

  • Éditeur de code

    Implémente le hook useErrorLogger

  • Clique dans l'ordre

    Clique les éléments dans l'ordre pour construire la journalisation d'erreur dans componentDidCatch :

  • Tri horizontal

    Ordonne la syntaxe correcte pour un gestionnaire global de rejet de Promise :

Articles utiles