前端监控与错误追踪
What — 是什么
前端监控是通过采集页面性能、错误、用户行为等数据,实现线上问题感知、定位和修复的系统性工程。
核心分类:
- 异常监控:JS 运行时错误、Promise 异常、资源加载失败、接口错误
- 性能监控:Web Vitals(LCP/FID/CLS/INP)、首屏时间、接口耗时
- 行为监控:PV/UV、用户点击路径、页面停留时长
- 录屏回放:通过 DOM 快照还原用户操作现场
关键特性:
- 监控的核心价值:发现问题 → 定位问题 → 修复问题
- SourceMap 上传到监控平台,线上代码也能定位到源码行
- 采样率控制成本,不必 100% 采集
Why — 为什么
适用场景:
- 所有线上项目(没有监控 = 盲飞)
- 线上白屏/报错用户反馈时快速定位
- 性能劣化趋势预警
- 用户行为分析辅助产品决策
对比方案:
| 维度 | Sentry | 自建监控 | Google Analytics | LogRocket |
|---|---|---|---|---|
| 错误追踪 | 极好 | 自定义 | 基础 | 好 |
| 性能监控 | 好 | 自定义 | 好 | 好 |
| 录屏回放 | 支持(付费) | 需自研 | 不支持 | 核心功能 |
| SourceMap | 自动还原 | 需自研 | 不支持 | 支持 |
| 成本 | 免费额度 + 付费 | 服务器成本 | 免费 | 付费 |
优缺点:
- ✅ 优点:
- 线上问题从”用户说”变为”数据说”
- 错误聚合去重,避免告警风暴
- SourceMap 还原源码位置
- ❌ 缺点:
- SDK 本身有性能开销(需控制采样率)
- 自建监控运维成本高
- 隐私合规需注意(脱敏用户数据)
How — 怎么用
快速上手
Sentry 接入:
// sentry.ts
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: 'https://xxx@sentry.io/123',
environment: import.meta.env.MODE, // development / production
release: import.meta.env.VITE_COMMIT_SHA,
tracesSampleRate: 0.1, // 性能采样 10%
replaysSessionSampleRate: 0.01, // 录屏采样 1%
replaysOnErrorSampleRate: 1.0, // 错误时 100% 录屏
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration(),
],
// 过滤无意义错误
ignoreErrors: [
'ResizeObserver loop limit exceeded',
'NetworkError',
/Non-Error promise rejection captured/,
],
// 用户的请求头中脱敏
beforeSend(event) {
if (event.request?.headers) {
delete event.request.headers.Authorization;
}
return event;
},
});
React 错误边界:
import * as Sentry from '@sentry/react';
const SentryErrorBoundary = Sentry.ErrorBoundary;
function App() {
return (
<SentryErrorBoundary fallback={<ErrorFallback />} showDialog>
<Router />
</SentryErrorBoundary>
);
}
代码示例
手动上报错误:
// 捕获异常
try {
await riskyOperation();
} catch (error) {
Sentry.captureException(error, {
tags: { module: 'payment' },
extra: { orderId: '12345', amount: 99.9 },
});
}
// 捕获消息
Sentry.captureMessage('支付超时', 'warning');
// 添加用户上下文
Sentry.setUser({ id: user.id, email: user.email });
Sentry.setTag('vip', user.isVip ? 'yes' : 'no');
Sentry.setContext('order', { id: orderId, status: 'pending' });
// 添加面包屑(操作轨迹)
Sentry.addBreadcrumb({
category: 'ui.click',
message: '点击提交按钮',
level: 'info',
});
全局错误捕获:
// JS 运行时错误
window.onerror = (message, source, lineno, colno, error) => {
Sentry.captureException(error);
};
// Promise 未捕获异常
window.addEventListener('unhandledrejection', (event) => {
Sentry.captureException(event.reason);
});
// 资源加载失败
window.addEventListener('error', (event) => {
if (event.target?.src || event.target?.href) {
Sentry.captureMessage(`资源加载失败: ${event.target.src || event.target.href}`, 'error');
}
}, true); // 捕获阶段
// 接口错误(axios 拦截器)
axios.interceptors.response.use(null, (error) => {
if (error.response?.status >= 500) {
Sentry.captureException(error, {
tags: { api: error.config?.url, status: error.response.status },
});
}
return Promise.reject(error);
});
自建性能监控:
// 采集 Web Vitals
function reportWebVitals() {
// LCP
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
report('LCP', lastEntry.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });
// FID / INP
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
report('INP', entry.duration);
});
}).observe({ type: 'event', buffered: true });
// CLS
let clsValue = 0;
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (!entry.hadRecentInput) clsValue += entry.value;
});
report('CLS', clsValue);
}).observe({ type: 'layout-shift', buffered: true });
}
function report(metric: string, value: number) {
const url = '/api/monitor';
if (navigator.sendBeacon) {
navigator.sendBeacon(url, JSON.stringify({ metric, value, url: location.href }));
} else {
fetch(url, { method: 'POST', body: JSON.stringify({ metric, value }), keepalive: true });
}
}
Vue 全局错误处理:
// main.ts
const app = createApp(App);
app.config.errorHandler = (error, instance, info) => {
Sentry.captureException(error as Error, {
tags: { component: instance?.$options?.name, info },
});
};
app.config.warnHandler = (msg, instance, trace) => {
// 开发环境警告也可上报
if (import.meta.env.PROD) {
Sentry.captureMessage(`Vue warn: ${msg}`, 'warning');
}
};
SourceMap 构建 & 上传:
// vite.config.ts
import { sentryVitePlugin } from '@sentry/vite-plugin';
export default defineConfig({
build: {
sourcemap: true, // 生成 SourceMap
},
plugins: [
sentryVitePlugin({
org: 'my-org',
project: 'my-project',
authToken: process.env.SENTRY_AUTH_TOKEN,
release: process.env.VITE_COMMIT_SHA,
// 构建后删除 SourceMap 文件,不部署到 CDN
sourcemaps: { filesToDeleteAfterUpload: ['dist/**/*.map'] },
}),
],
});
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 线上代码无法定位行号 | SourceMap 未上传或未配置 | 构建时上传 SourceMap 到 Sentry,不在 CDN 暴露 |
| 告警风暴 | 同一错误大量触发 | Sentry 内置聚合 + 采样率 + ignoreErrors |
| SDK 影响性能 | 上报请求过多 | 批量上报 + 采样率 + sendBeacon |
| 用户数据泄露 | 上报包含敏感信息 | beforeSend 脱敏处理 |
| 跨域脚本无堆栈 | 跨域 JS 无 SourceMap | <script crossorigin> + CDN 配置 CORS |
最佳实践
- 错误监控 100% 采集,性能/录屏采样降低成本
- SourceMap 构建时上传到监控平台,不部署到 CDN
beforeSend脱敏用户数据,避免隐私泄露- 告警按模块/级别分组,配置通知规则避免噪音
- 上报用
sendBeacon确保页面关闭时不丢失
面试题
Q1: 前端有哪些错误类型?如何捕获?
四种主要类型:JS 运行时错误(
window.onerror/try-catch);Promise 未捕获异常(unhandledrejection事件);资源加载失败(捕获阶段的error事件,检查event.target);接口错误(axios 拦截器捕获 HTTP 状态码异常)。
Q2: SourceMap 的原理是什么?线上如何安全使用?
SourceMap 是构建产物与源码的映射文件,记录压缩/编译后代码位置到源码位置的对应关系。线上安全做法:构建时生成 SourceMap 并上传到监控平台(如 Sentry),上传后从构建产物中删除
.map文件,不部署到 CDN,避免源码泄露。
Q3: 前端错误上报有哪些方式?各有什么优缺点?
三种主流方式:
navigator.sendBeacon(异步发送,页面关闭时不丢失,但有数据大小限制);fetch+keepalive(类似 Beacon,支持更大数据量);Image打点(兼容性最好,仅支持 GET,数据量极有限)。推荐优先用sendBeacon。
Q4: 采样率如何设计?
错误监控 100% 采集(错误量小且价值高);性能监控按比例采样(如 10%~20%),根据流量调整;录屏回放低采样率(1%),错误触发时 100% 录屏。核心原则:高价值数据全量采集,成本高的数据按需采样,通过环境区分(生产采样,开发全量)。
相关链接: