一. js錯誤監控方式
1. 主動判斷
我們在一些運算之后,得到一個期望的結果,然而結果不是我們想要的
// test.js function calc(){ // code... return val; } if(calc() !== "someVal"){ Reporter.send({ position: "test.js::<Function>calc" msg: "calc error" }); }
這種屬於邏輯錯誤/狀態錯誤的反饋,在接口 status
判斷中用的比較多。
2. try..catch
捕獲
判斷一個代碼段中存在的錯誤:
try { init(); // code... } catch(e){ Reporter.send(format(e)); }
以 init
為程序的入口,代碼中所有同步執行出現的錯誤都會被捕獲,這種方式也可以很好的避免程序剛跑起來就掛。
3. window.onerror
捕獲全局錯誤:
window.onerror = function() { var errInfo = format(arguments); Reporter.send(errInfo); return true; };
在上面的函數中返回 return true
,錯誤便不會暴露到控制台中。下面是它的參數信息:
/** * @param {String} errorMessage 錯誤信息 * @param {String} scriptURI 出錯的文件 * @param {Long} lineNumber 出錯代碼的行號 * @param {Long} columnNumber 出錯代碼的列號 * @param {Object} errorObj 錯誤的詳細信息,Anything */ window.onerror = function(errorMessage, scriptURI, lineNumber,columnNumber,errorObj) { // code.. }
window.onerror
算是一種特別暴力的容錯手段,try..catch
也是如此,他們底層的實現就是利用 C/C++ 中的 goto
語句實現,一旦發現錯誤,不管目前的堆棧有多深,不管代碼運行到了何處,直接跑到頂層或者 try..catch
捕獲的那一層,這種一腳踢開錯誤的處理方式並不是很好。
關於 window.onerror 還有兩點需要值得注意
- 對於 onerror 這種全局捕獲,最好寫在所有 JS 腳本的前面,因為你無法保證你寫的代碼是否出錯,如果寫在后面,一旦發生錯誤的話是不會被 onerror 捕獲到的。
- 另外 onerror 是無法捕獲到網絡異常的錯誤。
當我們遇到 <img src="./404.png">
報 404 網絡請求異常的時候,onerror 是無法幫助我們捕獲到異常的。
<script> window.onerror = function (msg, url, row, col, error) { console.log('我知道異步錯誤了'); console.log({ msg, url, row, col, error }) return true; }; </script> <img src="./404.png">
由於網絡請求異常不會事件冒泡,因此必須在捕獲階段將其捕捉到才行,但是這種方式雖然可以捕捉到網絡請求的異常,但是無法判斷 HTTP 的狀態是 404 還是其他比如 500 等等,所以還需要配合服務端日志才進行排查分析才可以。
<script> window.addEventListener('error', (msg, url, row, col, error) => { console.log('我知道 404 錯誤了'); console.log( msg, url, row, col, error ); return true; }, true); </script> <img src="./404.png" alt="">
這點知識還是需要知道,要不然用戶訪問網站,圖片 CDN 無法服務,圖片加載不出來而開發人員沒有察覺就尷尬了。
Promise 錯誤
通過 Promise 可以幫助我們解決異步回調地獄的問題,但是一旦 Promise 實例拋出異常而你沒有用 catch 去捕獲的話,onerror 或 try-catch 也無能為力,無法捕捉到錯誤。
window.addEventListener('error', (msg, url, row, col, error) => { console.log('我感知不到 promise 錯誤'); console.log( msg, url, row, col, error ); }, true); Promise.reject('promise error'); new Promise((resolve, reject) => { reject('promise error'); }); new Promise((resolve) => { resolve(); }).then(() => { throw 'promise error' });
雖然在寫 Promise 實例的時候養成最后寫上 catch 函數是個好習慣,但是代碼寫多了就容易糊塗,忘記寫 catch。
所以如果你的應用用到很多的 Promise 實例的話,特別是你在一些基於 promise 的異步庫比如 axios 等一定要小心,因為你不知道什么時候這些異步請求會拋出異常而你並沒有處理它,所以你最好添加一個 Promise 全局異常捕獲事件 unhandledrejection。
window.addEventListener("unhandledrejection", function(e){ e.preventDefault() console.log('我知道 promise 的錯誤了'); console.log(e.reason); return true; }); Promise.reject('promise error'); new Promise((resolve, reject) => { reject('promise error'); }); new Promise((resolve) => { resolve(); }).then(() => { throw 'promise error' });
window.onerror 能否捕獲 iframe 的錯誤
當你的頁面有使用 iframe 的時候,你需要對你引入的 iframe 做異常監控的處理,否則一旦你引入的 iframe 頁面出現了問題,你的主站顯示不出來,而你卻渾然不知。
首先需要強調,父窗口直接使用 window.onerror 是無法直接捕獲,如果你想要捕獲 iframe 的異常的話,有分好幾種情況。
如果你的 iframe 頁面和你的主站是同域名的話,直接給 iframe 添加 onerror 事件即可。
<iframe src="./iframe.html" frameborder="0"></iframe> <script> window.frames[0].onerror = function (msg, url, row, col, error) { console.log('我知道 iframe 的錯誤了,也知道錯誤信息'); console.log({ msg, url, row, col, error }) return true; }; </script>
讀者可以通過 npm run iframe
查看效果:
如果你嵌入的 iframe 頁面和你的主站不是同個域名的,但是 iframe 內容不屬於第三方,是你可以控制的,那么可以通過與 iframe 通信的方式將異常信息拋給主站接收。與 iframe 通信的方式有很多,常用的如:postMessage,hash 或者 name 字段跨域等等,這里就不展開了,感興趣的話可以看:跨域,你需要知道的全在這里
如果是非同域且網站不受自己控制的話,除了通過控制台看到詳細的錯誤信息外,沒辦法捕獲,這是出於安全性的考慮,你引入了一個百度首頁,人家頁面報出的錯誤憑啥讓你去監控呢,這會引出很多安全性的問題。
異常上報方式
監控拿到報錯信息之后,接下來就需要將捕捉到的錯誤信息發送到信息收集平台上,常用的發送形式主要有兩種:
- 通過 Ajax 發送數據
- 動態創建 img 標簽的形式
實例 - 動態創建 img 標簽進行上報
function report(error) { var reportUrl = 'http://xxxx/report'; new Image().src = reportUrl + 'error=' + error; }
收集異常信息量太多,怎么辦
如果你的網站訪問量很大,假如網頁的 PV 有 1kw,那么一個必然的錯誤發送的信息就有 1kw 條,我們可以給網站設置一個采集率:
Reporter.send = function(data) { // 只采集 30% if(Math.random() < 0.3) { send(data) // 上報錯誤信息 } }
二. 接口請求時長
/** * 攔截接口請求,上報接口信息 */ //統一攔截ajax請求 var start_time = 0, gap_time = 0; //計算請求延時 window.addEventListener('ajaxReadyStateChange', function (e) { var xhr = e.detail, status = xhr.status, readyState = xhr.readyState, responseText = xhr.responseText; /** * 計算請求延時 */ if(readyState == 1){ start_time = (new Date()).getTime(); } if(readyState == 4){ gap_time = (new Date()).getTime() - start_time; } /** * 上報請求信息 * @Author smy * @DateTime 2018-06-27T16:32:30+0800 */ if(readyState == 4){ httpReport(gap_time, status, xhr.responseURL) } })
三. 頁面加載時長
參見:https://blog.oldj.net/2012/01/09/measuring-the-user-latency/
--------------------------------------------------------------
已知問題:
1. ios蘋果不支持crossorigin=“anonymous”, 只支持crossorigin="use-credentials",cdn 不支持credentials。
2. ios 會對客戶端調用webview方法找不到進行報錯到window.onerror, 當上報的值是不存在的對象屬性時,報錯內容為global code,比如這時上報error.stack, 此時error為null,應上報message。
---------------------------------------------------------------
參考:
https://www.cnblogs.com/hustskyking/p/fe-monitor.html
http://web.jobbole.com/93684/
https://github.com/happylindz/blog/issues/5