一步一步搭建前端監控系統:介面請求異常監控篇
摘要: 如何監控HTTP請求錯誤?
- 作者:一步一個腳印一個坑
- 原文:搭建前端監控系統(四)介面請求異常監控篇
Fundebug經授權轉載,版權歸原作者所有。
背景:市面上的監控系統有很多,大多收費,對於小型前端專案來說,必然是痛點。另一點主要原因是,功能雖然通用,卻未必能夠滿足我們自己的需求, 所以我們自給自足也許是個不錯的辦法。
這是搭建前端監控系統的第四章,主要是介紹如何統計靜態資源載入報錯,跟著我一步步做,你也能搭建出一個屬於自己的前端監控系統。
上一章介紹瞭如何統計靜態資源載入報錯,今天要說的是介面請求報錯。可能有人會認為介面的報錯應該由後臺來關注,統計,並修復。 確實如此,而且後臺服務有了很多成熟完善的統計工具,完全能夠應對大部分的異常情況, 那麼為什麼還需要前端對介面請求進行監控呢。原因很簡單,因為前端是bug的第一發現位置,在你幫後臺背鍋之前怎麼快速把過甩出去呢,這時候,我們就需要有一個介面的監控系統,哈哈 :)
- 我們要監控介面報錯的情況,及時定位線上問題產生的原因
- 我們要分析介面的效能,以輔助我們對前端應用的優化。
好了, 進入正題吧:
如何監控前端介面請求呢?
一般前端請求都是用jquery的ajax請求,也有用fetch請求的,以及前端框架自己封裝的請求等等。總之他們封裝的方法各不相同,但是萬變不離其宗,他們都是對瀏覽器的這個物件 window.XMLHttpRequest 進行了封裝,所以我們只要能夠監聽到這個物件的一些事件,就能夠把請求的資訊分離出來。
1. 如何監聽ajax請求
如果你用的jquery、zepto、或者自己封裝的ajax方法,就可以用如下的方法進行監聽。我們監聽 XMLHttpRequest 物件的兩個事件 loadstart, loadend。但是監聽的結果並不是像我們想象的那麼容易理解,我們先看下ajaxLoadStart,ajaxLoadEnd的回撥方法。
/** * 頁面介面請求監控 */ function recordHttpLog() { // 監聽ajax的狀態 function ajaxEventTrigger(event) { var ajaxEvent = new CustomEvent(event, { detail: this }); window.dispatchEvent(ajaxEvent); } var oldXHR = window.XMLHttpRequest; function newXHR() { var realXHR = new oldXHR(); realXHR.addEventListener( "loadstart", function() { ajaxEventTrigger.call(this, "ajaxLoadStart"); }, false ); realXHR.addEventListener( "loadend", function() { ajaxEventTrigger.call(this, "ajaxLoadEnd"); }, false ); // 此處的捕獲的異常會連日誌介面也一起捕獲,如果日誌上報介面異常了,就會導致死迴圈了。 // realXHR.onerror = function () { // siftAndMakeUpMessage("Uncaught FetchError: Failed to ajax", WEB_LOCATION, 0, 0, {}); // } return realXHR; } var timeRecordArray = []; window.XMLHttpRequest = newXHR; window.addEventListener("ajaxLoadStart", function(e) { var tempObj = { timeStamp: new Date().getTime(), event: e }; timeRecordArray.push(tempObj); }); window.addEventListener("ajaxLoadEnd", function() { for (var i = 0; i < timeRecordArray.length; i++) { if (timeRecordArray[i].event.detail.status > 0) { var currentTime = new Date().getTime(); var url = timeRecordArray[i].event.detail.responseURL; var status = timeRecordArray[i].event.detail.status; var statusText = timeRecordArray[i].event.detail.statusText; var loadTime = currentTime - timeRecordArray[i].timeStamp; if (!url || url.indexOf(HTTP_UPLOAD_LOG_API) != -1) return; var httpLogInfoStart = new HttpLogInfo( HTTP_LOG, url, status, statusText, "發起請求", timeRecordArray[i].timeStamp, 0 ); httpLogInfoStart.handleLogInfo(HTTP_LOG, httpLogInfoStart); var httpLogInfoEnd = new HttpLogInfo( HTTP_LOG, url, status, statusText, "請求返回", currentTime, loadTime ); httpLogInfoEnd.handleLogInfo(HTTP_LOG, httpLogInfoEnd); // 當前請求成功後就在陣列中移除掉 timeRecordArray.splice(i, 1); } } }); }
一個頁面上會有很多個請求,當一個頁面發出多個請求的時候,ajaxLoadStart事件被監聽到,但是卻無法區分出來到底傳送的是哪個請求,只返回了一個內容超多的事件物件,而且事件物件的內容幾乎完全一樣。當ajaxLoadEnd事件被監聽到的時候,也會返回一個內容超多的時間物件,這個時候事件物件裡包含了介面請求的所有資訊。幸運的是,兩個物件是同一個引用,也就意味著,ajaxLoadStart和ajaxLoadEnd事件被捕獲的時候,他們作用的是用一個物件。那我們就有辦法分析出來了。
當ajaxLoadStart事件發生的時候,我們將回調方法中的事件物件全都放進陣列timeRecordArray裡,當ajaxLoadEnd發生的時候,我們就去遍歷這個資料,遇到又返回結果的事件物件,說明介面請求已經完成,記錄下來,並從陣列中刪除該事件物件。這樣我們就能夠逐一分析出介面請求的內容了。
如何監聽fetch請求
通過第一種方法,已經能夠監聽到大部分的ajax請求了。然而,使用fetch請求的人越來越多,因為fetch的鏈式呼叫可以讓我們擺脫ajax的巢狀地獄,被更多的人所青睞。奇怪的是,我用第一種方式,卻無法監聽到fetch的請求事件,這是為什麼呢?
return new Promise(function(resolve, reject) {
var request = new Request(input, init);
var xhr = new XMLHttpRequest();
xhr.onload = function() {
var options = {
status: xhr.status,
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders() || "")
};
options.url =
"responseURL" in xhr
? xhr.responseURL
: options.headers.get("X-Request-URL");
var body = "response" in xhr ? xhr.response : xhr.responseText;
resolve(new Response(body, options));
};
// .......
xhr.send(
typeof request._bodyInit === "undefined" ? null : request._bodyInit
);
});
這個是fetch的一段原始碼, 可以看到,它建立了一個Promise, 並新建了一個XMLHttpRequest物件 var xhr =newXMLHttpRequest()。由於fetch的程式碼是內建在瀏覽器中的,它必然先用監控程式碼執行,所以,我們在新增監聽事件的時候,是無法監聽fetch裡邊的XMLHttpRequest物件的。怎麼辦呢,我們需要重寫一下fetch的程式碼。只要在監控程式碼執行之後,我們重寫一下fetch,就可以正常監聽使用fetch方式傳送的請求了。就這麼簡單 :)
看一下需要監聽的欄位:
// 設定日誌物件類的通用屬性
function setCommonProperty() {
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 = localStorage.wmUserInfo
? JSON.parse(localStorage.wmUserInfo)
: "";
this.userId = utils.b64EncodeUnicode(wmUserInfo.userId || "");
this.firstUserParam = utils.b64EncodeUnicode(
wmUserInfo.firstUserParam || ""
);
this.secondUserParam = utils.b64EncodeUnicode(
wmUserInfo.secondUserParam || ""
);
}
// 介面請求日誌,繼承於日誌基類MonitorBaseInfo
function HttpLogInfo(
uploadType,
url,
status,
statusText,
statusResult,
currentTime,
loadTime
) {
setCommonProperty.apply(this);
this.uploadType = uploadType; // 上傳型別
this.httpUrl = utils.b64EncodeUnicode(encodeURIComponent(url)); // 請求地址
this.status = status; // 介面狀態
this.statusText = statusText; // 狀態描述
this.statusResult = statusResult; // 區分發起和返回狀態
this.happenTime = currentTime; // 客戶端傳送時間
this.loadTime = loadTime; // 介面請求耗時
}
所有工作準備完畢,如果把收集到的日誌從不同的維度展現出來,我就不細說了,直接上圖了。如此,便能夠對前端介面報錯的情況有一個清晰的瞭解,也能夠快速的發現線上的問題。
參考
關於Fundebug
Fundebug專注於JavaScript、微信小程式、微信小遊戲、支付寶小程式、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有陽光保險、核桃程式設計、荔枝FM、掌門1對1、微脈、青團社等眾多品牌企業。歡迎