Guide complet du monitoring d'erreurs frontend : Sentry, SourceMap et capture d'exceptions transparente pour l'utilisateur
Pourquoi le monitoring d'erreurs frontend est indispensable
Les utilisateurs confrontés à des écrans blancs, des boutons qui ne répondent pas ou des erreurs d'API en production — si vous n'en êtes pas informé, vous ne pouvez pas les corriger. Le monitoring d'erreurs frontend est le pont qui relie l'expérience réelle de l'utilisateur à la conscience du développeur.
| Coût de l'absence de monitoring | Bénéfice du monitoring |
|---|---|
| Bugs découverts uniquement par les plaintes des utilisateurs | Détection d'anomalies en temps réel, réponse à la minute |
| Problèmes sporadiques non reproductibles | Stack traces complets + replay du comportement utilisateur |
| Incidents en production durant des heures | Alertes automatiques, MTTR drastiquement réduit |
| Corrections basées sur des suppositions | Basé sur les données, localisation précise |
Les cinq sources d'erreurs frontend
1. Erreurs de syntaxe et d'exécution
// ReferenceError: x is not defined
console.log(x);
// TypeError: Cannot read properties of undefined
const user = undefined;
user.name;
2. Erreurs de chargement de ressources
// Échecs de chargement d'images, scripts, feuilles de style
const img = new Image();
img.src = '/broken-image.png';
img.onerror = () => {
trackError({ type: 'resource', tag: 'img', src: img.src });
};
3. Exceptions Promise non capturées
// Le type d'erreur le plus facilement négligé
fetch('/api/data')
.then(res => res.json())
// .then lève une exception mais pas de .catch
.then(data => data.results.map(r => r.name));
4. Exceptions d'API
// Codes d'erreur HTTP + codes d'erreur métier
fetch('/api/order')
.then(res => {
if (!res.ok) {
trackError({ type: 'http', status: res.status, url: res.url });
}
return res.json();
});
5. Erreurs Web Worker / iframe
// Les exceptions internes du Worker ne remontent pas vers le thread principal
const worker = new Worker('/worker.js');
worker.onerror = (e) => {
trackError({
type: 'worker',
message: e.message,
filename: e.filename,
lineno: e.lineno,
});
};
Capture globale d'erreurs : Quatre lignes de défense
Ligne de défense 1 : window.onerror
window.onerror = (message, source, lineno, colno, error) => {
trackError({
type: 'runtime',
message,
source,
lineno,
colno,
stack: error?.stack,
});
return false; // Don't suppress default console output
};
Limitation : Ne peut pas capturer les erreurs de chargement de ressources ni les exceptions Promise.
Ligne de défense 2 : window.addEventListener('error')
window.addEventListener('error', (event) => {
if (event.target instanceof HTMLElement) {
// Erreur de chargement de ressource
trackError({
type: 'resource',
tagName: event.target.tagName,
src: event.target.src || event.target.href,
});
} else {
// Erreur d'exécution (peut dupliquer avec onerror, déduplication nécessaire)
trackError({
type: 'runtime',
message: event.message,
stack: event.error?.stack,
});
}
}, true); // Utiliser la phase de capture
Ligne de défense 3 : window.addEventListener('unhandledrejection')
window.addEventListener('unhandledrejection', (event) => {
trackError({
type: 'unhandledRejection',
reason: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
});
});
Ligne de défense 4 : React Error Boundary
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
trackError({
type: 'react',
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
});
}
render() {
if (this.state.hasError) {
return <FallbackUI onRetry={() => this.setState({ hasError: false })} />;
}
return this.props.children;
}
}
Désobfuscation SourceMap : Restaurer les vrais stack traces à partir du code minifié
Le code de production est minifié et obfusqué, rendant les stack traces d'erreurs totalement illisibles. La désobfuscation SourceMap est la capacité centrale pour le dépannage en production.
Exemple de stack trace minifié
TypeError: Cannot read properties of undefined (reading 'name')
at a (main.abc123.js:1:2345)
at o (main.abc123.js:1:5678)
at t (main.abc123.js:1:9012)
Stack trace réel désobfusqué
TypeError: Cannot read properties of undefined (reading 'name')
at getUserInfo (src/services/user.ts:42:15)
at fetchDashboard (src/pages/dashboard.tsx:128:22)
at useEffectCallback (src/pages/dashboard.tsx:115:5)
Stratégies de sécurité SourceMap
| Stratégie | Sécurité | Complexité | Recommandé pour |
|---|---|---|---|
| NPM privé + désobfuscation en CI | Élevée | Moyenne | La plupart des équipes |
| Désobfuscation côté serveur avec Sentry | Moyenne | Faible | Intégration rapide |
| SourceMap Base64 en ligne | Faible | Faible | Environnement de développement uniquement |
| Fichiers .map indépendants en production | Très faible | Faible | Non recommandé |
// Deobfuscate using the source-map library
import { SourceMapConsumer } from 'source-map';
async function applySourceMap(position, mapContent) {
const consumer = await new SourceMapConsumer(mapContent);
const original = consumer.originalPositionFor({
line: position.line,
column: position.column,
});
return original;
}
Intégration de Sentry en pratique
Installation et initialisation
npm install @sentry/react @sentry/tracing
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: 'https://xxx@o123456.ingest.sentry.io/789',
environment: process.env.NODE_ENV,
release: process.env.APP_VERSION,
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0.01,
replaysOnErrorSampleRate: 1.0,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration(),
],
beforeSend(event, hint) {
// Filter meaningless errors
if (event.exception?.values?.[0]?.type === 'ResizeObserver loop limit exceeded') {
return null;
}
return event;
},
});
Intégration avec React Router
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
function App() {
return (
<SentryRoutes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</SentryRoutes>
);
}
Signalement manuel et fils d'Ariane
// Signalement manuel d'exceptions métier
Sentry.captureException(new Error('Order payment timeout'), {
tags: { orderId: 'ORD-12345', paymentMethod: 'alipay' },
extra: { retryCount: 3, lastAttempt: Date.now() },
});
// Ajouter des fils d'Ariane (trace du comportement utilisateur)
Sentry.addBreadcrumb({
category: 'ui.click',
message: 'Clicked submit button',
level: 'info',
});
Construire un système de monitoring léger
Si vous ne souhaitez pas dépendre de services tiers, vous pouvez construire le vôtre :
SDK de collecte de données
class ErrorMonitor {
constructor(options) {
this.endpoint = options.endpoint;
this.queue = [];
this.timer = null;
this.install();
}
install() {
window.addEventListener('error', this.handleError.bind(this), true);
window.addEventListener('unhandledrejection', this.handleRejection.bind(this));
}
handleError(event) {
this.report({
type: event.target instanceof HTMLElement ? 'resource' : 'runtime',
message: event.message || event.target?.outerHTML,
stack: event.error?.stack,
timestamp: Date.now(),
url: location.href,
ua: navigator.userAgent,
});
}
handleRejection(event) {
this.report({
type: 'unhandledRejection',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
timestamp: Date.now(),
});
}
report(data) {
this.queue.push(data);
if (!this.timer) {
this.timer = setTimeout(() => this.flush(), 5000);
}
}
flush() {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0);
navigator.sendBeacon(this.endpoint, JSON.stringify(batch));
this.timer = null;
}
}
Tableau de bord des métriques clés
| Métrique | Calcul | Seuil d'alerte |
|---|---|---|
| Taux d'erreurs JS | PV d'erreurs / PV total | > 0.1% |
| Taux d'échec API | Nombre de 5xx / Total des requêtes | > 1% |
| Taux de crash au premier écran | PV d'écran blanc / PV total | > 0.01% |
| Temps moyen de réparation | Temps de la première alerte au déploiement | > 30min |
Liste de contrôle des meilleures pratiques
- Couverture complète avec quatre lignes de défense : onerror + événement error + unhandledrejection + Error Boundary
- Pas de SourceMap en production : Artefacts de build séparés, désobfuscation dans l'environnement CI
- Échantillonnage et filtrage des erreurs : Empêcher les erreurs fréquentes sans signification de noyer les problèmes réels
- Replay du comportement utilisateur : Fils d'Ariane + Session Replay pour accélérer la localisation
- Niveaux d'alerte : P0 notification immédiate, P1 suivi par ticket, P2 résumé dans le rapport hebdomadaire
- Association de versions : Chaque release porte un numéro de version, les erreurs sont automatiquement associées aux changements de code
- Conformité au respect de la vie privée : Nettoyer les données rapportées, ne pas inclure d'informations personnelles utilisateur
- Corrélation de performance : Lier le monitoring d'erreurs au monitoring de performance pour découvrir les causes racines de la dégradation des performances
Essayez ces outils exécutés localement dans le navigateur — aucune inscription requise →