Полное руководство по мониторингу ошибок фронтенда: Sentry, SourceMap и прозрачный для пользователя перехват исключений

前端工程

Почему мониторинг ошибок фронтенда незаменим

Пользователи сталкиваются с пустыми экранами, неработающими кнопками или ошибками API в продакшене — если вы об этом не знаете, вы не можете это исправить. Мониторинг ошибок фронтенда — это мост, соединяющий реальный пользовательский опыт с осведомлённостью разработчиков.

Цена отсутствия мониторинга Преимущество мониторинга
Баги обнаруживаются только через жалобы пользователей Обнаружение аномалий в реальном времени, реакция на уровне минут
Невоспроизводимые спорадические проблемы Полные трассировки стека + воспроизведение поведения пользователя
Продакшен-инциденты, длящиеся часами Автоматические оповещения, радикально сниженный MTTR
Исправления, основанные на догадках На основе данных, точная локализация

Пять источников ошибок фронтенда

1. Синтаксические ошибки и ошибки времени выполнения

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

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

2. Ошибки загрузки ресурсов

// Ошибки загрузки изображений, скриптов, стилей
const img = new Image();
img.src = '/broken-image.png';
img.onerror = () => {
  trackError({ type: 'resource', tag: 'img', src: img.src });
};

3. Необработанные исключения Promise

// Самый легко упускаемый тип ошибок
fetch('/api/data')
  .then(res => res.json())
  // .then выбрасывает исключение, но нет .catch
  .then(data => data.results.map(r => r.name));

4. Исключения API

// HTTP-коды ошибок + бизнес-коды ошибок
fetch('/api/order')
  .then(res => {
    if (!res.ok) {
      trackError({ type: 'http', status: res.status, url: res.url });
    }
    return res.json();
  });

5. Ошибки Web Worker / iframe

// Внутренние исключения Worker не всплывают в основной поток
const worker = new Worker('/worker.js');
worker.onerror = (e) => {
  trackError({
    type: 'worker',
    message: e.message,
    filename: e.filename,
    lineno: e.lineno,
  });
};

Глобальный перехват ошибок: Четыре линии обороны

Линия обороны 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
};

Ограничение: Не может перехватывать ошибки загрузки ресурсов или исключения Promise.

Линия обороны 2: window.addEventListener('error')

window.addEventListener('error', (event) => {
  if (event.target instanceof HTMLElement) {
    // Ошибка загрузки ресурсов
    trackError({
      type: 'resource',
      tagName: event.target.tagName,
      src: event.target.src || event.target.href,
    });
  } else {
    // Ошибка времени выполнения (может дублировать onerror, требуется дедупликация)
    trackError({
      type: 'runtime',
      message: event.message,
      stack: event.error?.stack,
    });
  }
}, true); // Использовать фазу перехвата

Линия обороны 3: window.addEventListener('unhandledrejection')

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

Линия обороны 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;
  }
}

Деобфускация SourceMap: Восстановление реальных трассировок стека из минифицированного кода

Продакшен-код минифицирован и обфусцирован, что делает трассировки стека ошибок совершенно нечитаемыми. Деобфускация SourceMap — это ключевая возможность для устранения проблем в продакшене.

Пример минифицированной трассировки стека

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)

Деобфусцированная реальная трассировка стека

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)

Стратегии безопасности SourceMap

Стратегия Безопасность Сложность Лучше всего для
Приватный NPM + деобфускация в CI Высокая Средняя Большинство команд
Серверная деобфускация Sentry Средняя Низкая Быстрая интеграция
Base64 встроенный SourceMap Низкая Низкая Только для среды разработки
Независимые .map-файлы в продакшене Очень низкая Низкая Не рекомендуется
// 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;
}

Интеграция Sentry на практике

Установка и инициализация

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

Интеграция с React Router

const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);

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

Ручная отправка и хлебные крошки

// Ручная отправка бизнес-исключений
Sentry.captureException(new Error('Order payment timeout'), {
  tags: { orderId: 'ORD-12345', paymentMethod: 'alipay' },
  extra: { retryCount: 3, lastAttempt: Date.now() },
});

// Добавление хлебных крошек (след поведения пользователя)
Sentry.addBreadcrumb({
  category: 'ui.click',
  message: 'Clicked submit button',
  level: 'info',
});

Создание легковесной системы мониторинга

Если вы не хотите зависеть от сторонних сервисов, вы можете создать свою собственную:

SDK сбора данных

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

Панель ключевых метрик

Метрика Расчёт Порог оповещения
Частота ошибок JS PV ошибок / Общий PV > 0.1%
Частота отказов API Количество 5xx / Общее количество запросов > 1%
Частота сбоев первого экрана PV пустого экрана / Общий PV > 0.01%
Среднее время восстановления Время от первого оповещения до развёртывания > 30мин

Контрольный список лучших практик

  1. Полное покрытие четырьмя линиями обороны: onerror + событие error + unhandledrejection + Error Boundary
  2. Без SourceMap в продакшене: Отдельные артефакты сборки, деобфускация в среде CI
  3. Сэмплирование и фильтрация ошибок: Предотвращение того, чтобы высокочастотные бессмысленные ошибки затмевали реальные проблемы
  4. Воспроизведение поведения пользователя: Хлебные крошки + Session Replay для ускорения локализации
  5. Многоуровневые оповещения: P0 немедленное уведомление, P1 отслеживание по тикету, P2 еженедельный сводный отчёт
  6. Привязка версий: Каждый релиз несёт номер версии, ошибки автоматически связываются с изменениями кода
  7. Соответствие требованиям конфиденциальности: Очищать отправляемые данные, не включать личную информацию пользователей
  8. Корреляция производительности: Связывать мониторинг ошибок с мониторингом производительности для обнаружения корневых причин деградации производительности

Попробуйте эти локальные браузерные инструменты — регистрация не требуется →

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