Skip to main content

异常监控

前端异常监控的目标是:及时发现线上问题 → 定位原因 → 评估影响面

一、监控哪些错误

类别例子捕获方式
JS 运行错误TypeErrorReferenceErrorwindow.onerrorunhandledrejection
资源加载错误图片/脚本 404addEventListener('error')
Promise 异常Promise.reject 未 catchunhandledrejection
网络请求错误接口 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 影响,最适合在页面卸载时上报最后一批数据。