前端监控 SDK(二):深入错误监控

377 阅读4分钟

文章中的代码在这里:github.com/wangkaiwd/m…

结合代码可以更好的理解相关知识

js 错误

监听 windowerror 事件来收集 js 报错

window.addEventListener('error', (event) => {
  console.log('args window error', event)
})

打印的结果如下:

image.png

JS 也支持通过 window.onerror 来监听事件,但是这样之后的监听会将之前的监听覆盖,而且参数和 addEventListener 不一样:

window.onerror = (message, source, lineno, colno, error) => {
	// do something
};

资源加载错误

image.png

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)

这里监听 documenterror 事件,并且设置第三个参数为 true , 方便和 windowerror 事件进行区分,来识别代码错误和资源加载错误

常见的资源有:

  • 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-…

如下是相应的示例:

  1. 用户点击一个按钮时引发错误
function App () {
  const [count, setCount] = useState(1)
  const onClick = () => {
    throw Error('error')
    setCount(count + 1)
  }
  return <div onClick={onClick}>{count}</div>
}

export default App
  1. 定时器中发生错误
function App () {
  const [count, setCount] = useState(1)
  useEffect(() => {
    setTimeout(() => {
      throw Error('error')
    })
  }, [])
  return <div>{count}</div>
}

上述的2种情况,都会被 windowerror 事件捕获

业务异常

自定义异常要业务方手动调用 API 来上报:

reportCustomError({
    params: {
            id: '1'
    },
    message: '业务异常'
})

错误行为记录

为了能更好的排查出错误的原因,我们可以记录用户的操作行为,每次将用户的行为记录到一个队列中,在报错时查看用户在报错前的一系列操作 image.png

出于对性能方面的考虑,需要限制队列的最大长度。当队列超过最大长度时,要将最早入队(队头)的元素出队,将用户最新的操作行为入队

enqueue (breadcrumb: Omit<EventInfo, 'triggerTime'>) {
  this.queue.push({
    ...breadcrumb as any,
    triggerTime: Date.now(),
  })
  // 达到最大数量时出队
  if (this.queue.length > this.maxBreadcrumbsCount) {
    this.dequeue()
  }
}

需要注意在收集 dom 相关事件时,要在 捕获阶段监听事件,保证该事件发生在错误收集之前。假设如下场景:

  1. 用户点击按钮
  2. 按钮绑定的点击事件引起报错

我们预期的结果应该是先触发点击事件,然后再报错,上报错误日志。但是监听的事件默认是在冒泡阶段触发,这样会导致先报错,然后再收集 click 事件。

要解决上边的顺序问题,需要在 捕获阶段进行事件代理addEventListener 的第三个参数设置为 true 即可:

window.addEventListener('click', (ev: Event) => {
  reportClick(ev)
}, true)

结语

本文详细介绍了前端各种错误的捕获方案,自己在实践过程中查找资料、阅读源码,也让自己对相关知识又了更深入的理解。如果你也在做相关事情,希望能对你有所帮助。

虽然我在整个过程中也不停的在思考各种情况和解决方案,但难免疏漏。如果有遗漏或出错的地方,欢迎指出。

参考