聊聊前端監控——錯誤監控篇
阿新 • • 發佈:2020-09-02
當有人問起:你們的公司的這款應用使用者體驗怎麼樣呀?訪問量怎麼樣?此時,你該怎麼回答呢?你會回答:UV、PV 巴拉巴拉,秒開率、FP、TTI 巴拉巴拉。
那麼,這些資料是哪裡來的呢?顯而易見,這些資料都來自前端監控系統。
### 前端監控的意義
當今時代,是一個快節奏的時代,應用的效能極大影響著使用者的留存率,沒有使用者會忍受一個卡到爆的應用。而監控應用效能的重擔,就由前端監控系統肩負著。
其次,對於線上應用來說,故障是不可避免的,對於高日活的應用來說,每次故障都意味著大量的損失。試想,如果是淘寶掛了一天,那麼損失是多麼慘痛。所以,對於開發人員來說,必須要儘早發現線上故障,而不是等到客戶打爆客服的電話才發現。線上錯誤監控,也是前端監控的任務之一。
最後,作為商業公司,需要根據使用者行為和資料進行分析,進一步制定各種策略,如果沒有各種資料,那麼 BI 會熱情的找你談談人生。而這些資料,也是前端監控系統獲取的。
總而言之,前端監控肩負著:效能監控、錯誤監控以及資料上報等功能,無論對於大公司還是小公司,可以說是必不可缺的了。
今天,我們先來聊聊前端監控中的錯誤監控。
### 錯誤監控概述
一般來說,按照錯誤監控錯誤監控可以分為:指令碼錯誤監控、請求錯誤監控以及資源錯誤監控。
### 指令碼錯誤監控
指令碼錯誤大體可以分為兩種:編譯時錯誤以及執行時錯誤。其中,編譯時錯誤一般在開發階段就會發現,配合 lint 工具比如 eslint、tslint 等以及 git 提交外掛比如 husky 等,基本可以保證線上程式碼不出現低階的編譯時錯誤。大廠一般都有釋出前置檢測平臺,能夠在釋出前提前發現編譯時錯誤,當然,原理依然和之前所說的類似。
而發現並上報執行時錯誤就是前端檢測平臺的本質工作啦,一般來說,指令碼錯誤監控指的就是執行時錯誤監控。
說到指令碼錯誤監控,你想到的第一個是什麼?對,就是 `try catch` !
在編寫 JavaScript 時,我們為了防止出現錯誤阻塞程式,我們會通過 `try catch` 捕獲錯誤,對於錯誤捕獲,這是最簡單也是最通用的方案。
但是,`try catch` 捕獲錯誤是侵入式的,需要在開發程式碼時即提前進行處理,而作為一個監控系統,無法做到在所有可能產生錯誤的程式碼片段中都嵌入 `try catch`。所以,我們需要全域性捕獲指令碼錯誤。
#### 常規指令碼錯誤
當頁面出現指令碼錯誤時,就會產生 `onerror` 事件,我們只需捕獲該事件即可。
```js
/**
* @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` 來代替。
```js
/**
* @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` 事件。
```js
// 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](https://cn.vuejs.org/v2/api/#errorHandler) 這個 API 來捕獲並處理錯誤。
```js
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` ,而資源錯誤沒有,這些都可以作為判斷資源錯誤或者指令碼錯誤的依據。
```js
/**
* @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…](https://github.com/KarthusLor