文章目錄
錯誤監控
前言
作爲一個前端,在開發過程即便十分小心,自測充分,在不同用戶複雜的操作下也難免會出現程序員意想不到的問題,給公司或個人帶來巨大的損失。
這時一款能夠及時上報錯誤和能夠幫助程序員很好的解決錯誤的前端錯誤監控系統就必不可少了。
接下來我們就聊聊常見的錯誤發生與處理。
本文主要圍繞以下幾點討論:
- 常見JS錯誤類型
- 常見JS處理錯誤方式
- 上報的方式,和上報內容的幾點思考
問題:
- JS、CSS、img等資源加載失敗(CDN或圖牀掛了,無意刪了、文件名變了)怎麼實時獲知?而不是用戶告訴你?
- 如何上報有用的錯誤信息能夠讓程序員快速定位錯誤並修復?而不是上報一些迷惑信息?
- 在當今無不用壓縮醜化代碼的工程化中,怎麼利用好 SourceMap 文件,處理錯誤信息?
- 如何出了問題,不用在讓用戶幫助你復現?要機型?要操作步驟?
- 如何更好統計問題的分佈(機型設備、瀏覽器、地理位置、帶寬等),自主根據數據來取捨兼容傾向性?
- …
常見錯誤
- 腳本錯誤
- 語法錯誤
- 運行時錯誤
- 同步錯誤
- 異步錯誤
- Promise 錯誤
- 網絡錯誤
- 資源加載錯誤
- 自定義請求錯誤
語法錯誤
例如,英文字符寫成中文字符。一般容易在開發時被發現。
語法錯誤無法被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 的時候,異常不會向上拋出,控制檯不會輸出錯誤
};
window.onerror 注意事項
window.onerror
可以捕獲常見語法、同步、異步錯誤等錯誤;window.onerror
無法捕獲Promise
錯誤、網絡錯誤;window.onerror
應該在所有JS腳本之前被執行,以免遺漏;window.onerror
容易被覆蓋,在處理回調時應該考慮,被人也在使用該事件監聽。
網絡錯誤
由於網絡請求異常不會冒泡,應此需要在事件捕獲階段才能獲取到。
我們可以利用 window.addEventListener
。比如代碼、圖片等重要 CDN
資源掛了,能及時獲得反饋是極爲重要的。
window.addEventListener('error', (error) => {
console.log('404 錯誤');
console.log(error);
// return true; // 中斷事件傳播
}, true);
對於這類資源加載錯誤,在事件對象中能獲得足夠的信息,配合短信、釘釘等第一時間通知開發者。
window.addEventListener('error', (e) => {
if (e.target !== window) { // 避免重複上報
console.log({
url: window.location.href, // 引用資源地址
srcUrl: e.target.src, // 資源加載出錯地址
})
}
}, true);
window.onerror
與 window.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.onunhandledrejection
或 window.addEventListener("unhandledrejection")
來監控錯誤。
接收一個PromiseError對象,可以解析錯誤對象中的 reason
屬性,有點類似 stack
。
具體兼容處理在 TraceKit.js 可以看到。
上報方式
img
上報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
。
Access-Control-Allow-Origin: You-allow-origin
;- script 標籤中添加
crossorigin
屬性,例如<script src="http://www.xxx.com/index.js" crossorigin></script>
響應頭和crossorigin
取值問題
crossorigin="anonymous"
(默認),CORS
不等於You-allow-origin
,不能帶cookie
crossorigin="use-credentials"
且Access-Control-Allow-Credentials: true
,CORS
不能設置爲*
,能帶cookie
。
如果CORS
不等於You-allow-origin
,瀏覽器不加載 js。
當你對自由能掌握的資源做好了 cors
時,Script error
基本可以過濾掉,不上報。
講了這麼多,還有一個非常重要的主題,如何分析我能捕獲的錯誤信息?
JavaScript 錯誤剖析
一個 JavaScript
錯誤通常由一下錯誤組成
- 錯誤信息(error message)
- 追溯棧(stack trace)
開發者可以通過不同方式來拋出一個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 年才新加的。
推薦做法
-
window.onerror
是捕獲JS 錯誤最好的方法,當有一個合法的Error對象和追溯棧時才上報。
也可以避免一些無法干擾的錯誤,例如插件錯誤和跨域等一些信息不全的錯誤。 -
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
文件就能解析獲取源代碼信息了。
解析結果
處理代碼如下:
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
文件的格式,就能很好的理解這段代碼了。
目前監控系統正在一點點開發當中,做的好用的話,會開源出來。。。