文章中的代码在这里:github.com/wangkaiwd/m…
结合代码可以更好的理解相关知识
js
错误
监听 window
的 error
事件来收集 js
报错
window.addEventListener('error', (event) => {
console.log('args window error', event)
})
打印的结果如下:
JS
也支持通过 window.onerror
来监听事件,但是这样之后的监听会将之前的监听覆盖,而且参数和 addEventListener
不一样:
window.onerror = (message, source, lineno, colno, error) => {
// do something
};
资源加载错误
error
事件不支持冒泡,可以在捕获阶段收集资源加载报错
// note: third argument is true
document.addEventListener('error', (ev) => {
console.log('args document error', ev)
const resourceUrl = (ev.target as any).src ?? (ev.target as any).href
}, true)
这里监听 document
的 error
事件,并且设置第三个参数为 true
, 方便和 window
的 error
事件进行区分,来识别代码错误和资源加载错误
常见的资源有:
link
script
img
audio
video
link
可以通过 href
来获取资源路径,其它资源可以通过 src
来获取资源路径
promise
错误
promise
错误要通过监听 unhandledrejection
收集
window.addEventListener('unhandledrejection', (ev) => {
console.log('unhandledrejection', ev)
})
unhandledrejection
事件没有办法获取到错误栈,在上报时只能上报它的 reason
字段
http
错误
js
通常是通过 XMLHttpRequest
来发起请求,可以重写相关的方法来获取请求和响应的数据进行上报
- 重写
open
方法用来获取请求的url, method
- 重写
send
方法用来获取请求参数,并且监听响应事件,当接口响应异常时进行错误上报
const xhrErrorHandler = () => {
// 重写 open 方法用来获取请求的 url, method
replaceAop(XMLHttpRequest.prototype, 'open', (origin: Function) => {
return function (this: MonitorXMLHttpRequest, ...args: any[]) {
const [method, url] = args
// 为this实例添加自定义属性httpReportProps存储上报参数
this.httpReportProps = {
method,
url,
}
return origin.apply(this, args)
}
})
// 重写 send 方法用来获取请求参数,并且监听响应事件,当接口响应异常时进行错误上报
replaceAop(XMLHttpRequest.prototype, 'send', (origin: Function) => {
return function (this: MonitorXMLHttpRequest, body: any) {
const result = origin.call(this, body)
this.addEventListener('loadend', (ev: ProgressEvent<XMLHttpRequestEventTarget>) => {
const { status, responseText, httpReportProps } = ev.target as MonitorXMLHttpRequest
// 这里可以自定义什么是异常情况,然后将异常请求信息进行上报
if (status === 0 || !(status >= 200 && status < 300)) {
if (httpReportProps) {
httpReportProps.status = status
httpReportProps.body = body
httpReportProps.response = responseText
report({ ...httpReportProps, errorType: 'xhr', eventType: 'error' })
// 上报后删除自定义属性
delete (ev.target as MonitorXMLHttpRequest).httpReportProps
}
}
})
return result
}
})
}
React异常
React
错误捕获是通过 class
组件来实现的,大部分介绍在老文档中,可以新老文档结合一起查看
下面重点介绍一些和 SDK
相关的知识点
在 React
中通过 class
组件来进行错误上报,代码如下
class ErrorBoundary extends React.Component<Props, State> {
constructor (props: Props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError () {
return { hasError: true }
}
componentDidCatch (error: Error, info: ErrorInfo) {
report({
eventType: 'error',
errorType: 'js',
message: error.message,
stack: error.stack,
componentStack: info.componentStack,
})
}
render () {
console.log('render')
if (this.state.hasError) {
return this.props.fallback
}
return this.props.children
}
}
需要注意的是下面俩种常见情况, React
不会进行错误捕获:
- 事件处理器
- 异步代码
更详细的介绍在这里:legacy.reactjs.org/docs/error-…
如下是相应的示例:
- 用户点击一个按钮时引发错误
function App () {
const [count, setCount] = useState(1)
const onClick = () => {
throw Error('error')
setCount(count + 1)
}
return <div onClick={onClick}>{count}</div>
}
export default App
- 定时器中发生错误
function App () {
const [count, setCount] = useState(1)
useEffect(() => {
setTimeout(() => {
throw Error('error')
})
}, [])
return <div>{count}</div>
}
上述的2种情况,都会被 window
的 error
事件捕获
业务异常
自定义异常要业务方手动调用 API
来上报:
reportCustomError({
params: {
id: '1'
},
message: '业务异常'
})
错误行为记录
为了能更好的排查出错误的原因,我们可以记录用户的操作行为,每次将用户的行为记录到一个队列中,在报错时查看用户在报错前的一系列操作
出于对性能方面的考虑,需要限制队列的最大长度。当队列超过最大长度时,要将最早入队(队头)的元素出队,将用户最新的操作行为入队
enqueue (breadcrumb: Omit<EventInfo, 'triggerTime'>) {
this.queue.push({
...breadcrumb as any,
triggerTime: Date.now(),
})
// 达到最大数量时出队
if (this.queue.length > this.maxBreadcrumbsCount) {
this.dequeue()
}
}
需要注意在收集 dom
相关事件时,要在 捕获阶段监听事件,保证该事件发生在错误收集之前。假设如下场景:
- 用户点击按钮
- 按钮绑定的点击事件引起报错
我们预期的结果应该是先触发点击事件,然后再报错,上报错误日志。但是监听的事件默认是在冒泡阶段触发,这样会导致先报错,然后再收集 click
事件。
要解决上边的顺序问题,需要在 捕获阶段进行事件代理, addEventListener
的第三个参数设置为 true
即可:
window.addEventListener('click', (ev: Event) => {
reportClick(ev)
}, true)
结语
本文详细介绍了前端各种错误的捕获方案,自己在实践过程中查找资料、阅读源码,也让自己对相关知识又了更深入的理解。如果你也在做相关事情,希望能对你有所帮助。
虽然我在整个过程中也不停的在思考各种情况和解决方案,但难免疏漏。如果有遗漏或出错的地方,欢迎指出。