1. 程式人生 > 其它 >前端異常監控

前端異常監控

前端異常是我們開發中經常出現的,由於一些條件限制,往往線上的前端異常比較難查詢定位,所以如何快速、準確的查詢到異常並上報,是快速解決前端問題的關鍵一步。

一、前端錯誤型別:

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 的崩潰統計方案:

  1. Service Worker 有自己獨立的工作執行緒,與網頁區分開,網頁崩潰了,Service Worker 一般情況下不會崩潰;
  2. Service Worker 生命週期一般要比網頁還要長,可以用來監控網頁的狀態;
  3. 網頁可以通過 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)      // 上報錯誤資訊
  }
}