Webfunny知識分享:JS錯誤監控

Webfunny前端監控發表於2020-08-20

現在的前端開發已不再是刀耕火種的年代了,各種框架、編譯工具層出不窮,前端監控系統也不甘其後,遍地開花。

前端正承受著越來越重的職責,前端的業務也變得越來越複雜,此時此刻我們就更需要一套完善的監控系統來為我們的線上應用保駕護航。

但是,想在眾多的監控系統挑出一個趁手的,還真不是一件容易的事。不過徒手撕一個前端監控系統,好像也絕非那麼容易。今天我們就以Webfunny前端監控為基礎,來說一下前端監控最核心的部分,Js錯誤監控。

監控流程:監控並收集錯誤 -> 儲存並上報錯誤 -> 分析並聚合錯誤 -> 傳送錯誤報警-> 定位並解決JS錯誤

一、監控並收集Javascript錯誤

眾所周知,我們是有辦法去監聽前端Js錯誤的,他們分別 window.onerror、window.onunhandledrejection、console.error方法。

通過這些方法能夠為我們記錄下線上的執行時錯誤,以及詳細的堆疊資訊。我將window.onerror(捕獲異常),console.error(自定義異常)方法收集到的錯誤資訊進行分析統計後的效果如下:

(1)重寫 window.onerror 方法

// 重寫 onerror 進行jsError的監聽
window.onerror = function(errorMsg, url, lineNumber, columnNumber, errorObj) {
  var errorStack = errorObj ? errorObj.stack : null;
  siftAndMakeUpMessage("on_error", errorMsg, url, lineNumber, columnNumber, errorStack);
};

window.onerror 方法以及它的引數我就不一一介紹了,我相信大家也已經耳熟能詳了;我們記錄下錯誤發生時的行、列號,以及錯誤堆疊。

(2)重寫 window.onunhandledrejection 方法

window.onunhandledrejection = function(e) {
  var errorMsg = "";
  var errorStack = "";
  if (typeof e.reason === "object") {
    errorMsg = e.reason.message;
    errorStack = e.reason.stack;
  } else {
    errorMsg = e.reason;
    errorStack = "";
  }
  // 分類解析
  siftAndMakeUpMessage("on_error", errorMsg, WEB_LOCATION, 0, 0, "UncaughtInPromiseError: " + errorStack);
}

window.onunhandledrejection 能夠捕獲到Promise未處理的rejection異常,rejection異常並不會阻斷頁面執行,容易被很多小夥伴所遺忘,所以我們監控了此型別的錯誤。

(3)重寫 console.error 方法

// 重寫console.error, 可以捕獲更全面的報錯資訊
var oldError = console.error;
console.error = function (tempErrorMsg) {
  var errorMsg = (arguments[0] && arguments[0].message) || tempErrorMsg;
  var lineNumber = 0;
  var columnNumber = 0;
  var errorObj = arguments[0] && arguments[0].stack;
    if (!errorObj) {
      if (typeof errorMsg == "object") {
        try {
          errorMsg = JSON.stringify(errorMsg)
        } catch(e) {
          errorMsg = "錯誤無法解析"
        }
      }
      siftAndMakeUpMessage("console_error", errorMsg, WEB_LOCATION, lineNumber, columnNumber, "CustomizeError: " + errorMsg);
    } else {
      // 如果報錯中包含錯誤堆疊,可以認為是JS報錯,而非自定義報錯
      siftAndMakeUpMessage("on_error", errorMsg, WEB_LOCATION, lineNumber, columnNumber, errorObj);
    }
    return oldError.apply(console, arguments);
  };

console.error 是用來列印警告日誌,所以我將其歸類為自定義異常。一般像前端框架、引入第三方的外掛都會用 console.error 來列印較為嚴重的警告資訊,而我在工作中也會將後臺丟擲的錯誤資訊(非後臺異常)用console.error列印出來,上報到監控系統裡。這樣對排查異常也是有很大作用的(這一點會在行為記錄查詢中有體現)。

 二、儲存並上報錯誤

Javascript錯誤產生後,應該存入瀏覽器的快取中,然後定時上傳,如果實時上傳,將會對伺服器造成壓力。通過介面將Js錯誤資訊上傳到伺服器,由後臺server對資料進行清洗分類,然後再進行持久化儲存。因為我用的是mysql來儲存日誌資訊,所以需要以JS錯誤為一個model,明確定義Js錯誤的每個欄位,定義如下:

  // 設定日誌物件類的通用屬性
  function setCommonProperty() {
    this.wmVersion = WM_VERSION; // 探針版本號
    this.happenTime = new Date().getTime(); // 日誌發生時間
    this.webMonitorId = WEB_MONITOR_ID;     // 用於區分應用的唯一標識(一個專案對應一個)
    this.simpleUrl =  window.location.href.split('?')[0].replace('#', ''); // 頁面的url
    this.completeUrl =  utils.b64EncodeUnicode(encodeURIComponent(window.location.href)); // 頁面的完整url
    this.customerKey = utils.getCustomerKey(); // 用於區分使用者,所對應唯一的標識,清理本地資料後失效,
    // 使用者自定義資訊, 由開發者主動傳入, 便於對線上問題進行準確定位
    var wmUserInfo = lsg.wmUserInfo ? JSON.parse(lsg.wmUserInfo) : {};
    this.userId = wmUserInfo.userId;
    this.firstUserParam = utils.b64EncodeUnicode(wmUserInfo.firstUserParam || "");
    this.secondUserParam = utils.b64EncodeUnicode(wmUserInfo.secondUserParam || "");
  }
  // JS錯誤日誌,繼承於日誌基類MonitorBaseInfo
  function JavaScriptErrorInfo(uploadType, infoType, errorMsg, errorStack) {
    setCommonProperty.apply(this);
    this.uploadType = uploadType;
    this.infoType = infoType;
    this.pageKey = utils.getPageKey();  // 用於區分頁面,所對應唯一的標識,每個新頁面對應一個值
    this.deviceName = DEVICE_INFO.deviceName;
    this.os = DEVICE_INFO.os + (DEVICE_INFO.osVersion ? " " + DEVICE_INFO.osVersion : "");
    this.browserName = DEVICE_INFO.browserName;
    this.browserVersion = DEVICE_INFO.browserVersion;
    // TODO 位置資訊, 待處理
    this.monitorIp = utils.getCookie("webfunny_ip");  // 使用者的IP地址
    this.country = "china";  // 使用者所在國家
    this.province = "";  // 使用者所在省份
    this.city = "";  // 使用者所在城市
    this.errorMessage = utils.b64EncodeUnicode(errorMsg)
    this.errorStack = utils.b64EncodeUnicode(errorStack);
    this.browserInfo = "";
  }

Js錯誤資訊需要包含系統版本號、應用版本號、平臺資訊、頁面Url、錯誤資訊、錯誤堆疊、發生時間等等,這樣才能幫助我們準確定位,至於資料庫的欄位如何定義,我就不贅述了,可以訪問我的git專案檢視。

 三、分析並聚合錯誤

如果每天都去盯著前端的報錯資料,真的很耗費精力,而且很難看出是今天發生的,還是一直存在的報錯。

其實前端專案每天都會有些報錯,比如:script error 。我們既不能控制,也不會影響我們的業務,但它會一直存在。

只要每天的錯誤量沒有波動太大,報錯資料比較平穩,就可以認為線上應用是健康的。所以我選擇跟一週前的資料進行比較,如果出現大幅上升,那麼就需要對這個專案進行關注了,而不是每天檢視具體的報錯資料。

本文上部的健康狀況看板圖片就是為了表達這種想法,截圖上正是前端發了嚴重的異常,而出現的曲線圖。

那我們來看看如何對這些錯誤進行聚合,且看下Webfunny錯誤聚合的效果:

首先,我們對捕獲的異常型別進行了分類(TypeError、ReferenceError、UncaughtInPromiseError),這樣錯誤型別可以一目瞭然。

同時我們對發生錯誤的作業系統(Android、ios、Pc)進行了分類統計,比如截圖中的第一個錯誤,就只會在蘋果手機上發生,排查範圍也就縮小了很多。

另外,我們把錯誤影響的人數也統計出來,就可以知道這個錯誤影響了多少使用者,從而確定修復的優先順序。

 四、傳送錯誤報警

這一步屬於監控的附加功能,主要包括郵箱、釘釘、簡訊等訊息通知,和本次講得知識點無關,我就不細說了。

 五、如何定位並解決JS錯誤

針對某一個錯誤,我們需要分析它發生的平臺,影響的人數,系統版本,網路環境等等,同時也需要分析最為重要的一步,就是程式碼的位置。

 

如圖所示

首先,我們分析了錯誤發生的具體時間、發生次數、影響人數、IP地址、瀏覽器版本、作業系統等環境因素

其次,我們還需統計這個報錯發生的時間曲線,如果是大量報錯,我們可以很容易定位到錯誤發生的起始點,針對那個時間點,對報錯的原因進行定位

 

還有一個重要的點,就是對程式碼程式碼堆疊的分析。我們預設會根據錯誤提示的行、列號擷取錯誤位置附近的一段程式碼,正常情況下,我們已經可以通過這段程式碼來定位出出錯的具體位置了。

但是有些小夥伴說,我就是看不出來怎麼辦,沒關係,我們還提供了利用Js的SourceMap檔案反向定位原始碼的功能,讓你準確定位到Js原始碼的位置。

PS:由於SourceMap檔案反向定位原始碼的功能較為複雜,我將放到下一個知識分享中進行講解。

最後,我們將Js錯誤結合到使用者的行為記錄中,這樣我們就能夠知道使用者在發生錯誤的前後都做了什麼,更進一步的瞭解錯誤發生原因,錯誤詳情頁提供了檢視行為軌跡的按鈕,我們來看看結果。

 

 

 

好了,說了這麼多方法,對Js錯誤的監控和解決方法已經不再是什麼難事了。

 

相關文章