Guía completa de monitoreo de errores frontend: Sentry, SourceMap y captura de excepciones transparente para el usuario

前端工程

Por qué el monitoreo de errores frontend es indispensable

Los usuarios que encuentran pantallas en blanco, botones que no responden o errores de API en producción — si no lo sabes, no puedes solucionarlo. El monitoreo de errores frontend es el puente que conecta la experiencia real del usuario con la conciencia del desarrollador.

Costo de no monitorear Beneficio de monitorear
Bugs descubiertos solo por quejas de usuarios Detección de anomalías en tiempo real, respuesta a nivel de minutos
Problemas esporádicos irreproducibles Stack traces completos + repetición del comportamiento del usuario
Incidentes en producción que duran horas Alertas automáticas, MTTR drásticamente reducido
Correcciones basadas en conjeturas Basado en datos, localización precisa

Las cinco fuentes de errores frontend

1. Errores de sintaxis y tiempo de ejecución

// ReferenceError: x is not defined
console.log(x);

// TypeError: Cannot read properties of undefined
const user = undefined;
user.name;

2. Errores de carga de recursos

// Fallos de carga de imágenes, scripts, hojas de estilo
const img = new Image();
img.src = '/broken-image.png';
img.onerror = () => {
  trackError({ type: 'resource', tag: 'img', src: img.src });
};

3. Excepciones Promise no capturadas

// El tipo de error más fácilmente pasado por alto
fetch('/api/data')
  .then(res => res.json())
  // .then lanza una excepción pero no hay .catch
  .then(data => data.results.map(r => r.name));

4. Excepciones de API

// Códigos de error HTTP + códigos de error de negocio
fetch('/api/order')
  .then(res => {
    if (!res.ok) {
      trackError({ type: 'http', status: res.status, url: res.url });
    }
    return res.json();
  });

5. Errores de Web Worker / iframe

// Las excepciones internas del Worker no se propagan al hilo principal
const worker = new Worker('/worker.js');
worker.onerror = (e) => {
  trackError({
    type: 'worker',
    message: e.message,
    filename: e.filename,
    lineno: e.lineno,
  });
};

Captura global de errores: Cuatro líneas de defensa

Línea de defensa 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
};

Limitación: No puede capturar errores de carga de recursos ni excepciones Promise.

Línea de defensa 2: window.addEventListener('error')

window.addEventListener('error', (event) => {
  if (event.target instanceof HTMLElement) {
    // Error de carga de recursos
    trackError({
      type: 'resource',
      tagName: event.target.tagName,
      src: event.target.src || event.target.href,
    });
  } else {
    // Error de tiempo de ejecución (puede duplicarse con onerror, necesita desduplicación)
    trackError({
      type: 'runtime',
      message: event.message,
      stack: event.error?.stack,
    });
  }
}, true); // Usar fase de captura

Línea de defensa 3: window.addEventListener('unhandledrejection')

window.addEventListener('unhandledrejection', (event) => {
  trackError({
    type: 'unhandledRejection',
    reason: event.reason?.message || String(event.reason),
    stack: event.reason?.stack,
  });
});

Línea de defensa 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;
  }
}

Deobfuscación de SourceMap: Restaurando stack traces reales desde código minificado

El código de producción está minificado y ofuscado, lo que hace que los stack traces de errores sean completamente ilegibles. La deobfuscación de SourceMap es la capacidad central para la resolución de problemas en producción.

Ejemplo de stack trace minificado

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 real deobfuscado

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)

Estrategias de seguridad de SourceMap

Estrategia Seguridad Complejidad Mejor para
NPM privado + deobfuscación en CI Alta Media La mayoría de equipos
Deobfuscación del lado del servidor con Sentry Media Baja Integración rápida
SourceMap inline en Base64 Baja Baja Solo entorno de desarrollo
Archivos .map independientes en producción Muy baja Baja No recomendado
// 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;
}

Integración de Sentry en la práctica

Instalación e inicialización

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;
  },
});

Integración con React Router

const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);

function App() {
  return (
    <SentryRoutes>
      <Route path="/" element={<Home />} />
      <Route path="/dashboard" element={<Dashboard />} />
    </SentryRoutes>
  );
}

Reporte manual y breadcrumbs

// Reporte manual de excepciones de negocio
Sentry.captureException(new Error('Order payment timeout'), {
  tags: { orderId: 'ORD-12345', paymentMethod: 'alipay' },
  extra: { retryCount: 3, lastAttempt: Date.now() },
});

// Agregar breadcrumbs (rastro de comportamiento del usuario)
Sentry.addBreadcrumb({
  category: 'ui.click',
  message: 'Clicked submit button',
  level: 'info',
});

Construyendo un sistema de monitoreo ligero

Si no quieres depender de servicios de terceros, puedes construir el tuyo propio:

SDK de recolección de datos

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;
  }
}

Panel de métricas clave

Métrica Cálculo Umbral de alerta
Tasa de errores JS PV de errores / PV total > 0.1%
Tasa de fallos de API Cantidad de 5xx / Total de solicitudes > 1%
Tasa de fallos en primera pantalla PV de pantalla en blanco / PV total > 0.01%
Tiempo medio de reparación Tiempo desde la primera alerta hasta el despliegue > 30min

Lista de verificación de mejores prácticas

  1. Cobertura completa con cuatro líneas de defensa: onerror + evento error + unhandledrejection + Error Boundary
  2. Sin SourceMap en producción: Artefactos de compilación separados, deobfuscación en entorno de CI
  3. Muestreo y filtrado de errores: Evitar que errores de alta frecuencia sin sentido ahoguen los problemas reales
  4. Repetición del comportamiento del usuario: Breadcrumbs + Session Replay para acelerar la localización
  5. Niveles de alerta: P0 notificación inmediata, P1 seguimiento por ticket, P2 resumen en reporte semanal
  6. Asociación de versiones: Cada release lleva un número de versión, los errores se asocian automáticamente con cambios de código
  7. Cumplimiento de privacidad: Sanitizar los datos reportados, no incluir información personal del usuario
  8. Correlación de rendimiento: Vincular el monitoreo de errores con el monitoreo de rendimiento para descubrir las causas raíz de la degradación del rendimiento

Prueba estas herramientas que se ejecutan en tu navegador — no requieren registro →

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