前端異常監控

本文大體圍繞下面幾點展開討論:
javascript

  1. JS 處理異常的方式
  2. 上報方式
  3. 異常監控上報常見問題

JS 異常處理

對於 Javascript 而言,咱們面對的僅僅只是異常,異常的出現不會直接致使 JS 引擎崩潰,最多隻會使當前執行的任務終止。html

  1. 當前代碼塊將做爲一個任務壓入任務隊列中,JS 線程會不斷地從任務隊列中提取任務執行。
  2. 當任務執行過程當中出現異常,且異常沒有捕獲處理,則會一直沿着調用棧一層層向外拋出,最終終止當前任務的執行。
  3. JS 線程會繼續從任務隊列中提取下一個任務繼續執行。
<script>
  error
  console.log('永遠不會執行');
</script>
<script>
  console.log('我繼續執行')
</script>
複製代碼

在對腳本錯誤進行上報以前,咱們須要對異常進行處理,程序須要先感知到腳本錯誤的發生,而後再談異常上報。前端

腳本錯誤通常分爲兩種:語法錯誤,運行時錯誤。java

下面就談談幾種異常監控的處理方式:webpack

try-catch 異常處理ios

try-catch 在咱們的代碼中常常見到,經過給代碼塊進行 try-catch 進行包裝後,當代碼塊發生出錯時 catch 將能捕捉到錯誤的信息,頁面也將能夠繼續執行。git

可是 try-catch 處理異常的能力有限,只能捕獲捉到運行時非異步錯誤,對於語法錯誤和異步錯誤就顯得無能爲力,捕捉不到。github

示例:運行時錯誤web

try {
  error    // 未定義變量 
} catch(e) {
  console.log('我知道錯誤了');
  console.log(e);
}
複製代碼

然而對於語法錯誤和異步錯誤就捕捉不到了。ajax

示例:語法錯誤

try {
  var error = 'error';   // 大寫分號
} catch(e) {
  console.log('我感知不到錯誤');
  console.log(e);
}
複製代碼

通常語法錯誤在編輯器就會體現出來,常表現的錯誤信息爲:

Uncaught SyntaxError: Invalid or unexpected token xxx

這樣。可是這種錯誤會直接拋出異常,常使程序崩潰,通常在編碼時候容易觀察獲得。

示例:異步錯誤

try {
  setTimeout(() => {
    error        // 異步錯誤
  })
} catch(e) {
  console.log('我感知不到錯誤');
  console.log(e);
}
複製代碼

除非你在 setTimeout 函數中再套上一層 try-catch,不然就沒法感知到其錯誤,但這樣代碼寫起來比較囉嗦。

window.onerror 異常處理

window.onerror 捕獲異常能力比 try-catch 稍微強點,不管是異步仍是非異步錯誤,onerror 都能捕獲到運行時錯誤。

示例:運行時同步錯誤

/**
 * @param {String}  msg    錯誤信息
 * @param {String}  url    出錯文件
 * @param {Number}  row    行號
 * @param {Number}  col    列號
 * @param {Object}  error  錯誤詳細信息
 */
 window.onerror = function (msg, url, row, col, error) {
  console.log('我知道錯誤了');
  console.log({
    msg,  url,  row, col, error
  })
  return true;
};
error;
複製代碼

示例:異步錯誤

window.onerror = function (msg, url, row, col, error) {
  console.log('我知道異步錯誤了');
  console.log({
    msg,  url,  row, col, error
  })
  return true;
};
setTimeout(() => {
  error;
});
複製代碼

然而 window.onerror 對於語法錯誤仍是無能爲力,因此咱們在寫代碼的時候要儘量避免語法錯誤的,不過通常這樣的錯誤會使得整個頁面崩潰,仍是比較容易可以察覺到的。

在實際的使用過程當中,onerror 主要是來捕獲預料以外的錯誤,而 try-catch 則是用來在可預見狀況下監控特定的錯誤,二者結合使用更加高效。

須要注意的是,window.onerror 函數只有在返回 true 的時候,異常纔不會向上拋出,不然即便是知道異常的發生控制檯仍是會顯示

Uncaught Error: xxxxx

關於 window.onerror 還有兩點須要值得注意

  1. 對於 onerror 這種全局捕獲,最好寫在全部 JS 腳本的前面,由於你沒法保證你寫的代碼是否出錯,若是寫在後面,一旦發生錯誤的話是不會被 onerror 捕獲到的。
  2. 另外 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'
});
複製代碼

固然,若是你的應用沒有作 Promise 全局異常處理的話,那極可能就像某乎首頁這樣:

異常上報方式

監控拿到報錯信息以後,接下來就須要將捕捉到的錯誤信息發送到信息收集平臺上,經常使用的發送形式主要有兩種:

  1. 經過 Ajax 發送數據
  2. 動態建立 img 標籤的形式

實例 - 動態建立 img 標籤進行上報

function report(error) {
  var reportUrl = 'http://xxxx/report';
  new Image().src = reportUrl + 'error=' + error;
}
複製代碼

監控上報常見問題

Script error 腳本錯誤是什麼

由於咱們在線上的版本,常常作靜態資源 CDN 化,這就會致使咱們常訪問的頁面跟腳本文件來自不一樣的域名,這時候若是沒有進行額外的配置,就會容易產生 Script error。

可經過

npm run nocors

查看效果。

Script error 是瀏覽器在同源策略限制下產生的,瀏覽器處於對安全性上的考慮,當頁面引用非同域名外部腳本文件時中拋出異常的話,此時本頁面是沒有權利知道這個報錯信息的,取而代之的是輸出 Script error 這樣的信息。

這樣作的目的是避免數據泄露到不安全的域中,舉個簡單的例子,

<script src="xxxx.com/login.html"></script>
複製代碼

上面咱們並無引入一個 js 文件,而是一個 html,這個 html 是銀行的登陸頁面,若是你已經登陸了,那 login 頁面就會自動跳轉到

Welcome xxx…

,若是未登陸則跳轉到

Please Login…

,那麼報錯也會是

Welcome xxx… is not defined,Please Login… is not defined

,經過這些信息能夠判斷一個用戶是否登陸他的賬號,給入侵者提供了十分便利的判斷渠道,這是至關不安全的。

介紹完背景後,那麼咱們應該去解決這個問題?

首先能夠想到的方案確定是同源化策略,將 JS 文件內聯到 html 或者放到同域下,雖然能簡單有效地解決 script error 問題,可是這樣沒法利用好文件緩存和 CDN 的優點,不推薦使用。正確的方法應該是從根本上解決 script error 的錯誤。

跨源資源共享機制( CORS )

首先爲頁面上的 script 標籤添加 crossOrigin 屬性

// http://localhost:8080/index.html
<script>
  window.onerror = function (msg, url, row, col, error) {
    console.log('我知道錯誤了,也知道錯誤信息');
    console.log({
      msg,  url,  row, col, error
    })
    return true;
  };
</script>
<script src="http://localhost:8081/test.js" crossorigin></script>

// http://localhost:8081/test.js
setTimeout(() => {
  console.log(error);
});
複製代碼

當你修改完前端代碼後,你還須要額外給後端在響應頭裏加上

Access-Control-Allow-Origin: localhost:8080

,這裏我以 Koa 爲例。

const Koa = require('koa');
const path = require('path');
const cors = require('koa-cors');
const app = new Koa();

app.use(cors());
app.use(require('koa-static')(path.resolve(__dirname, './public')));

app.listen(8081, () => {
  console.log('koa app listening at 8081')
});
複製代碼

讀者可經過

npm run cors

詳細的跨域知識我就不展開了,有興趣能夠看看我以前寫的文章:跨域,你須要知道的全在這裏

你覺得這樣就完了嗎?並無,下面就說一些 Script error 你不常碰見的點:

咱們都知道 JSONP 是用來跨域獲取數據的,而且兼容性良好,在一些應用中仍然會使用到,因此你的項目中可能會用這樣的代碼:

// http://localhost:8080/index.html
window.onerror = function (msg, url, row, col, error) {
  console.log('我知道錯誤了,但不知道錯誤信息');
  console.log({
    msg,  url,  row, col, error
  })
  return true;
};
function jsonpCallback(data) {
  console.log(data);
}
const url = 'http://localhost:8081/data?callback=jsonpCallback';
const script = document.createElement('script');
script.src = url;
document.body.appendChild(script);
複製代碼

由於返回的信息會當作腳本文件來執行,一旦返回的腳本內容出錯了,也是沒法捕捉到錯誤的信息。

解決辦法也不難,跟以前同樣,在添加動態添加腳本的時候加上 crossOrigin,而且在後端配上相應的 CORS 字段便可.

const script = document.createElement('script');
script.crossOrigin = 'anonymous';
script.src = url;
document.body.appendChild(script);
複製代碼

讀者能夠經過

npm run jsonp

查看效果

知道原理以後你可能會以爲沒什麼,不就是給每一個動態生成的腳本添加 crossOrigin 字段嘛,可是在實際工程中,你多是面向不少庫來編程,好比使用 jQuery,Seajs 或者 webpack 來異步加載腳本,許多庫封裝了異步加載腳本的能力,以 jQeury 爲例你多是這樣來觸發異步腳本。

$.ajax({
  url: 'http://localhost:8081/data',
  dataType: 'jsonp',
  success: (data) => {
    console.log(data);
  }
})
複製代碼

假如這些庫中沒有提供 crossOrigin 的能力的話(jQuery jsonp 可能有,僞裝你不知道),那你只能去修改人家寫的源代碼了,因此我這裏提供一個思路,就是去劫持 document.createElement,從根源上去爲每一個動態生成的腳本添加 crossOrigin 字段。

document.createElement = (function() {
  const fn = document.createElement.bind(document);
  return function(type) {
    const result = fn(type);
    if(type === 'script') {
      result.crossOrigin = 'anonymous';
    }
    return result;
  }
})();
window.onerror = function (msg, url, row, col, error) {
  console.log('我知道錯誤了,也知道錯誤信息');
  console.log({
    msg,  url,  row, col, error
  })
  return true;
};
$.ajax({
  url: 'http://localhost:8081/data',
  dataType: 'jsonp',
  success: (data) => {
    console.log(data);
  }
})
複製代碼

效果也是同樣的,讀者能夠經過

npm run jsonpjq

來查看效果:

這樣重寫 createElement 理論上沒什麼問題,可是入侵了本來的代碼,不保證必定不會出錯,在工程上仍是須要多嘗試下看看再使用,可能存在兼容性上問題,若是你以爲會出現什麼問題的話也歡迎留言討論下。

關於 Script error 的問題就寫到這裏,若是你理解了上面的內容,基本上絕大部分的 Script 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 字段跨域等等,這裏就不展開了,感興趣的話能夠看:跨域,你須要知道的全在這裏

若是是非同域且網站不受本身控制的話,除了經過控制檯看到詳細的錯誤信息外,沒辦法捕獲,這是出於安全性的考慮,你引入了一個百度首頁,人家頁面報出的錯誤憑啥讓你去監控呢,這會引出不少安全性的問題。

壓縮代碼如何定位到腳本異常位置

線上的代碼幾乎都通過了壓縮處理,幾十個文件打包成了一個並醜化代碼,當咱們收到

a is not defined

的時候,咱們根本不知道這個變量 a 到底是什麼含義,此時報錯的錯誤日誌顯然是無效的。

第一想到的辦法是利用 sourcemap 定位到錯誤代碼的具體位置,詳細內容能夠參考:Sourcemap 定位腳本錯誤

另外也能夠經過在打包的時候,在每一個合併的文件之間添加幾行空格,並相應加上一些註釋,這樣在定位問題的時候很容易能夠知道是哪一個文件報的錯誤,而後再經過一些關鍵詞的搜索,能夠快速地定位到問題的所在位置。

收集異常信息量太多,怎麼辦

若是你的網站訪問量很大,假如網頁的 PV 有 1kw,那麼一個必然的錯誤發送的信息就有 1kw 條,咱們能夠給網站設置一個採集率:

Reporter.send = function(data) {
  // 只採集 30%
  if(Math.random() < 0.3) {
    send(data)      // 上報錯誤信息
  }
}
複製代碼

這個採集率能夠經過具體實際的狀況來設定,方法多樣化,可使用一個隨機數,也能夠具體根據用戶的某些特徵來進行斷定。

上面差很少是我對前端代碼監控的一些理解,提及來容易,可是一旦在工程化運用,不免須要考慮到兼容性等種種問題,讀者能夠經過本身的具體狀況進行調整,前端代碼異常監控對於咱們的網站的穩定性起着相當重要的做用。如若文中全部不對的地方,還望指正。

相關文章
相關標籤/搜索