前端異常監控
前端異常是我們開發中經常出現的,由於一些條件限制,往往線上的前端異常比較難查詢定位,所以如何快速、準確的查詢到異常並上報,是快速解決前端問題的關鍵一步。
一、前端錯誤型別:
1、ECMAScript exceptions;
2、DOMException;
3、網路靜態資源載入錯誤;
4、跨域引用script導致的script error;
5、頁面崩潰。
ECMAScript exceptions
ECMAScript異常就是javascript在執行過程中所發生的錯誤。每一種錯誤都有對應的錯誤型別。當錯誤發生的時候,就會丟擲相應型別的錯誤物件。主要有以下六種錯誤型別:
(1) SyntaxError:語法錯誤
// 1. Syntax Error: 語法錯誤
// 1.1 變數名不符合規範
var 1 // Uncaught SyntaxError: Unexpected number
var 1a // Uncaught SyntaxError: Invalid or unexpected token
// 1.2 給關鍵字賦值
function = 5 // Uncaught SyntaxError: Unexpected token =
// 1.3 關鍵詞寫錯
lett a = 1
(2) Uncaught ReferenceError:引用錯誤
引用一個不存在的變數時發生的錯誤。將一個值分配給無法分配的物件,比如對函式的執行結果或者函式賦值。
// 2.1 引用了不存在的變數
a() // Uncaught ReferenceError: a is not defined
console.log(b) // Uncaught ReferenceError: b is not defined
// 2.2 給一個無法被賦值的物件賦值
console.log("abc") = 1 // Uncaught ReferenceError: Invalid left-hand side in assignment
(3)RangeError:範圍錯誤
// 3.1 陣列長度為負數 [].length = -5 // Uncaught RangeError: Invalid array length // 3.2 Number物件的方法引數超出範圍 12.89.toFixed(-1) // Uncaught RangeError: toFixed() digits argument must be between 0 and 20 at Number.toFixed // 說明: toFixed方法的作用是將數字四捨五入為指定小數位數的數字,引數是小數點後的位數,範圍為0-20.
(4) TypeError型別錯誤
變數和引數不是預期型別時報的錯,比如呼叫不存在的方法,使用new操作符後面跟基本型別
// 4.1 呼叫不存在的方法
123() // Uncaught TypeError: 123 is not a function
var o = {}
o.run() // Uncaught TypeError: o.run is not a function
// 4.2 new關鍵字後接基本型別
var p = new 456 // Uncaught TypeError: 456 is not a constructor
(5) URIError,URL錯誤
decodeURI("%") // Uncaught URIError: URI malformed at decodeURI
URI相關引數不正確時丟擲的錯誤,主要涉及encodeURI、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()六個函式。
(6)EvalError eval()函式執行錯誤
eval("2+3") // 返回 5
var myeval = eval; // 可能會丟擲 EvalError 異常
myeval("2+3"); // 可能會丟擲 EvalError 異常
需要注意的是:ES5以上的JavaScript中已經不再丟擲該錯誤,但依然可以通過new關鍵字來自定義該型別的錯誤提示。
DOMException
DOMException就是我們在呼叫web API方法或者屬性的時候所發生的錯誤,或者簡單地說是在執行DOM操作的時候所丟擲的錯誤。
比如在錯誤地呼叫了DOM介面:
<head>
<script type="text/javascript">
function ThrowDOMException () {
var elem = document.createAttribute ("123");
}
</script>
</head>
<body>
<button onclick="ThrowDOMException ()">Throw a DOM exception</button>
</body>
瀏覽器報錯:Uncaught DOMException: Failed to execute 'createAttribute' on 'Document': The localName provided ('123') contains an invalid character.
網路靜態資源載入錯誤
常見的網路靜態資源包括有,html檔案,css檔案,javascript檔案,圖片,音訊,視訊,iframe等等。所有的網路靜態資源都有可能遇到載入錯誤這個問題。載入錯誤的原因也不一而定,有可能是url寫錯了,有可能是伺服器根本就沒有這個資源,有可能是伺服器出現內部錯誤,也有可能是網路忙,請求超時而導致資源載入失敗。不管什麼原因,凡是客戶端沒有載入成功的,一律視為出現了載入錯誤。
script error
當跨域引用了另外一個域下javascript資源,並且這個javascript執出錯的情況下,瀏覽器就會丟擲這個錯誤
假如,我們在your.com域名下有這麼一個js檔案:
const a = {};
console.log(a.b.c);
我們在mine.com 去引用這個檔案
<script src="http://your.com/index.js"></script>
那麼這種場景下,瀏覽器就會丟擲一個script error型別的錯誤。script error型別的錯誤基本上是在告訴你,你跨域引用的指令碼執行出錯了,但是你沒有知道具體錯誤資訊的權利。
頁面崩潰
當你訪問一個不靠譜的web應用的時候,它可能會做出一個瘋狂和不可預測的操作,這個時候,就會導致整個瀏覽器的崩潰..
二、錯誤處理
錯誤處理的幾個方法:
- try...catch
- window.onerror
- window.addEventListener('error',()=>{})
- Promise Catch與window.addEventListener("unhandledrejection",()=> {})
- iframe與iframe.onload
- 其他
try...catch
try-catch 只能捕獲到同步的執行時錯誤,對語法和非同步錯誤卻無能為力,捕獲不到。
1、同步執行時錯誤:
try {
let name = 'jartto';
console.log(nam);
} catch(e) {
console.log('捕獲到異常:',e);
}
捕獲到異常:ReferenceError: nam is not defined
at <anonymous>:3:15
2、不能捕獲到具體的語法錯誤,只有一個語法錯誤提示。我們修改一下程式碼,刪掉一個單引號:
try {
let name = 'jartto;
console.log(nam);
} catch(e) {
console.log('捕獲到異常:',e);
}
Uncaught SyntaxError: Invalid or unexpected token
不過語法錯誤在我們開發階段就可以看到,應該不會順利上到線上環境。
3、非同步錯誤
try {
setTimeout(() => {
undefined.map(v => v);
}, 1000)
} catch(e) {
console.log('捕獲到異常:',e);
}
Uncaught TypeError: Cannot read property 'map' of undefined
at setTimeout (<anonymous>:3:11)
window.onerror
當 JS 執行時錯誤發生時,window 會觸發一個 ErrorEvent 介面的 error 事件,並執行 window.onerror()。
/**
* @param {String} message 錯誤資訊
* @param {String} source 出錯檔案
* @param {Number} lineno 行號
* @param {Number} colno 列號
* @param {Object} error Error物件(物件)
*/
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕獲到異常:',{message, source, lineno, colno, error});
}
1、同步程式碼執行錯誤
window.onerror = function(message, source, lineno, colno, error) {
// message:錯誤資訊(字串)。
// source:發生錯誤的指令碼URL(字串)
// lineno:發生錯誤的行號(數字)
// colno:發生錯誤的列號(數字)
// error:Error物件(物件)
console.log('捕獲到異常:',{message, source, lineno, colno, error});
}
Jartto;
可以看到,我們捕獲到了異常:
2、再試試語法錯誤
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕獲到異常:',{message, source, lineno, colno, error});
}
let name = 'Jartto
Uncaught SyntaxError: Invalid or unexpected token
沒有捕獲語法錯誤
3、非同步程式碼錯誤
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕獲到異常:',{message, source, lineno, colno, error});
}
setTimeout(() => {
Jartto;
});
捕獲到異常:{message: "Uncaught ReferenceError: Jartto is not defined", source: "http://127.0.0.1:8001/", lineno: 36, colno: 5, error: ReferenceError: Jartto is not defined
at setTimeout (http://127.0.0.1:8001/:36:5)}
setTimeout、setInterval的錯誤是可以捕獲的,但是其他的比如Promise、async裡面的錯誤不能捕獲:
window.onerror = function (message, source, lineno, colno, error) {
console.log('window.onerror捕獲的異常:', message)
return true
}
new Promise((resolve, reject) => {
console.log(a)
resolve(1)
})
async function test() {
console.log(a)
}
4、接著,我們試試網路請求異常的情況:
<script>
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕獲到異常:',{message, source, lineno, colno, error});
return true;
}
</script>
<img src="./jartto.png">
我們發現,不論是靜態資源異常,或者介面異常,錯誤都無法捕獲到
window.onerror 函式只有在返回 true 的時候,異常才不會向上丟擲,否則即使是知道異常的發生控制檯還是會顯示 Uncaught Error: xxxxx
需要注意:
- onerror 最好寫在所有 JS 指令碼的前面,否則有可能捕獲不到錯誤;
- onerror 無法捕獲語法錯誤;
在實際的使用過程中,onerror 主要是來捕獲預料之外的錯誤,而 try-catch 則是用來在可預見情況下監控特定的錯誤,兩者結合使用更加高效。
window.addEventListener
當一項資源(如圖片或指令碼)載入失敗,載入資源的元素會觸發一個 Event 介面的 error 事件,並執行該元素上的onerror() 處理函式。這些 error 事件不會向上冒泡到 window ,不過(至少在 Firefox 中)能被單一的window.addEventListener 捕獲。
window.addEventListener('error', (error) => {
console.log('addEventListener捕獲到異常:', error);
}, true)
<img src="./jartto.png" alt="">
這種方法不能判斷資源載入異常是404還是500等,需要配合後臺日誌查詢
需要注意:
不同瀏覽器下返回的 error 物件可能不同,需要注意相容處理。
需要注意避免 addEventListener 重複監聽。
Promise Catch
在 promise 中使用 catch 可以非常方便的捕獲到非同步 error。
沒有寫 catch 的 Promise 中丟擲的錯誤無法被 onerror 或 try-catch 捕獲到,所以我們務必要在 Promise 中不要忘記寫 catch 處理丟擲的異常。
解決方案:為了防止有漏掉的 Promise 異常,建議在全域性增加一個對 unhandledrejection 的監聽,用來全域性監聽Uncaught Promise Error。
window.addEventListener("unhandledrejection", function(e){
e.preventDefault() // 用於去除控制檯的報錯
console.log('捕獲到異常:', e);
return true;
});
new Promise((resolve, reject) => {
console.log(cxc)
})
iframe與iframe.onload
用window.onerror處理
<iframe src="./iframe.html" frameborder="0"></iframe>
<script>
window.frames[0].onerror = function (message, source, lineno, colno, error) {
console.log('捕獲到 iframe 異常:',{message, source, lineno, colno, error});
return true;
};
</script>
Page Crash
if(sessionStorage.getItem('good_exit') &&
sessionStorage.getItem('good_exit') !== 'true') {
/*
insert crash logging code here
*/
alert('Hey, welcome back from your crash, looks like you crashed on: ' + sessionStorage.getItem('time_before_crash'));
}
window.addEventListener('load', function () {
sessionStorage.setItem('good_exit', 'pending');
setInterval(function () {
sessionStorage.setItem('time_before_crash', new Date().toString());
}, 1000);
});
window.addEventListener('beforeunload', function () {
sessionStorage.setItem('good_exit', 'true');
});
處理頁面崩潰的原理是,崩潰的頁面關閉時是無法觸發beforeunload的。當第一次進去頁面是崩潰的時候,good_exit是pending狀態,重新整理後,因為無法監聽到beforeunload,再次進去的時候還是pending,這時候就可以斷定崩潰了。
基於 Service Worker 的崩潰統計方案:
- Service Worker 有自己獨立的工作執行緒,與網頁區分開,網頁崩潰了,Service Worker 一般情況下不會崩潰;
- Service Worker 生命週期一般要比網頁還要長,可以用來監控網頁的狀態;
- 網頁可以通過 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 傳送訊息。
方案:
- p1:網頁載入後,通過 postMessage API 每 5s 給 sw 傳送一個心跳,表示自己的線上,sw 將線上的網頁登記下來,更新登記時間;
- p2:網頁在 beforeunload 時,通過 postMessage API 告知自己已經正常關閉,sw 將登記的網頁清除;
- p3:如果網頁在執行的過程中 crash 了,sw 中的 running 狀態將不會被清除,更新時間停留在奔潰前的最後一次心跳;
- p4:Service Worker 每 10s - 檢視一遍登記中的網頁,發現登記時間已經超出了一定時間(比如 15s)即可判定該網頁 crash 了。
Script Error
一般情況,如果出現 Script error 這樣的錯誤,基本上可以確定是出現了跨域問題。
<script src="http://jartto.wang/main.js" crossorigin="anonymous"></script>
特別注意,伺服器端需要設定:Access-Control-Allow-Origin
Vue中錯誤的處理
errorHandler
指定元件的渲染和觀察期間未捕獲錯誤的處理函式。這個處理函式被呼叫時,可獲取錯誤資訊和 Vue 例項
// main.js
Vue.config.errorHandler = function (err, vm, info) {
#處理錯誤資訊, 進行錯誤上報
#err錯誤物件
#vm Vue例項
#`info` 是 Vue 特定的錯誤資訊,比如錯誤所在的生命週期鉤子
#只在 2.2.0+ 可用
}
從 2.2.0 起,這個鉤子也會捕獲元件生命週期鉤子裡的錯誤。同樣的,當這個鉤子是 undefined 時,被捕獲的錯誤會通過 console.error 輸出而避免應用崩潰。
從 2.4.0 起,這個鉤子也會捕獲 Vue 自定義事件處理函式內部的錯誤了。
從 2.6.0 起,這個鉤子也會捕獲 v-on DOM 監聽器內部丟擲的錯誤。
errorCaptured
errorCaptured
當捕獲一個來自子孫元件的錯誤時被呼叫。此鉤子會收到三個引數:錯誤物件、發生錯誤的元件例項以及一個包含錯誤來源資訊的字串。此鉤子可以返回 false 以阻止該錯誤繼續向上傳播
舉個例子:
Vue.component('cat', {
template:`
<div><h1>Cat: </h1>
<slot></slot>
</div>`,
props:{
name:{
required:true,
type:String
}
},
errorCaptured(err,vm,info) {
console.log(`cat EC: ${err.toString()}\ninfo: ${info}`);
return false;
}
});
Vue.component('kitten', {
template:'<div><h1>Kitten: {{ dontexist() }}</h1></div>',
props:{
name:{
required:true,
type:String
}
}
});
其中kitten是有bug的
<div id="app" v-cloak>
<cat name="my cat">
<kitten></kitten>
</cat>
</div>
執行結果:
cat EC: TypeError: dontexist is not a function
info: render
三、錯誤上報
無論是埋點上報還是錯誤上報,常用方案就是使用Image物件來發送請求。這麼做,有以下幾點好處:
1、瀏覽器相容性好。所有瀏覽器都支援Image物件。
2、可以避免same-origin-policy的限制。實際開發中,通常都是一臺伺服器負責接收所有的伺服器的錯誤上報。而這種情況下,就會存在跨域問題,單純的XMLHttpRequest是解決不了這個問題。
3、在上報過程上,本身出現錯誤的概率比較低。如果你自己封裝了一個ajax庫或者使用了外部ajax庫,如果這些庫本身就有問題的話,你還指望它去上報的話,可想而知,結果是無法上報成功的
下面,我們簡單實現一個用於上報錯誤的方法:
function postError(type, msg) {
const img = new Image();
img.src = `log.php?type=${encodeURICoponent(type)}&msg=${encodeURICoponent(msg)}`
}
設定採集率:
postError.send = function(data) {
// 只採集 30%
if(Math.random() < 0.3) {
send(data) // 上報錯誤資訊
}
}