凌晨三点,我正准备睡觉,突然收到了几十条线上报错的告警。打开监控面板一看,生产环境的某个页面报错率突然飙升到了 15%。作为技术负责人,这种时候必须立即处理。但当我查看错误日志时,却发现记录的信息少得可怜,根本无法定位具体问题。
这不是第一次遇到类似的情况了。随着公司业务的快速发展,前端项目越来越复杂,每天的 PV 已经突破百万,仅凭借控制台日志和用户反馈已经无法及时发现和解决问题。这次事件成为了导火索,我决定带领团队搭建一个完整的前端错误监控系统。
需求分析
经过和团队的深入讨论,我们梳理出了几个核心痛点。去年双十一期间,我们就吃过类似的亏。记得那天早上:
"页面打不开了!"客服部门疯狂地反馈用户投诉。 "控制台没有任何报错啊!"前端开发手足无措。 "后端接口都是正常的!"后端同学也很委屈。
最后花了整整两个小时才发现是一个第三方 SDK 的异常导致页面白屏。如果当时有完善的错误监控系统,也许十分钟就能解决问题。
监控系统设计
就像医院的体检系统,我们的错误监控也需要做到全面且精准。首先要明确监控的范围:
- 前端异常:
- JavaScript 运行时错误
- Promise 异常
- 资源加载失败
- API 请求异常
- 白屏检测
- 性能指标:
- 页面加载时间
- 首屏渲染时间
- 资源加载耗时
- 接口响应时间
- 用户行为:
- 页面访问路径
- 点击事件追踪
- 报错时的用户操作
架构实现
经过调研,我们设计了一个轻量级但功能完整的监控系统。就像搭建一个医疗监控系统,我们需要采集数据(体征指标),传输数据(医疗记录),分析数据(诊断报告),最后及时预警(病情告警)。
首先是错误采集层。我们通过重写浏览器的原生方法来捕获各类错误:
class ErrorCatcher {
constructor() {
// 捕获全局错误
window.addEventListener('error', this.handleError.bind(this), true)
// 捕获 Promise 异常
window.addEventListener('unhandledrejection', this.handlePromiseError.bind(this), true)
// 重写 console.error
this.wrapConsoleError()
}
handleError(event) {
const errorInfo = {
type: 'javascript',
message: event.message,
filename: event.filename,
position: `${event.lineno}:${event.colno}`,
stack: event.error?.stack,
// 收集用户信息和环境信息
userAgent: navigator.userAgent,
timestamp: new Date().getTime(),
url: window.location.href,
// 获取最近的用户行为
userActions: this.getUserActions()
}
this.report(errorInfo)
}
getUserActions() {
// 返回最近的用户操作记录
return this.actionTracker.getRecentActions()
}
}
然后是数据上报服务。考虑到错误发生时网络可能不稳定,我们实现了一个可靠的上报机制:
class Reporter {
constructor() {
this.queue = []
this.retryTimes = 3
this.batchSize = 10
// 使用 IndexedDB 做离线存储
this.initStorage()
}
async report(data) {
try {
// 优先使用 sendBeacon,它更可靠
if (navigator.sendBeacon) {
const success = navigator.sendBeacon('/api/errors', JSON.stringify(data))
if (success) return
}
// 降级使用 fetch
await this.sendWithRetry(data)
} catch (error) {
// 存储到本地
await this.saveToIndexedDB(data)
}
}
async sendWithRetry(data, times = 0) {
try {
const response = await fetch('/api/errors', {
method: 'POST',
body: JSON.stringify(data)
})
if (!response.ok) throw new Error('上报失败')
} catch (error) {
if (times < this.retryTimes) {
// 延迟重试,避免网络风暴
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, times)))
return this.sendWithRetry(data, times + 1)
}
throw error
}
}
}
为了更好地分析问题,我们还加入了用户行为追踪:
class ActionTracker {
constructor() {
this.actions = []
this.maxActions = 50
this.initTrackers()
}
initTrackers() {
// 记录点击事件
document.addEventListener(
'click',
event => {
const target = event.target
this.addAction({
type: 'click',
element: this.getElementPath(target),
timestamp: new Date().getTime()
})
},
true
)
// 记录路由变化
if (window.history) {
const oldPushState = window.history.pushState
window.history.pushState = (...args) => {
oldPushState.apply(window.history, args)
this.addAction({
type: 'navigation',
to: window.location.pathname,
timestamp: new Date().getTime()
})
}
}
}
getElementPath(element) {
// 生成元素的唯一路径
const path = []
while (element && element.nodeType === Node.ELEMENT_NODE) {
let selector = element.nodeName.toLowerCase()
if (element.id) {
selector += `#${element.id}`
path.unshift(selector)
break
}
path.unshift(selector)
element = element.parentNode
}
return path.join(' > ')
}
}
数据分析和告警
收集到数据后,最关键的是如何分析和利用这些数据。我们建立了一个实时分析系统,它能够:
- 自动对错误进行分类和聚合
- 计算各类错误的发生频率和影响范围
- 识别异常的错误波峰
- 追踪错误的来源和传播路径
当发现异常时,系统会通过多个渠道及时通知相关人员:
- 钉钉群机器人发送告警消息
- 邮件通知详细的错误报告
- 严重问题时自动发起语音通话
实践效果
系统上线后,我们很快就尝到了甜头。有一次,系统在凌晨检测到了一个第三方 SDK 的异常,在影响到大量用户之前,我们就及时进行了降级处理。
最让我印象深刻的是双十一当天。去年我们手忙脚乱了一整天,而今年,通过监控系统我们精准定位到了几个性能瓶颈,提前进行了优化,整个活动过程异常平稳。
经验总结
建设监控系 统的过程中,我们学到了很多:
监控要全面但要有重点,就像体检要查全身,但对重要器官要特别关注。错误信息要详细但不能冗余,就像病历要记录关键症状,但不需要记录病人吃早餐吃了什么。最重要的是要建立快速响应机制,就像急诊科要确保随时有医生值班。
写在最后
前端错误监控就像是应用的健康检查系统,它不能保证不出问题,但能帮助我们更快地发现和解决问题。正如那句老话说的:"预防胜于治疗",一个好的监控系统就是最好的预防。
有什么问题欢迎在评论区讨论,让我们一起探讨前端监控的最佳实践!
如果觉得有帮助,别忘了点赞关注,我会继续分享更多实战经验~