异常监控
前端异常监控的目标是:及时发现线上问题 → 定位原因 → 评估影响面。
一、监控哪些错误
| 类别 | 例子 | 捕获方式 |
|---|---|---|
| JS 运行错误 | TypeError、ReferenceError | window.onerror、unhandledrejection |
| 资源加载错误 | 图片/脚本 404 | addEventListener('error') |
| Promise 异常 | Promise.reject 未 catch | unhandledrejection |
| 网络请求错误 | 接口 4xx/5xx、超时 | axios 拦截器 |
| 白屏 | 首屏渲染失败 | DOM 检测 + 截图 |
| 性能异常 | 长任务、卡顿 | PerformanceObserver |
| 自定义异常 | 业务关键路径出错 | try/catch + 上报 |
二、错误捕获
1. 全局 JS 错误
// 全局同步错误
window.addEventListener("error", (event) => {
report({
type: "js_error",
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
});
});
// Promise 异常
window.addEventListener("unhandledrejection", (event) => {
report({
type: "promise_error",
reason: String(event.reason),
stack: event.reason?.stack,
});
});
2. React Error Boundary
React 组件树内的错误不会冒泡到 window.onerror,必须用 ErrorBoundary:
import React from "react";
interface Props { fallback?: React.ReactNode; children: React.ReactNode; }
interface State { hasError: boolean; }
export class ErrorBoundary extends React.Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
report({
type: "react_error",
message: error.message,
stack: error.stack,
componentStack: info.componentStack,
});
}
render() {
if (this.state.hasError) return this.props.fallback ?? <div>页面出错了</div>;
return this.props.children;
}
}
// 使用
<ErrorBoundary fallback={<ErrorPage />}>
<App />
</ErrorBoundary>
3. Vue errorHandler
app.config.errorHandler = (err, instance, info) => {
report({
type: "vue_error",
message: String(err),
stack: err.stack,
info, // "render" | "lifecycle hook" | ...
});
};
4. 资源加载错误
window.addEventListener("error", (event) => {
const target = event.target as HTMLElement;
if (target instanceof HTMLImageElement || target instanceof HTMLScriptElement) {
report({
type: "resource_error",
tag: target.tagName,
src: (target as any).src,
});
}
}, true); // 注意:捕获阶段
5. axios 拦截
service.interceptors.response.use(
(res) => res,
(err) => {
report({
type: "http_error",
url: err.config?.url,
method: err.config?.method,
status: err.response?.status,
message: err.message,
});
return Promise.reject(err);
}
);
三、上报策略
1. 数据结构
interface ErrorReport {
type: string; // js_error / promise_error / http_error ...
message: string;
stack?: string;
url: string; // 当前页面
userId?: string;
userAgent: string;
appVersion: string; // 发版标识,用于按版本聚合
timestamp: number;
// 辅助字段
breadcrumbs?: Breadcrumb[]; // 用户行为栈
extra?: Record<string, any>;
}
2. 上报方式
| 方式 | 优点 | 缺点 |
|---|---|---|
navigator.sendBeacon | 异步、不阻塞、不影响页面卸载 | 部分老浏览器不支持 |
new Image().src | 兼容性好 | URL 长度受限、丢失率高 |
fetch keepalive | 现代方案 | 兼容性略差 |
function report(data: ErrorReport) {
const body = JSON.stringify(data);
// 优先用 sendBeacon
if (navigator.sendBeacon) {
navigator.sendBeacon("/api/monitor", body);
} else {
// 兜底
new Image().src = `/api/monitor?data=${encodeURIComponent(body)}`;
}
}
3. 采样与去重
- 采样:错误量大时按比例上报(如 10%)
- 去重:相同
message + filename + lineno在一定时间窗口内只上报一次 - 合并:相同错误的多次发生聚合为一次(count 字段累加)
- 离线缓存:上报失败时存到
localStorage,下次启动时重发
四、Sentry 集成
Sentry 是业界最成熟的方案,开箱即用。
pnpm add @sentry/react @sentry/tracing
import * as Sentry from "@sentry/react";
import { BrowserTracing } from "@sentry/tracing";
Sentry.init({
dsn: "https://xxx@sentry.io/xxx",
integrations: [new BrowserTracing()],
tracesSampleRate: 0.2, // 采样 20%
environment: process.env.NODE_ENV,
release: process.env.APP_VERSION, // 与发版关联
beforeSend(event) {
// 过滤掉不关心的错误
if (event.exception?.values?.[0]?.type === "ChunkLoadError") return null;
return event;
},
});
五、白屏监控
function checkWhiteScreen() {
// 在根节点插入探针
const probe = document.createElement("div");
probe.style.cssText = "position:fixed;top:0;left:0;width:100px;height:100px;z-index:9999;";
document.body.appendChild(probe);
// 等首屏渲染完
setTimeout(() => {
const centerX = document.documentElement.clientWidth / 2;
const centerY = document.documentElement.clientHeight / 2;
const el = document.elementFromPoint(centerX, centerY);
if (el?.tagName === "BODY" || !el) {
report({ type: "white_screen", message: "疑似白屏" });
}
}, 3000);
}
六、面试高频
- Q:error 和 unhandledrejection 的区别?
A:
error捕获同步运行时错误;unhandledrejection专门处理未捕获的 Promise reject。 - Q:React 为什么需要 ErrorBoundary? A:React 组件渲染错误不会冒泡到 window,全局监听拿不到;ErrorBoundary 可以在组件树边界 catch 住并降级 UI。
- Q:sendBeacon 为什么适合上报?
A:异步、不阻塞页面、不受
beforeunload影响,最适合在页面卸载时上报最后一批数据。