錯誤監控原理解析

錯誤監控

原文地址

前言

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

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

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

問題:

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

常見錯誤

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

語法錯誤

例如,英文字符寫成中文字符。一般容易在開發時被發現。

syntaxError

語法錯誤無法被try catch 處理

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

同步錯誤

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

try {
  error
} catch(e) {
  console.log(e);
}

異步錯誤

常見的 setTimeout 等方法會創建新的事件任務插入事件棧中,待後續執行。
所以try catch 無法捕獲其他上下文的代碼錯誤。

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

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

/**
 * @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 資源掛了,能及時獲得反饋是極爲重要的。

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/[email protected]/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;
  begin = begin <= 0 ? 1 : begin;
  // 輸出源碼行,因爲數組索引從0開始,故行數需要-1
  const context = rawLines.slice(begin - 1, end).join('\n');
  // 記得銷燬
  consumer.destroy();
  return {
    context,
    originLine: sm.line,
    source: sm.source,
  }
};

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

目前監控系統正在一點點開發當中,做的好用的話,會開源出來。。。

參考網站

  1. mozilla/source-map
  2. 前端代碼異常監控實戰
  3. 前端異常監控 - BadJS
  4. 腳本錯誤量極致優化-讓腳本錯誤一目瞭然
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章