总结:这三年沉淀的前端错误监控系统,一篇文章讲透给你

优采云 发布时间: 2022-11-20 20:25

  总结:这三年沉淀的前端错误监控系统,一篇文章讲透给你

  // 控制台运行<br />fetch('/remote/notdefined', {})<br />复制代码<br />

  采集

错误

  所有的原因都来自错误,那么我们如何捕获错误。

  试着抓

  可以捕获常规运行时错误,语法错误和异步错误不能

  // 常规运行时错误,可以捕获 ✅<br />try {<br />  console.log(notdefined);<br />} catch(e) {<br />  console.log('捕获到异常:', e);<br />}<br /><br />// 语法错误,不能捕获 ❌<br />try {<br />  const notdefined,<br />} catch(e) {<br />  console.log('捕获到异常:', e);<br />}<br /><br />// 异步错误,不能捕获 ❌<br />try {<br />  setTimeout(() => {<br />    console.log(notdefined);<br />  }, 0)<br />} catch(e) {<br />  console.log('捕获到异常:',e);<br />}<br />复制代码<br />

  try/catch的优点是处理细心,但缺点也很明显。

  window.onerror 错误

  纯js错误集合,window.onerror,当发生JS运行时错误时,window会触发ErrorEvent接口的错误事件。

  /**<br />* @param {String}  message    错误信息<br />* @param {String}  source    出错文件<br />* @param {Number}  lineno    行号<br />* @param {Number}  colno    列号<br />* @param {Object}  error  Error对象<br />*/<br /><br />window.onerror = function(message, source, lineno, colno, error) {<br />   console.log('捕获到异常:', {message, source, lineno, colno, error});<br />}<br />复制代码<br />

  先验证接下来的几个错误能不能被捕获。

  // 常规运行时错误,可以捕获 ✅<br /><br />window.onerror = function(message, source, lineno, colno, error) {<br />  console.log('捕获到异常:',{message, source, lineno, colno, error});<br />}<br />console.log(notdefined);<br /><br />// 语法错误,不能捕获 ❌<br />window.onerror = function(message, source, lineno, colno, error) {<br />  console.log('捕获到异常:',{message, source, lineno, colno, error});<br />}<br />const notdefined,<br />      <br />// 异步错误,可以捕获 ✅<br />window.onerror = function(message, source, lineno, colno, error) {<br />  console.log('捕获到异常:',{message, source, lineno, colno, error});<br />}<br />setTimeout(() => {<br />  console.log(notdefined);<br />}, 0)<br /><br />// 资源错误,不能捕获 ❌<br /><br />  window.onerror = function(message, source, lineno, colno, error) {<br />  console.log('捕获到异常:',{message, source, lineno, colno, error});<br />  return true;<br />}<br /><br />/yun.tuia.cn/image/kkk.png"><br />复制代码<br />

  window.onerror 抓不到资源错误怎么办?

  窗户。添加事件*敏*感*词*器

  当资源(如图片或脚本)加载失败时,加载该资源的元素会触发Event接口的错误事件。这些错误事件不会冒泡到窗口,但可以被捕获。而window.onerror无法*敏*感*词*和捕捉。

  // 图片、script、css加载错误,都能被捕获 ✅<br /><br />  window.addEventListener('error', (error) => {<br />     console.log('捕获到异常:', error);<br />  }, true)<br /><br /><br /><br /><br />  <br />// new Image错误,不能捕获 ❌<br /><br />  window.addEventListener('error', (error) => {<br />    console.log('捕获到异常:', error);<br />  }, true)<br /><br /><br />  new Image().src = 'https://yun.tuia.cn/image/lll.png'<br /><br /><br />// fetch错误,不能捕获 ❌<br /><br />  window.addEventListener('error', (error) => {<br />    console.log('捕获到异常:', error);<br />  }, true)<br /><br /><br />  fetch('https://tuia.cn/test')<br /><br />复制代码<br />

  new Image的使用比较少,自己处理错误即可。

  但是一般的获取呢?Fetch 返回 Promise,但无法捕获 Promise 错误。我们应该做什么?

  承诺错误

  常见的 Promise 错误

  try/catch 无法捕获 Promise 中的错误

  // try/catch 不能处理 JSON.parse 的错误,因为它在 Promise 中<br />try {<br />  new Promise((resolve,reject) => { <br />    JSON.parse('')<br />    resolve();<br />  })<br />} catch(err) {<br />  console.error('in try catch', err)<br />}<br /><br />// 需要使用catch方法<br />new Promise((resolve,reject) => { <br />  JSON.parse('')<br />  resolve();<br />}).catch(err => {<br />  console.log('in catch fn', err)<br />})<br />复制代码<br />

  异步错误

  try/catch 无法捕获异步包装的错误

  const getJSON = async () => {<br />  throw new Error('inner error')<br />}<br /><br />// 通过try/catch处理<br />const makeRequest = async () => {<br />    try {<br />        // 捕获不到<br />        JSON.parse(getJSON());<br />    } catch (err) {<br />        console.log('outer', err);<br />    }<br />};<br /><br />try {<br />    // try/catch不到<br />    makeRequest()<br />} catch(err) {<br />    console.error('in try catch', err)<br />}<br /><br />try {<br />    // 需要await,才能捕获到<br />    await makeRequest()<br />} catch(err) {<br />    console.error('in try catch', err)<br />}<br />复制代码<br />

  导入块错误

  import实际上返回的是一个promise,所以使用下面两个方法来捕获错误

  // Promise catch方法<br />import(/* webpackChunkName: "incentive" */'./index').then(module => {<br />    module.default()<br />}).catch((err) => {<br />    console.error('in catch fn', err)<br />})<br /><br />// await 方法,try catch<br />try {<br />    const module = await import(/* webpackChunkName: "incentive" */'./index');<br />    module.default()<br />} catch(err) {<br />    console.error('in try catch', err)<br />}<br />复制代码<br />

  总结:全局捕获Promise中的错误

  以上三种其实归结为Promise类型的错误,可以通过unhandledrejection捕获

  // 全局统一处理Promisewindow.addEventListener("unhandledrejection", function(e){  console.log('捕获到异常:', e);});fetch('https://tuia.cn/test')复制代码<br />

  为了防止遗漏Promise异常,可以使用unhandledrejection全局监控Uncaught Promise Error。

  视图错误

  由于Vue会捕获所有Vue单文件组件或继承自Vue.extend的代码,因此Vue中发生的错误不会被window.onerror直接捕获,而是抛给Vue.config.errorHandler。

  /** * 全局捕获Vue错误,直接扔出给onerror处理 */Vue.config.errorHandler = function (err) {  setTimeout(() => {    throw err  })}复制代码<br />

  反应错误

  React通过componentDidCatch声明一个边界错误的组件

  class ErrorBoundary extends React.Component {<br />  constructor(props) {<br />    super(props);<br />    this.state = { hasError: false };<br />  }<br /><br />  static getDerivedStateFromError(error) {<br />    // 更新 state 使下一次渲染能够显示降级后的 UI<br />    return { hasError: true };<br />  }<br /><br />  componentDidCatch(error, errorInfo) {<br />    // 你同样可以将错误日志上报给服务器<br />    logErrorToMyService(error, errorInfo);<br />  }<br /><br />  render() {<br />    if (this.state.hasError) {<br />      // 你可以自定义降级后的 UI 并渲染<br />      return Something went wrong.;<br />    }<br /><br />    return this.props.children; <br />  }<br />}<br /><br />class App extends React.Component {<br />   <br />  render() {<br />    return (<br />    <br />      <br />      <br />    )<br />  }<br />}<br />复制代码<br />

  但是错误边界不会捕获以下错误:React 事件处理、异步代码、错误边界本身抛出的错误。

  跨域问题

  一般情况下,如果出现Script error之类的错误,基本可以确定是存在跨域问题。

  如果当前服务页面和云JS位于不同的域名,云JS出错,window.onerror会显示Script Error。可以通过以下两种方法解决。

  const script = document.createElement('script');script.crossOrigin = 'anonymous';script.src = 'http://yun.tuia.cn/test.js';document.body.appendChild(script);复制代码<br />

  <br /><br /><br />  Test page in http://test.com<br /><br /><br />  <br />  <br />  window.onerror = function (message, url, line, column, error) {<br />    console.log(message, url, line, column, error);<br />  }<br /><br />  try {<br />    foo(); // 调用testerror.js中定义的foo方法<br />  } catch (e) {<br />    throw e;<br />  }<br />  <br /><br /><br />复制代码<br />

  你会发现如果不加try catch,console.log会打印script error。可以通过添加try catch来捕获。

  我们来看看现场。一般是调用remote js,常见的有以下三种情况。

  调用方法场景

  通过封装一个函数,可以对原来的方法进行修饰,使其可以被try/catch。

  <br /><br /><br />  Test page in http://test.com<br /><br /><br />  <br />  <br />  window.onerror = function (message, url, line, column, error) {<br />    console.log(message, url, line, column, error);<br />  }<br /><br />  function wrapErrors(fn) {<br />    // don't wrap function more than once<br />    if (!fn.__wrapped__) {<br />      fn.__wrapped__ = function () {<br />        try {<br />          return fn.apply(this, arguments);<br />        } catch (e) {<br />          throw e; // re-throw the error<br />        }<br />      };<br />    }<br /><br />    return fn.__wrapped__;<br />  }<br /><br />  wrapErrors(foo)()<br />  <br /><br /><br /><br />复制代码<br />

  可以尝试去掉wrapErrors感受一下。

  活动现场

  本机方法可以被劫持。

  

" />

  <br /><br /><br />  Test page in http://test.com<br /><br /><br />  <br />    const originAddEventListener = EventTarget.prototype.addEventListener;<br />    EventTarget.prototype.addEventListener = function (type, listener, options) {<br />      const wrappedListener = function (...args) {<br />        try {<br />          return listener.apply(this, args);<br />        }<br />        catch (err) {<br />          throw err;<br />        }<br />      }<br />      return originAddEventListener.call(this, type, wrappedListener, options);<br />    }<br />  <br />  http://test.com<br />  <br />  <br />  window.onerror = function (message, url, line, column, error) {<br />    console.log(message, url, line, column, error);<br />  }<br />  <br /><br /><br />复制代码<br />

  大家可以尝试去掉封装EventTarget.prototype.addEventListener的代码感受一下。

  报告界面

  为什么不能直接使用GET/POST/HEAD请求接口上报?

  这个比较容易想到。一般来说,托管域名不是当前域名,所以所有的接口请求都会构成跨域。

  为什么请求其他文件资源(js/css/ttf)不能报错?

  资源节点创建后,只有将对象注入浏览器DOM树后,浏览器才会真正发送资源请求。而且加载js/css资源也会阻塞页面渲染,影响用户体验。

  不仅不需要在构造图片中插入DOM,只要在js中创建一个Image对象就可以发起请求,不存在阻塞问题。在没有js的浏览器环境下,也可以通过img标签正常管理。

  使用新的图像进行界面报告。最后一题也是一张图。报告时,选择了 1x1 透明 GIF 而不是其他 PNG/JEPG/BMP 文件。

  首先,1x1 像素是最小的合法图像。而且,因为是通过图片来完成的,所以图片应该是透明的,这样才不会影响页面本身的显示效果。两者表示图片是透明的,只要用一个二进制位来标记图片为透明色,就不需要存储颜色空间数据,可以节省体积。因为需要透明色,所以可以直接排除JPEG。

  对于同样的响应,GIF 比 BMP 可以节省 41% 的流量,比 PNG 可以节省 35% 的流量。GIF 是必经之路。

  使用 1*1 gif

  非阻塞加载

  尽量避免SDK js资源加载的影响。

  通过先缓存window.onerror的错误记录,然后异步加载SDK,再在SDK中处理报错。

  <br /><br /><br />    <br />        (function(w) {<br />            w._error_storage_ = [];<br />            function errorhandler(){<br />                // 用于记录当前的错误            <br />                w._error_storage_&&w._error_storage_.push([].slice.call(arguments));<br />            } <br />            w.addEventListener && w.addEventListener("error", errorhandler, true);<br />            var times = 3,<br />            appendScript = function appendScript() {<br />                var sc = document.createElement("script");<br />                sc.async = !0,<br />                sc.src = './build/skyeye.js',  // 取决于你存放的位置<br />                sc.crossOrigin = "anonymous",<br />                sc.onerror = function() {<br />                    times--,<br />                    times > 0 && setTimeout(appendScript, 1500)<br />                },<br />                document.head && document.head.appendChild(sc);<br />            };<br />            setTimeout(appendScript, 1500);<br />        })(window);<br />    <br /><br /><br />    这是一个测试页面(new)<br /><br /><br />复制代码<br />

  采集聚合端(日志服务器)

  在这个环节中,输入是借口收到的错误记录,输出是有效数据存储。核心功能需要清洗数据,顺便解决了过大的服务压力。另一个核心功能是存储数据。

  整体流程可以看做是错误识别-&gt;错误过滤-&gt;错误接收-&gt;错误存储。

  错误识别(SDK配合)

  在聚合之前,我们需要具备识别不同维度错误的能力,可以理解为定位单个错误条目或者单个错误事件的能力。

  单个错误条目 ****

  通过日期和随机值生成对应的错误条目id。

<p>const errorKey = `${+new Date()}@${randomString(8)}`function randomString(len) {      len = len || 32;    let chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';    let maxPos = chars.length;    let pwd = '';      for (let i = 0; i  {<br />  setTimeout(() => {<br />    maxLimit = MAX_LIMIT;<br />    task();<br />  }, TICK);<br />};<br />task();<br /><br />const check = () => {<br />  if (maxLimit 

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线