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

  1. Couverture complète avec quatre lignes de défense : onerror + événement error + unhandledrejection + Error Boundary
  2. Pas de SourceMap en production : Artefacts de build séparés, désobfuscation dans l'environnement CI
  3. Échantillonnage et filtrage des erreurs : Empêcher les erreurs fréquentes sans signification de noyer les problèmes réels
  4. Replay du comportement utilisateur : Fils d'Ariane + Session Replay pour accélérer la localisation
  5. Niveaux d'alerte : P0 notification immédiate, P1 suivi par ticket, P2 résumé dans le rapport hebdomadaire
  6. Association de versions : Chaque release porte un numéro de version, les erreurs sont automatiquement associées aux changements de code
  7. Conformité au respect de la vie privée : Nettoyer les données rapportées, ne pas inclure d'informations personnelles utilisateur
  8. 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 →

#错误监控#Sentry#SourceMap#异常捕获#DevOps