前端监控包含哪些方面?如何实现错误监控?
前端监控三大领域
- 错误监控:JS 错误、资源加载失败、API 错误
- 性能监控:加载时间、FCP、LCP、白屏时间
- 行为监控:PV/UV、点击流、用户路径
错误监控
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,
timestamp: Date.now()
})
}, true)2. Promise 未捕获异常
window.addEventListener('unhandledrejection', (event) => {
report({
type: 'promise_error',
reason: event.reason?.message || event.reason,
stack: event.reason?.stack,
timestamp: Date.now()
})
})3. 资源加载错误
window.addEventListener('error', (event) => {
const target = event.target
if (target && (target.tagName === 'IMG' || target.tagName === 'SCRIPT' || target.tagName === 'LINK')) {
report({
type: 'resource_error',
tagName: target.tagName,
src: target.src || target.href,
timestamp: Date.now()
})
}
}, true) // 必须用捕获阶段4. 接口错误
// 拦截 fetch
const originalFetch = window.fetch
window.fetch = async (...args) => {
const [url, options] = args
const startTime = Date.now()
try {
const response = await originalFetch(...args)
if (!response.ok) {
report({
type: 'api_error',
url: typeof url === 'string' ? url : url.url,
status: response.status,
duration: Date.now() - startTime
})
}
return response
} catch (error) {
report({
type: 'api_error',
url: typeof url === 'string' ? url : url.url,
error: error.message,
duration: Date.now() - startTime
})
throw error
}
}
// 拦截 XMLHttpRequest
const originalOpen = XMLHttpRequest.prototype.open
const originalSend = XMLHttpRequest.prototype.send
XMLHttpRequest.prototype.open = function(method, url, ...args) {
this._url = url
this._method = method
this._startTime = Date.now()
originalOpen.call(this, method, url, ...args)
}
XMLHttpRequest.prototype.send = function(...args) {
this.addEventListener('loadend', () => {
if (this.status >= 400) {
report({
type: 'api_error',
url: this._url,
method: this._method,
status: this.status,
duration: Date.now() - this._startTime
})
}
})
originalSend.call(this, ...args)
}性能监控
// Web Vitals
import { onCLS, onFID, onLCP, onFCP, onTTFB } from 'web-vitals'
onCLS(console.log) // 累积布局偏移
onFID(console.log) // 首次输入延迟
onLCP(console.log) // 最大内容绘制
onFCP(console.log) // 首次内容绘制
onTTFB(console.log) // 首字节时间
// Performance API
const timing = performance.getEntriesByType('navigation')[0]
console.log({
dns: timing.domainLookupEnd - timing.domainLookupStart,
tcp: timing.connectEnd - timing.connectStart,
ttfb: timing.responseStart - timing.requestStart,
domReady: timing.domComplete - timing.domLoading,
load: timing.loadEventEnd - timing.fetchStart
})上报方案
function report(data) {
data.userAgent = navigator.userAgent
data.url = location.href
data.userId = getUserId()
// 方案 1:sendBeacon(推荐,页面卸载时也能发送)
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' })
navigator.sendBeacon('/api/monitor', blob)
return
}
// 方案 2:fetch keepalive
fetch('/api/monitor', {
method: 'POST',
body: JSON.stringify(data),
keepalive: true
})
// 方案 3:Image(简单但只支持 GET)
new Image().src = `/api/monitor?data=${encodeURIComponent(JSON.stringify(data))}`
}Source Map 还原
生产环境代码经过压缩混淆,错误堆栈不可读:
// 服务器端使用 source-map 库还原
const sourceMap = require('source-map')
const fs = require('fs')
async function还原(line, column) {
const rawSourceMap = JSON.parse(fs.readFileSync('app.js.map', 'utf8'))
const consumer = await new sourceMap.SourceMapConsumer(rawSourceMap)
const original = consumer.originalPositionFor({ line, column })
console.log(original.source, original.line, original.name)
}常用监控工具
| 工具 | 特点 |
|---|---|
| Sentry | 错误监控、Source Map |
| LogRocket | 录屏回放 |
| Google Analytics | 行为分析 |
| 阿里云 ARMS | 全链路监控 |
| Datadog | 全栈监控 |
监控指标
- 错误率:错误数 / PV
- API 成功率:成功请求 / 总请求
- 平均响应时间
- 白屏时间
- 核心 Web Vitals