前端監控系統之錯誤監控

這是我參與8月更文挑戰的第5天,活動詳情查看:8月更文挑戰javascript

前言

上一篇講了搭建前端監控系統的接口監控,由於本系列文章主要是講關於前端收集數據的SDK的實現,這一篇來說錯誤監控,收集錯誤的過程也能學習到不少東西。咱們對於通過打包處理的前端應用還會使用sourcemap映射處理錯誤堆棧信息。html

JavaScirpt的常見錯誤類型

  1. SyntaxError:語法錯誤前端

  2. Uncaught ReferenceError(引用錯誤)java

  3. RangeError(範圍錯誤)react

  4. TypeError(類型錯誤)webpack

  5. URIError(URL 錯誤)ios

  6. 資源加載錯誤web

  7. 接口錯誤面試

  8. promise未處catch的錯誤ajax

  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

相關文章
相關標籤/搜索