阅读 462

前端监控系统之错误监控

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

前言

上一篇讲了搭建前端监控系统的接口监控,因为本系列文章主要是讲关于前端收集数据的SDK的实现,这一篇来讲错误监控,收集错误的过程也能学习到很多东西。我们对于经过打包处理的前端应用还会使用sourcemap映射处理错误堆栈信息。

JavaScirpt的常见错误类型

  1. SyntaxError:语法错误

  2. Uncaught ReferenceError(引用错误)

  3. RangeError(范围错误)

  4. TypeError(类型错误)

  5. URIError(URL 错误)

  6. 资源加载错误

  7. 接口错误

  8. promise未处catch的错误

  9. 跨域脚本错误

    我们来区分一下这些错误。

语法错误:

<script>
  const value = 10
  @!
  console.log(value)  
</script>
复制代码

js引擎在解析scrpit代码时,会先进行词法分析, 将js转换成[{}] 格式的tokens流, 为什么要这么做呢? 因为后面要将tokens流转换成抽象语法树, 在生成语法树的过程中,引擎会有语法分析器对语法进行判断, 上述代码语法分析器判断无法生成一颗有效的语法树,抛出语法错误。停止对代码的解析。

抛出错误: Uncaught SyntaxError: Invalid or unexpected token

引用错误:

<script>
  const value = 10
  console.log(test)
  console.log(value)
</script>
复制代码

js引擎对当前script中的代码先进行词法分析,语法解析完成后,成功构成一颗ast语法树,此时js引擎会对当前ast树进行预编译。即在内存中开辟空间,将变量/函数存放到分配的空间中,声明变量/函数,此时变量/函数提升就发生在这个阶段。

预编译阶段的执行流程

  1. 创建GO对象
  2. 声明的变量赋予GO对象,值为undefined,声明的函数赋予GO对象,值为函数体
    1. 遇到函数,就会创建AO对象
    2. 查找函数形参及函数内变量声明,形参名及变量名作为AO对象的属性,值为undefined
    3. 实参形参相统一,实参值赋给形参
    4. 查找函数声明,函数名作为AO对象的属性,值为函数引用
  3. 一切声明的全局变量,全挂载到window上。

当预编译完成后,js引擎会进入运行阶段, 代码运行到console.log(test) 时,进行作用域查找,当查找到顶层window时此时还没有这个变量的声明,那么js引擎抛出错误. 代码不再向下执行。

抛出的错误test.html:12 Uncaught ReferenceError: test is not defined

范围错误:

<script>
  const value = []
  value.length = -1
  console.log(value)
</script>
复制代码

当预编译完成后,js引擎会进入运行阶段,代码运行到value.length = -1 时, js引擎发现了value的length被赋值为了-1。此时js引擎抛出 Uncaught RangeError: Invalid array length 。因为-1 值不在数组所允许的范围或者集合中。抛出错误后,js代码终止。

还有其他这几种情况

// Number 对象的方法参数超出范围
const num = new Number(12.34);
console.log(num.toFixed(-1));
// 函数堆栈超过最大值
const foo = () => foo();
foo(); // RangeError: Maximum call stack size exceeded
复制代码

抛出的错误: Uncaught RangeError: Invalid array length

类型错误:

<script>
  const value = []
  const test = {};
	test.go(); 
  console.log(value)
</script>
复制代码

值的类型或参数不是预期类型时发生的错误

抛出错误: Uncaught TypeError: test.go is not a function

URL 错误:

<script>
  const value = []
  decodeURI("%"); // URIError: URI malformed
  console.log(value)
</script>
复制代码

使用全局 URI 处理函数而产生的错误。

抛出错误: Uncaught URIError: URI malformed at decodeURI (<anonymous>)

资源加载错误:

	<script src="./notfound.js"></script>
复制代码

资源加载错误,即网站中的资源如果加载失败则抛出资源加载错误

抛出错误 GET http://127.0.0.1:5500/notfound.js net::ERR_ABORTED 404 (Not Found)

接口错误

axios.get('/notfound')
复制代码

在我们上一章中对接口监控监听到这个错误

抛出错误: GET http://127.0.0.1:5501/notfound 404 (Not Found)

promise未catch的错误

<script>
  const value = []
  new Promise((resolve, reject) => {
    resolve(a.b)
  })
console.log(value)
</script>
复制代码

在promise中出现的错误会被统一放到Promise的catch中进行处理。如果没有catch则向上抛出,因为执行机制的原因,并不会阻塞线程执行,所以value是可以正常打印

抛出错误: Uncaught (in promise) ReferenceError: a is not defined

跨域脚本错误

	<script src="https://test.bootcdn.net/ajax/libs/test.js"></script>
复制代码

前端需要在script标签中配置 crossorigin 才可以捕获到跨站脚本内部报出的错误,因为浏览器只允许同域下的脚本捕获具体的错误信息。

因为可能有些浏览器对crossorigin 不支持,此时我们用try catch继续向上抛出。

如何捕获

  • window.onerror

    当发生 JavaScript 运行时错误(包括处理程序中引发的语法错误和异常)时,会触发window.onerror的回调。

    但是window.onerror捕获不到资源加载错误

  • 使用window.addEventListener('error')捕获资源错误,但是window.addEventListener('error')也可以捕获到js运行错误,可以通过target?.src || target?.href区分是资源加载错误还是js运行时错误。

既然window.addEventListener('error')也可以捕获到错误,那么我们为什么要用window.onerror呢?

因为window.onerror的事件对象数据更加多,更加清晰。

  • window.addEventListener('unhandledrejection')

    捕获promise未catch的错误

为什么不用try/catch捕获

面试官:请用一句话描述 try catch 能捕获到哪些 JS 异常

async await promise try...catch

设计错误监控数据结构

js运行加载时的数据结构

{
  content: // 堆栈信息
  col: // 列
  row: // 行
  message // 主要错误信息 
  name // 错误的主要name
  resourceUrl // url
  errorMessage // 完整的错误信息
  scriptURI // 脚本url
  lineNumber: // 行号
  columnNumber // 列号
}
// 如果是使用webpack打包的框架代码报错,在处理一次
添加
{
  source // 对应的资源
  sourcesContentMap // sourceMap的信息
}
复制代码

资源错误数据结构

{
  url
}
复制代码

Promise catch的错误

{
  type: // 类型
  reason // 原因
}
复制代码

设计代码

  • 对错误进行格式化处理
let formatError = errObj => {
  let col = errObj.column || errObj.columnNumber // Safari Firefox 浏览器中才有的信息
  let row = errObj.line || errObj.lineNumber // Safari Firefox   浏览器中才有的信息
  let message = errObj.message
  let name = errObj.name

  let { stack } = errObj
  if (stack) {
    let matchUrl = stack.match(/https?:\/\/[^\n]+/) // 匹配从http?s 开始到出现换行符的信息, 这个不仅包括报错的文件信息而且还包括了报错的行列信息
    let urlFirstStack = matchUrl ? matchUrl[0] : ''
    // 获取到报错的文件
    let regUrlCheck = /https?:\/\/(\S)*\.js/

    let resourceUrl = ''
    if (regUrlCheck.test(urlFirstStack)) {
      resourceUrl = urlFirstStack.match(regUrlCheck)[0]
    }

    let stackCol = null // 获取statck中的列信息
    let stackRow = null // 获取statck中的行信息
    let posStack = urlFirstStack.match(/:(\d+):(\d+)/) // // :行:列
    if (posStack && posStack.length >= 3) {
      ;[, stackCol, stackRow] = posStack
    }

    // TODO formatStack
    return {
      content: stack,
      col: Number(col || stackCol),
      row: Number(row || stackRow),
      message,
      name,
      resourceUrl
    }
  }

  return {
    row,
    col,
    message,
    name
  }
}
复制代码
  • 对webpack打包的项目需要单独处理一次
let frameError = async errObj => {
  const result = await $.get(`http://localhost:3000/sourcemap?col=${errObj.col}&row=${errObj.row}`)
  return result
}
复制代码
  • window.onerror

    let _originOnerror = window.onerror
        window.onerror = async (...arg) => {
          let [errorMessage, scriptURI, lineNumber, columnNumber, errorObj] = arg
          let errorInfo = formatError(errorObj)
          // 如果是使用webpack打包的框架代码报错,在处理一次,这里暂时使用resourceUrl字段进行区分是否是框架代码报错
          if (errorInfo.resourceUrl === 'http://localhost:3000/react-app/dist/main.bundle.js') {
            let frameResult = await frameError(errorInfo)
    
            errorInfo.col = frameResult.column
            errorInfo.row = frameResult.line
            errorInfo.name = frameResult.name
            errorInfo.source = frameResult.source
            errorInfo.sourcesContentMap = frameResult.sourcesContentMap
          }
    
          errorInfo._errorMessage = errorMessage
          errorInfo._scriptURI = scriptURI
          errorInfo._lineNumber = lineNumber
          errorInfo._columnNumber = columnNumber
          errorInfo.type = 'onerror'
          cb(errorInfo)
          _originOnerror && _originOnerror.apply(window, arg)
        }
    复制代码
  • window.onunhandledrejection

 let _originOnunhandledrejection = window.onunhandledrejection
    window.onunhandledrejection = (...arg) => {
      let e = arg[0]
      let reason = e.reason
      cb({
        type: e.type || 'unhandledrejection',
        reason
      })
      _originOnunhandledrejection && _originOnunhandledrejection.apply(window, arg)
    }
复制代码
  • window.addEventListener
window.addEventListener(
      'error',
      event => {
        // 过滤js error
        let target = event.target || event.srcElement
        let isElementTarget =
          target instanceof HTMLScriptElement ||
          target instanceof HTMLLinkElement ||
          target instanceof HTMLImageElement
        if (!isElementTarget) return false
        // 上报资源地址
        let url = target.src || target.href
        cb({
          url
        })
      },
      true
    )
复制代码

参考文章

面试官:请用一句话描述 try catch 能捕获到哪些 JS 异常

async await promise try...catch

window.onerror &&window.addEventListener('error')

WindowEventHandlers.onunhandledrejection

文章分类
前端
文章标签