前端異常監控平台的設計思路及實現

作者:百度外賣FE 賈碩

JavaScript作為當前web前端主力語言,承擔著非常重要的責任,用戶體驗與其息息相關,但作為一個腳本語言,它有著天然的弱點,弱類型帶來的類型檢查導致無法提前發現異常,特定的用戶環境、特定的設備、特定的網路以及特定的用戶都可能造成JavaScript的異常。

本文主要講述如何實現一個包含異常捕獲、異常上報、異常日誌收集以及異常報表可視化展示的建異常監控平台。

一、如何捕獲異常

1.1 window.error

瀏覽器端提供了用於捕獲異常的方式window.onerror

window.onerror = function(msg, url, row, col, error){n report({n msg, // 錯誤信息n url, // 發生錯誤對應的腳本鏈接n row, // 行號n col // 列號n })n}n

如上所示,一個簡單的上報方案就完成了。但還很不完善,msg提供的信息不夠豐富:Uncaught ReferenceError: c is not defined 這樣的信息還不足以幫助我們追蹤異常。

註: onerror兼容性有問題,ie10 & safari6 才完整支持以上全部參數。

1.2 error.stack

這時候我們發現error.stack可以追蹤異常棧,異常棧有很詳細的信息來告知我們的發生異常的順序以及函數細節:

Uncaught ReferenceError: c is not definedn at f (error.js:23)n at e (error.js:19)n at d (error.js:15)n at error.js:13n

到此為止已經可以上報&追蹤異常了。

1.3 進一步優化

stack message 處理,便於後期展示與解析

const _processStackMsg = error => {n let stack = error.stackn .replace(/n/gi, ) // 去掉換行,節省傳輸內容大小n .replace(/batb/gi, @) // chrome中是at,ff中是@n .split(@) // 以@分割信息n .slice(0, 10) // 最大堆棧長度(Error.stackTraceLimit = 10),所以只取前10條n .map(v => v.replace(/^s*|s*$/g, )) //去除多餘空格n .join(~) // 手動添加分隔符,便於後期展示n .replace(/?[^:]+/gi, ); // 去除js文件鏈接的多餘參數(?x=1之類)n let msg = error.toString();n if (stack.indexOf(msg) < 0) {n stack = msg + ~ + stack;n }n return STACK: + stack;n};n

無stack兼容方案:

部分瀏覽器無stack , PC端:ie10 & safari6 才支持stack,移動端:Android Browser4 && Safari Mobile6 才支持stack。

我們的方案是利用arguments.callee.caller遞歸得出調用堆棧, 不過嚴格模式下不可用。

while ( call.arguments && call.arguments.callee && call.arguments.callee.caller ) {n // 收集函數名n stack.push(function_name(call));nn call = call.arguments.callee.caller;n if (call.caller === call) {n break;n }nn if (deep++ > MAX_DEEP) {n break;n }n}n

支持自定義上報異常(常常與`try...catch`配合):

var typeError = new Error(something error)nn// Report opt must be an error object!nwindow.wmErrorReport && window.wmErrorReport(typeError);nnntry {n console.log(a);n}catch(e){n window.wmErrorReport && window.wmErrorReport(e);n}n

1.4『Script error.』

現代的大中型網站中,靜態資源一般都是CDN部署方式,存放在另外的域名下,與使用該資源的網站不屬於同域。

當引用的非同域JS文件中產生異常以後,會得到一個『Script error.』異常,無法得到更多信息,這屬於瀏覽器做的一個限制。

解決方法:

  1. CDN資源頭設置Access-Control-Allow-Origin: *
  2. <script src="stb0.waimai.baidu.com/x" crossorigin></script>腳本新增crossorigin屬性。

1.5 Ajax Error

很多時候web請求也會造成服務異常,對請求進行監控也是有必要的。

在這裡我們學習了一些框架中的做法,做簡單的hook

XMLHttpRequest.prototype.open = function(){n // do somethingn}nXMLHttpRequest.prototype.send = function(){n // do somethingn registerComplete(this);n}nregisterComplete = xhr => {n xhr.addEventListener(readystatechange,()=>{n // 如果狀態碼為40X則上報異常n if (xhr.status>=400) {n report()n }n })n}n

通過hookXMLHttpRequest的原型方法以及監聽readystatechange方法可以監控資源請求異常狀況,進行上報操作。

1.6 其他捕獲問題

通過以上方式,我們已經可以捕獲大部分JavaScript異常了,不過呢,作為一個特別的語言,js還有很多異常無法通過 window.onerror來直接捕獲到,還有一些工作需要做:

  • setTimeout/setInterval/requestAnimationFrame 異常捕獲
  • 業務入口包裹try…catch 來截獲異常
  • define/require 入口包裹
  • fetch 請求hook

二、如何收集異常

上面說了很多如何去捕獲異常,那麼這些監控代碼怎麼才能運行在被監控的頁面里呢?異常信息是如何發出的呢?如何保證異常日誌收集穩定可靠呢?

2.1 監控客戶端部署

監控客戶端也就是一段js腳本,頁面中嵌入js的方法就兩種,一種是內聯,一種是外鏈。但是,這兩種方案都不能滿足我們下面的需求:

  1. 性能問題:希望監控能盡量減小對頁面性能的影響,載入盡量延後,資源盡量走緩存,沒有緩存走cdn。
  2. 緩存的問題:作為一個線上的產品,一定要做到上下線可控制。新迭代上線一定要低延遲或者0延遲的更新到所有產品線,但是又希望沒有更新的時候盡量走緩存。
  3. 使用的便捷性:每次迭代上線不需要各個業務方上線,統一升級。

如果外鏈就沒辦法同時解決性能和緩存的問題,如果用內聯的方式又沒發解決業務方使用的便捷性。

我們採用了兩者結合的方式,取長補短:

  1. 非同步載入客戶端:監控客戶端是頁面load之後通過內聯代碼非同步載入,避免對頁面載入性能的影響。
  2. loader機制解決緩存問題:所有的客戶端都是通過一個超小體積的loader載入,loader永不緩存,客戶端有更新就更新對應客戶端的地址,沒有更新地址不變,就會走緩存。
  3. 統一升級:通過以上機制,監控的升級也不需要業務方再參與了

細心的同學會發現,如果有異常在load事件之前拋出,是不能捕獲的,所以我們的內聯代碼還有一個功能就是暫存日誌。客戶端載入完成之後會重放一遍。

2.2 異常日誌收集

異常請求的部署完監控客戶端之後就可以進行異常上報了,異常上報是通過載入一張圖片去觸發一個http請求,異常信息序列化之後放在請求的參數中,後端收到請求後產出日誌,然後通過一系列的操作把日誌存儲到資料庫中以供查詢。

最開始我們只是面向新品和b端的一些產品線,數據量級小,所以是通過php產出的pb日誌,整個流程如下圖所示,這種方式日誌產出有一天的延遲,

後來我們為了支持實時報表和更大的量級,後端架構如下:

這個架構可以做到大量級的日誌實時處理,也有很好的擴展性

三、高效的異常報表呈現

3.1 把最嚴重的異常展示在前面

出現次數最多的異常就是對頁面影響最嚴重的,異常列表默認會把相同的異常聚合,然後按照每種異常的出現次數降序展示。

那麼,怎麼定義相同的異常?在同一文件同一行同一列拋出的,並且調用棧相同的異常就是同一個異常。

3.2 直接查看源代碼,快速定位異常

監控客戶端會記錄發生異常時的頁面地址,異常發生的文件,行號,列號,異常發生時的調用棧。這些信息都會在報表裡展示,但是還不夠,通常線上的代碼都是壓縮過的,根據動輒5位數的列號去尋找異常位置是非常困難的,所以我們開發了直接查看源碼的功能

研發不管是用webpack還是fis壓縮代碼都可以產出sourcemap,把產出的sourcemap上傳到我們的平台之後,再點擊調用棧的時候,就可以看到對應調用棧的源代碼,如下圖

我們還開發了sourcemap上傳組件,不需要在瀏覽器端點擊,在編譯完成後執行一個腳本就可以上傳。

3.3 復現異常的環境

每次異常發生時,監控客戶端出了異常的上下文信息之外,也會記錄異常發生時的外部環境包括屏幕解析度、dpr、useragent等,這些信息都會在報表有展示,方便研發復現異常

3.4 變化趨勢展示

總量變化可以按照日周月的維度查看,可以很方便的知道當前產品線的健康度趨勢,如果異常pv增長過大就要考慮是不是最近的上線引入了bug,如果減小了就是異常修復有效果了。

四、我們接下來還要做什麼

性能報表,同時根據性能的pv數據更加智能的產出報警


推薦閱讀:

精讀《null >= 0?》
【Web系列】小說在線閱讀
JS trick之babel-stage語法雜談
極樂技術周報(第十八期)

TAG:前端开发 | 前端工程师 |