Полное руководство по мониторингу ошибок фронтенда: 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мин |
Контрольный список лучших практик
- Полное покрытие четырьмя линиями обороны: onerror + событие error + unhandledrejection + Error Boundary
- Без SourceMap в продакшене: Отдельные артефакты сборки, деобфускация в среде CI
- Сэмплирование и фильтрация ошибок: Предотвращение того, чтобы высокочастотные бессмысленные ошибки затмевали реальные проблемы
- Воспроизведение поведения пользователя: Хлебные крошки + Session Replay для ускорения локализации
- Многоуровневые оповещения: P0 немедленное уведомление, P1 отслеживание по тикету, P2 еженедельный сводный отчёт
- Привязка версий: Каждый релиз несёт номер версии, ошибки автоматически связываются с изменениями кода
- Соответствие требованиям конфиденциальности: Очищать отправляемые данные, не включать личную информацию пользователей
- Корреляция производительности: Связывать мониторинг ошибок с мониторингом производительности для обнаружения корневых причин деградации производительности
Попробуйте эти локальные браузерные инструменты — регистрация не требуется →
#错误监控#Sentry#SourceMap#异常捕获#DevOps