前端錯誤監控指南

前言

做爲一個前端,在開發過程即使十分當心,自測充分,在不一樣用戶複雜的操做下也不免會出現程序員意想不到的問題,給公司或我的帶來巨大的損失。 這時一款可以及時上報錯誤和可以幫助程序員很好的解決錯誤的前端錯誤監控系統就必不可少了。 接下來咱們就聊聊常見的錯誤發生與處理。javascript

本文主要圍繞如下幾點討論:html

  1. 常見JS錯誤類型
  2. 常見JS處理錯誤方式
  3. 上報的方式,和上報內容的幾點思考

問題:前端

  1. JS、CSS、img等資源加載失敗(CDN或圖牀掛了,無心刪了、文件名變了)怎麼實時獲知?而不是用戶告訴你?
  2. 如何上報有用的錯誤信息可以讓程序員快速定位錯誤並修復?而不是上報一些迷惑信息?
  3. 在當今無不用壓縮醜化代碼的工程化中,怎麼利用好 SourceMap 文件,處理錯誤信息?
  4. 如何出了問題,不用在讓用戶幫助你復現?要機型?要操做步驟?
  5. 如何更好統計問題的分佈(機型設備、瀏覽器、地理位置、帶寬等),自主根據數據來取捨兼容傾向性?
  6. ...

常見錯誤

  1. 腳本錯誤
    • 語法錯誤
    • 運行時錯誤
      • 同步錯誤
      • 異步錯誤
      • Promise 錯誤
  2. 網絡錯誤
    • 資源加載錯誤
    • 自定義請求錯誤

能夠閱讀監控類庫源碼 errorWatch 來加深理解,也能夠直接用於項目。java

語法錯誤

例如,英文字符寫成中文字符。通常容易在開發時被發現。git

syntaxError

語法錯誤沒法被try catch 處理程序員

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

同步錯誤

JS引擎在執行腳本時,把任務分塊壓入事件棧,輪詢取出執行,每一個事件任務都有本身的上下文環境, 在當前上下文環境同步執行的代碼發生錯誤都能被try catch 捕獲,保證後續的同步代碼被執行。github

try {
  error
} catch(e) {
  console.log(e);
}
複製代碼

異步錯誤

常見的 setTimeout 等方法會建立新的事件任務插入事件棧中,待後續執行。 因此try catch 沒法捕獲其餘上下文的代碼錯誤。web

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

爲了便於分析發生的錯誤,通常利用 window.onerror 事件來監聽錯誤的發生。 它比try catch的捕獲錯誤信息的能力要強大。ajax

/** * @param {String} msg 錯誤描述 * @param {String} url 報錯文件 * @param {Number} row 行號 * @param {Number} col 列號 * @param {Object} error 錯誤Error對象 */
 window.onerror = function (msg, url, row, col, error) {
  console.log('我知道錯誤了');
  // return true; // 返回 true 的時候,異常不會向上拋出,控制檯不會輸出錯誤
};
複製代碼

windowOnerror

  • window.onerror 注意事項
  1. window.onerror 能夠捕獲常見語法、同步、異步錯誤等錯誤;
  2. window.onerror 沒法捕獲 Promise 錯誤、網絡錯誤;
  3. window.onerror 應該在全部JS腳本以前被執行,以避免遺漏;
  4. window.onerror 容易被覆蓋,在處理回調時應該考慮,被人也在使用該事件監聽。

網絡錯誤

因爲網絡請求異常不會冒泡,應此須要在事件捕獲階段才能獲取到。 咱們能夠利用 window.addEventListener。好比代碼、圖片等重要 CDN 資源掛了,能及時得到反饋是極爲重要的。chrome

window.addEventListener('error', (error) => {
  console.log('404 錯誤');
  console.log(error);
  // return true; // 中斷事件傳播
}, true);
複製代碼

addEventListener

對於這類資源加載錯誤,在事件對象中能得到足夠的信息,配合短信、釘釘等第一時間通知開發者。

window.addEventListener('error', (e) => {
  if (e.target !== window) { // 避免重複上報
    console.log({
    	url: window.location.href, // 引用資源地址
    	srcUrl: e.target.src, // 資源加載出錯地址
    })
  }
}, true);
複製代碼
  • window.onerrorwindow.addEventListener

window.addEventListener 的好處,不怕回調被覆蓋,能夠監聽多個回調函數,但記得銷燬避免內存泄漏與錯誤。 但沒法獲取 window.onerror 那麼豐富的信息。通常只用window.addEventListener 來監控資源加載錯誤。

  • 對於網絡請求自定義錯誤,最好是手動上報。

Promise 錯誤

若是你在使用 promise 時未 catch 的話,那麼 onerror 也無能爲力了。

Promise.reject('promise error');
new Promise((resolve, reject) => {
  reject('promise error');
});
new Promise((resolve) => {
  resolve();
}).then(() => {
  throw 'promise error';
});
複製代碼

一樣你能夠利用 window.onunhandledrejectionwindow.addEventListener("unhandledrejection")來監控錯誤。 接收一個PromiseError對象,能夠解析錯誤對象中的 reason 屬性,有點相似 stack

具體兼容處理在 TraceKit.js 能夠看到。

上報方式

  1. img 上報
  2. ajax 上報
function report(errInfo) {
  new Image().src = 'http://your-api-website?data=' + errInfo;
}
複製代碼

ajax 應使用的類庫而已,大同小異。

  • 注意:img 請求有長度限制,數據太大最好仍是用 ajax.post

補充

Script error

引用不一樣域名的腳本,若是沒有特殊處理,報錯誤了,通常瀏覽器處於安全考慮,不顯示具體錯誤而是 Script error. 例如他人別有用心引用你的線上非開源業務代碼,你的腳本報錯信息固然不想讓他知道了。

若是解決自有腳本的跨域報錯問題?

  • 全部資源切換到統一域名,可是這樣就失去了 CDN 的優點。
  • 在腳本文件的 HTTP response header 中設置 CORS
  1. Access-Control-Allow-Origin: You-allow-origin
  2. script 標籤中添加 crossorigin 屬性,例如 <script src="http://www.xxx.com/index.js" crossorigin></script>

響應頭和crossorigin取值問題

  1. crossorigin="anonymous"(默認),CORS 不等於 You-allow-origin,不能帶 cookie
  2. crossorigin="use-credentials"Access-Control-Allow-Credentials: true ,CORS 不能設置爲 *,能帶 cookie。 若是 CORS 不等於 You-allow-origin,瀏覽器不加載 js。

當你對自由能掌握的資源作好了 cors 時,Script error 基本能夠過濾掉,不上報。

講了這麼多,還有一個很是重要的主題,如何分析我能捕獲的錯誤信息?

JavaScript 錯誤剖析

一個 JavaScript 錯誤一般由一下錯誤組成

  • 錯誤信息(error message)
  • 追溯棧(stack trace)

error

consoleError

開發者能夠經過不一樣方式來拋出一個JavaScript 錯誤:

  • throw new Error('Problem description.')
  • throw Error('Problem description.') <-- equivalent to the first one
  • throw 'Problem description.' <-- bad
  • throw null <-- even worse

推薦使用第二種,第三四種瀏覽器沒法就以上兩種方式生成追溯棧。

若是能解析每行追溯棧中的錯誤信息,行列在配合 SourceMap 不就能定位到每行具體源代碼了嗎。 問題在於不一樣瀏覽器在以上信息給出中,並無一個通用標準的格式。難點就在於解決兼容性問題。

例如 window.onerror 第五個參數 error 對象是2013年加入到 WHATWG 規範中的。 早期Safari 和 IE10尚未,Firefox是從14版本加入Error對象的,chrome 也是 2013 年才新加的。

推薦作法

  1. window.onerror是捕獲JS 錯誤最好的方法,當有一個合法的Error對象和追溯棧時才上報。 也能夠避免一些沒法干擾的錯誤,例如插件錯誤和跨域等一些信息不全的錯誤。

  2. try catch 加強,拋出的錯誤信息較全,能夠彌補 window.onerror 的不足。但就像先前說過的, try catch 沒法捕獲異步錯誤和promise錯誤,也不利用 V8 引擎性能優化。

例如騰訊的 BadJS,對如下推薦進行了try catch包裹

  • setTimeout 和 setInterval
  • 事件綁定
  • ajax callback
  • define 和 require
  • 業務主入口

具體是否須要作到如此細粒度的包裹,仍是視狀況而定。

SourceMap

例若有如下錯誤追溯棧(stack trace)

ReferenceError: thisIsAbug is not defined
    at Object.makeError (http://localhost:7001/public/js/traceKit.min.js:1:9435)
    at http://localhost:7001/public/demo.html:28:12
複製代碼

可以解析成一下格式

[
	{
	  "args" : [],
	  "url" : "http://localhost:7001/public/js/traceKit.min.js",
	  "func" : "Object.makeError",
	  "line" : 1,
	  "column" : 9435,
	  "context" : null
	}, 
	{
	  "args" : [],
	  "url" : "http://localhost:7001/public/demo.html",
	  "func" : "?",
	  "line" : 28,
	  "column" : 12,
	  "context" : null
	}
]
複製代碼

在有了行列和對應的 SourceMap 文件就能解析獲取源代碼信息了。

sourceMapDel

解析結果

sourceMapDel

處理代碼以下:

import { SourceMapConsumer } from 'source-map';

// 必須初始化
SourceMapConsumer.initialize({
  'lib/mappings.wasm': 'https://unpkg.com/source-map@0.7.3/lib/mappings.wasm',
});

/** * 根據sourceMap文件解析源代碼 * @param {String} rawSourceMap sourceMap文件 * @param {Number} line 壓縮代碼報錯行 * @param {Number} column 壓縮代碼報錯列 * @param {Number} offset 設置返回臨近行數 * @returns {Promise<{context: string, originLine: number | null, source: string | null}>} * context:源碼錯誤行和上下附近的 offset 行,originLine:源碼報錯行,source:源碼文件名 */
export const sourceMapDeal = async (rawSourceMap, line, column, offset) => {
  // 經過sourceMap庫轉換爲sourceMapConsumer對象
  const consumer = await new SourceMapConsumer(rawSourceMap);
  // 傳入要查找的行列數,查找到壓縮前的源文件及行列數
  const sm = consumer.originalPositionFor({
    line, // 壓縮後的行數
    column, // 壓縮後的列數
  });
  // 壓縮前的全部源文件列表
  const { sources } = consumer;
  // 根據查到的source,到源文件列表中查找索引位置
  const smIndex = sources.indexOf(sm.source);
  // 到源碼列表中查到源代碼
  const smContent = consumer.sourcesContent[smIndex];
  // 將源代碼串按"行結束標記"拆分爲數組形式
  const rawLines = smContent.split(/\r?\n/g);
  let begin = sm.line - offset;
  const end = sm.line + offset + 1;
  begin = begin < 0 ? 0 : begin;
  const context = rawLines.slice(begin, end).join('\n');
  // 記得銷燬
  consumer.destroy();
  return {
    context,
    originLine: sm.line + 1, // line 是從 0 開始數,因此 +1
    source: sm.source,
  }
};
複製代碼

你們根據 SourceMap 文件的格式,就能很好的理解這段代碼了。

參考網站

  1. mozilla/source-map
  2. 前端代碼異常監控實戰
  3. 前端異常監控 - BadJS
  4. 腳本錯誤量極致優化-讓腳本錯誤一目瞭然
相關文章
相關標籤/搜索