监控与异常上报
约 1403 字大约 5 分钟
监控前端监控异常上报Sentry
2026-04-16
为什么需要监控
前端监控是保障应用稳定性和用户体验的重要手段。线上问题往往难以复现,没有监控的情况下:
- 用户反馈滞后,问题发现时影响范围已扩大
- 异常原因不明,定位问题耗时耗力
- 性能瓶颈未知,优化方向不明确
监控能够帮助团队快速发现问题、定位原因、验证方案,形成开发闭环。
监控体系
性能监控
性能监控关注页面加载速度和交互响应能力。
Performance API
// 获取关键性能指标
const perf = performance.getEntriesByType('navigation')[0];
console.log({
dns: perf.domainLookupEnd - perf.domainLookupStart, // DNS 解析
tcp: perf.connectEnd - perf.connectStart, // TCP 连接
ttfb: perf.responseStart - perf.requestStart, // 首字节时间
download: perf.responseEnd - perf.responseStart, // 内容下载
domParse: perf.domInteractive - perf.responseEnd, // DOM 解析
domReady: perf.domContentLoadedEventEnd - perf.fetchStart, // DOM Ready
load: perf.loadEventEnd - perf.fetchStart, // 页面加载
});Core Web Vitals
// LCP (最大内容绘制)
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });
// FID (首次输入延迟)
new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
console.log('FID:', entry.processingStart - entry.startTime);
});
}).observe({ type: 'first-input', buffered: true });
// CLS (累积布局偏移)
new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (!entry.hadRecentInput) {
console.log('CLS:', entry.value);
}
});
}).observe({ type: 'layout-shift', buffered: true });Lighthouse
Lighthouse 是 Google 提供的自动化性能审计工具,可通过 Chrome DevTools 或 CLI 使用:
# 安装并运行
npm install -g lighthouse
lighthouse https://example.com --view错误监控
错误监控捕获 JavaScript 运行时的各类异常。
JS 异常
window.onerror = function(message, source, lineno, colno, error) {
console.log('全局错误:', {
message,
source,
lineno,
colno,
stack: error?.stack,
});
// 返回 true 阻止默认处理
return true;
};资源加载错误
window.addEventListener('error', (event) => {
// 区分 JS 错误和资源错误
if (event.target !== window) {
console.log('资源加载失败:', {
src: event.target.src || event.target.href,
tagName: event.target.tagName,
});
}
}, true); // 捕获阶段Promise 拒绝
window.addEventListener('unhandledrejection', (event) => {
console.log('未处理的 Promise 拒绝:', {
reason: event.reason,
stack: event.reason?.stack,
});
});业务监控
业务监控关注用户行为和 API 调用情况。
用户行为追踪
// 点击事件追踪
document.addEventListener('click', (event) => {
const target = event.target.closest('[data-track]');
if (target) {
console.log('用户点击:', {
action: target.dataset.track,
text: target.textContent.trim(),
position: {
x: event.clientX,
y: event.clientY,
},
});
}
});
// PV/UV 追踪
function trackPageView() {
console.log('页面浏览:', {
url: location.href,
referrer: document.referrer,
timestamp: Date.now(),
});
}API 调用监控
const originalFetch = window.fetch;
window.fetch = async function(...args) {
const [url, options] = args;
const startTime = Date.now();
try {
const response = await originalFetch(...args);
const duration = Date.now() - startTime;
console.log('API 调用:', {
url,
method: options?.method || 'GET',
status: response.status,
duration,
ok: response.ok,
});
return response;
} catch (error) {
const duration = Date.now() - startTime;
console.log('API 错误:', {
url,
method: options?.method || 'GET',
duration,
error: error.message,
});
throw error;
}
};异常上报机制
跨域错误处理
跨域脚本错误无法直接捕获,需要通过以下方式处理:
- CORS 配置:在资源服务器上添加
Access-Control-Allow-Origin响应头 - script 标签 crossorigin 属性:加载跨域脚本时添加
crossorigin="anonymous" - 使用 try-catch 包装:在 script 内部用 try-catch 捕获并通过 postMessage 传递
// 跨域脚本的错误捕获
window.addEventListener('error', (event) => {
if (event.message === 'Script error.' && !event.colno) {
console.log('跨域错误,需要检查 CORS 配置');
}
}, true);上报去重
频繁触发同一错误会导致大量重复上报,应实现去重机制:
const errorSet = new Set();
const errorTimeout = new Map();
function reportError(error) {
const key = `${error.message}@${error.lineno}:${error.colno}`;
if (errorSet.has(key)) {
// 已上报过,减少频率
const lastTime = errorTimeout.get(key);
if (Date.now() - lastTime < 60000) return; // 1分钟内不重复上报
}
errorSet.add(key);
errorTimeout.set(key, Date.now());
sendToServer(error);
}监控 SDK 设计要点
数据收集
class Monitor {
constructor(options = {}) {
this.appId = options.appId || 'app';
this.dsn = options.dsn;
this.sampleRate = options.sampleRate || 1; // 采样率 0-1
this.queue = [];
}
// 收集错误信息
collectError(event) {
if (Math.random() > this.sampleRate) return;
const errorInfo = {
appId: this.appId,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: location.href,
...event,
};
this.queue.push(errorInfo);
this.flush();
}
// 收集性能数据
collectPerformance() {
const perf = performance.getEntriesByType('navigation')[0];
const paint = performance.getEntriesByType('paint');
return {
ttfb: perf.responseStart - perf.requestStart,
fcp: paint.find(e => e.name === 'first-contentful-paint')?.startTime,
domReady: perf.domContentLoadedEventEnd - perf.fetchStart,
load: perf.loadEventEnd - perf.fetchStart,
};
}
}数据上报
// beacon 方式(推荐)
function reportByBeacon(data) {
navigator.sendBeacon('/api/monitor', JSON.stringify(data));
}
// img 方式(兼容性好)
function reportByImg(data) {
const img = new Image();
img.src = `/api/monitor?data=${encodeURIComponent(JSON.stringify(data))}`;
}
// fetch 方式
async function reportByFetch(data) {
await fetch('/api/monitor', {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
keepalive: true, // 页面关闭时仍发送
});
}采样率
class Monitor {
constructor(options = {}) {
this.sampleRate = options.sampleRate || 1;
}
shouldSample() {
return Math.random() <= this.sampleRate;
}
collectError(event) {
if (!this.shouldSample()) return;
// 继续上报流程
}
}
// 使用
const monitor = new Monitor({ sampleRate: 0.1 }); // 10% 采样常用监控服务
Sentry(推荐)
Sentry 是功能完善、开源的前端监控平台:
# 安装
npm install @sentry/browserimport * as Sentry from '@sentry/browser';
Sentry.init({
dsn: 'https://xxx@sentry.io/xxx',
environment: process.env.NODE_ENV,
release: '1.0.0',
sampleRate: 1,
tracesSampleRate: 0.1, // 性能采样
beforeSend(event) {
// 过滤敏感信息
delete event.user;
return event;
},
});
// 手动上报
Sentry.captureException(new Error('something went wrong'));
Sentry.captureMessage('warning message');FunDebug
FunDebug 是国内的前端监控服务:
import fundebug from 'fundebug-javascript';
fundebug.init({
apikey: 'your-api-key',
silentResource: true, // 不监控资源加载错误
});自建方案
自建监控平台需要考虑:
| 组件 | 说明 |
|---|---|
| 数据采集 | SDK 采集错误、性能、行为数据 |
| 数据传输 | 通过 beacon 或 HTTP 上报到服务端 |
| 数据存储 | 选择时序数据库如 InfluxDB、ClickHouse |
| 数据展示 | 可用 Grafana 展示 |
| 告警 | 配置告警规则,触发时通知 |
告警与告警策略
告警指标
| 指标 | 说明 | 建议阈值 |
|---|---|---|
| 错误率 | 错误数 / PV | > 1% |
| JS 错误数 | 页面 JavaScript 错误 | > 10/min |
| API 错误率 | API 请求失败比例 | > 5% |
| 白屏时间 | FCP | > 3s |
| 页面加载 | L | > 5s |
告警策略
// 示例:基于错误率的告警
function checkErrorRate() {
const errorCount = getErrorCountInWindow(60000); // 1分钟内错误数
const pvCount = getPVInWindow(60000); // 1分钟内 PV
if (pvCount === 0) return;
const errorRate = errorCount / pvCount;
if (errorRate > 0.01) {
sendAlert({
type: 'error_rate',
rate: errorRate,
errorCount,
pvCount,
});
}
}告警通知渠道
- 邮件通知:适合不紧急的告警
- 钉钉/企业微信群:适合团队协作
- SMS/电话:适合严重故障需立即处理
