聊聊前端監控——錯誤監控篇

格子熊發表於2020-09-02

當有人問起:你們的公司的這款應用使用者體驗怎麼樣呀?訪問量怎麼樣?此時,你該怎麼回答呢?你會回答:UV、PV 巴拉巴拉,秒開率、FP、TTI 巴拉巴拉。

那麼,這些資料是哪裡來的呢?顯而易見,這些資料都來自前端監控系統。

前端監控的意義

當今時代,是一個快節奏的時代,應用的效能極大影響著使用者的留存率,沒有使用者會忍受一個卡到爆的應用。而監控應用效能的重擔,就由前端監控系統肩負著。

其次,對於線上應用來說,故障是不可避免的,對於高日活的應用來說,每次故障都意味著大量的損失。試想,如果是淘寶掛了一天,那麼損失是多麼慘痛。所以,對於開發人員來說,必須要儘早發現線上故障,而不是等到客戶打爆客服的電話才發現。線上錯誤監控,也是前端監控的任務之一。

最後,作為商業公司,需要根據使用者行為和資料進行分析,進一步制定各種策略,如果沒有各種資料,那麼 BI 會熱情的找你談談人生。而這些資料,也是前端監控系統獲取的。

總而言之,前端監控肩負著:效能監控、錯誤監控以及資料上報等功能,無論對於大公司還是小公司,可以說是必不可缺的了。

今天,我們先來聊聊前端監控中的錯誤監控。

錯誤監控概述

一般來說,按照錯誤監控錯誤監控可以分為:指令碼錯誤監控、請求錯誤監控以及資源錯誤監控。

指令碼錯誤監控

指令碼錯誤大體可以分為兩種:編譯時錯誤以及執行時錯誤。其中,編譯時錯誤一般在開發階段就會發現,配合 lint 工具比如 eslint、tslint 等以及 git 提交外掛比如 husky 等,基本可以保證線上程式碼不出現低階的編譯時錯誤。大廠一般都有釋出前置檢測平臺,能夠在釋出前提前發現編譯時錯誤,當然,原理依然和之前所說的類似。

而發現並上報執行時錯誤就是前端檢測平臺的本質工作啦,一般來說,指令碼錯誤監控指的就是執行時錯誤監控。

說到指令碼錯誤監控,你想到的第一個是什麼?對,就是 try catch

在編寫 JavaScript 時,我們為了防止出現錯誤阻塞程式,我們會通過 try catch 捕獲錯誤,對於錯誤捕獲,這是最簡單也是最通用的方案。

但是,try catch 捕獲錯誤是侵入式的,需要在開發程式碼時即提前進行處理,而作為一個監控系統,無法做到在所有可能產生錯誤的程式碼片段中都嵌入 try catch。所以,我們需要全域性捕獲指令碼錯誤。

常規指令碼錯誤

當頁面出現指令碼錯誤時,就會產生 onerror 事件,我們只需捕獲該事件即可。

/**
 * @description window.onerror 全域性捕獲錯誤
 * @param event 錯誤資訊,如果是
 * @param source 錯誤原始檔URL
 * @param lineno 行號
 * @param colno 列號
 * @param error Error物件
 */
window.onerror = function (event, source, lineno, colno, error) {
  // 上報錯誤
  // 如果不想在控制檯丟擲錯誤,只需返回 true 即可
};

可以發現,各種錯誤監控所需的資訊,如錯誤資訊、錯誤原始檔的 URL、錯誤行號、錯誤列號都被回撥函式所傳入。

但是,window.onload 有兩個缺點:

  1. 只能繫結一個回撥函式,如果想在不同檔案中想繫結不同的回撥函式,window.onload 顯然無法完成;同時,不同回撥函式直接容易造成互相覆蓋。
  2. 回撥函式的引數過於離散,使用不方便

所以,一般情況下,我們使用 addEventListener 來代替。

/**
 * @param event 事件名
 * @param function 回撥函式
 * @param useCapture 回撥函式是否在捕獲階段執行,預設是false,在冒泡階段執行
 */
window.addEventListener('error', (event) => {
  // addEventListener 回撥函式的離散引數全部聚合在 error 物件中
  // 上報錯誤
}, true)

tips:在一些特殊情況下,我們依然需要使用 window.onload。比如,不期望在控制檯丟擲錯誤時,因為只有 window.onload 才能阻止丟擲錯誤到控制檯

Promise 錯誤

使用了這兩種方法,是不是可以捕獲所有指令碼錯誤了呢?這個問題再幾年前其實是正確的,但是隨著前端技術的發展,出現了 Promise 這項技術,而使用這兩種常規方法無法捕獲 Promise 錯誤。

和常規指令碼錯誤的捕獲一樣,我們只需捕獲 Promise 對應的錯誤事件即可。而 Promise 錯誤事件有兩種,unhandledrejection 以及 rejectionhandled

Promise 被 reject 且沒有 reject 處理器的時候,會觸發 unhandledrejection 事件。

Promise 被 reject 且有 reject 處理器的時候,會觸發 rejectionhandled 事件。

// unhandledrejection 推薦處理方案
window.addEventListener('unhandledrejection', (event) => {
  console.log(event)
}, true);

// unhandledrejection 備選處理方案
window.onunhandledrejection = function (error) {
  console.log(error)
}

// rejectionhandled 推薦處理方案
window.addEventListener('rejectionhandled', (event) => {
  console.log(event)
}, true);

// rejectionhandled 備選處理方案
window.onrejectionhandled = function (error) {
  console.log(error)
}

框架錯誤

由於我 React 使用的不多,所以在此只討論下 Vue 的框架錯誤處理,如果有大佬瞭解 React 的框架錯誤處理,歡迎補充~

在 Vue 中,框架提供了 errorHandler 這個 API 來捕獲並處理錯誤。

Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` 是 Vue 特定的錯誤資訊,比如錯誤所在的生命週期鉤子
  // 只在 2.2.0+ 可用
}

值得一提的是,框架錯誤指的不是框架層面的錯誤,而是指框架提供了 API 來捕獲全域性錯誤。

請求錯誤監控

一般來說,前端請求有兩種方案,使用 ajax 或者 fetch ,所以只需重寫兩種方法,進行代理,即可實現請求錯誤監控。

代理的核心在於使用 apply 重新執行原有方法,並且在執行原有方法之前進行監聽操作。在請求錯誤監控中,我們關心三種錯誤事件:abort,error 以及 timeout,所以,只需在代理中對這三種事件進行統一處理即可。

tips:如果能夠統一使用一種請求工具,如 axios 等,那麼不需要重寫 ajax 或者 fetch 只需在請求攔截器以及響應攔截器進行處理上報即可

資源錯誤監控

資源錯誤監控本質上和常規指令碼錯誤監控一樣,都是監控錯誤事件實現錯誤捕獲。

那麼如果區分指令碼錯誤還是資源錯誤呢?我們可以通過 instanceof 區分,指令碼錯誤引數物件 instanceof ErrorEvent,而資源錯誤的引數物件 instanceof Event

值得一提的是,由於 ErrorEvent 繼承於 Event ,所以不管是指令碼錯誤還是資源錯誤的引數物件,它們都 instanceof Event,所以,需要先判斷指令碼錯誤。

此外,兩個引數物件之間有一些細微的不同,比如,指令碼錯誤的引數物件中包含 message ,而資源錯誤沒有,這些都可以作為判斷資源錯誤或者指令碼錯誤的依據。

/**
 * @param event 事件名
 * @param function 回撥函式
 * @param useCapture 回撥函式是否在捕獲階段執行,預設是false,在冒泡階段執行
 */
window.addEventListener('error', (event) => {
  if (event instanceof ErrorEvent) {
    console.log('指令碼錯誤')
  } else if (event instanceof Event) {
    console.log('資源錯誤')
  }
}, true);

tips:使用 addEventListener 捕獲資源錯誤時,一定要將 useCapture 即第三個選項設為 true,因為資源錯誤沒有冒泡,所以只能在捕獲階段捕獲。同理,由於 window.onerror 是通過在冒泡階段捕獲錯誤,所以無法捕獲資源錯誤。

補充:跨域指令碼錯誤捕獲

為了效能方面的考慮,我們一般會將指令碼檔案放到 CDN ,這種方法會大大加快首屏時間。但是,如果指令碼報錯,此時,瀏覽器出於於安全方面的考慮,對於不同源的指令碼報錯,無法捕獲到詳細錯誤資訊,只會顯示 Script Error。那麼,有解決該問題的方案嗎?

  1. 方案一:所有的指令碼全部放到同一源下,但是,該方案會放棄 CDN ,降低效能。
  2. 方案二:在 script 標籤中,新增 crossorigin 屬性(推薦使用 webpack 外掛自動新增);同時,配置 CDN 伺服器,為跨域指令碼配上 CORS

可以發現,方案二基本可以完美解決跨域指令碼錯誤捕獲的問題。但是,其實該方案有一個隱藏的坑,即相容性問題,crossorigin 屬性對於 IE 以及 Safari 支援程度不高。

所以,該如何真正完美的解決跨域指令碼錯誤捕獲問題?

終極解決方案:對所有原生方法進行代理~

但是,一方面,很難覆蓋所有的原生方法,另一方面,對原生方法進行代理容易出現無法預知的問題。

綜合所有方案,看起來還是方案二最靠譜,至於低階瀏覽器,就讓它們隨風消逝吧~

如果有不同想法的同學,歡迎一起交流~

我的 github:github.com/KarthusLori…

相關文章