怎樣定位前端線上問題,一直以來,都是很頭疼的問題,因為它發生於用戶的一系列操作之后。錯誤的原因可能源於機型,網絡環境,接口請求,復雜的操作行為等等,在我們想要去解決的時候很難復現出來,自然也就無法解決。 當然,這些問題並非不能克服,讓我們來一起看看如何去監控並定位線上的問題吧。
背景:市面上的前端監控系統有很多,功能齊全,種類繁多,不管你用或是不用,它都在那里,密密麻麻。往往我需要的功能都在別人家的監控系統里,手動無奈,罷了,怎么才能擁有一個私人定制的前端監控系統呢?做一個自帶前端監控系統的前端工程獅是一種怎樣的體驗呢?
這是搭建前端監控系統的第四章,主要是介紹如何統計靜態資源加載報錯,跟着我一步步做,你也能搭建出一個屬於自己的前端監控系統。
如果感覺有幫助,或者有興趣,請關注 or Star Me 。
============================================================================
============================================================================
上一章介紹了如何統計靜態資源加載報錯,今天要說的是前端接口請求監控的問題。
可能有人會認為接口的報錯應該由后台來關注,統計,並修復。 確實如此,而且后台服務有了很多成熟完善的統計工具,完全能夠應對大部分的異常情況, 那么為什么還需要前端對接口請求進行監控呢。原因很簡單,因為前端是bug的第一發現位置,在你幫后台背鍋之前怎么快速把過甩出去呢,這時候,我們就需要有一個接口的監控系統,哈哈 :)那么,我們需要哪些監控數據才能夠把鍋甩出去呢?
1. 我們要監控所有的接口請求
2. 我們要監控並記錄所有接口請求的返回狀態和返回結果
3. 我們要監控接口的報錯情況,及時定位線上問題產生的原因
4. 我們要分析接口的性能,以輔助我們對前端應用的優化。
好了, 進入正題吧:
如何監控前端接口請求呢
一般前端請求都是用jquery的ajax請求,也有用fetch請求的,以及前端框架自己封裝的請求等等。總之他們封裝的方法各不相同,但是萬變不離其宗,他們都是對瀏覽器的這個對象 window.XMLHttpRequest 進行了封裝,所以我們只要能夠監聽到這個對象的一些事件,就能夠把請求的信息分離出來。
- 如何監聽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; } function handleHttpResult(i, tempResponseText) { if (!timeRecordArray[i] || timeRecordArray[i].uploadFlag === true) { return; } var responseText = ""; try { responseText = tempResponseText ? JSON.stringify(utils.encryptObj(JSON.parse(tempResponseText))) : ""; } catch (e) { responseText = ""; } var simpleUrl = timeRecordArray[i].simpleUrl; 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, simpleUrl, url, status, statusText, "發起請求", "", timeRecordArray[i].timeStamp, 0); httpLogInfoStart.handleLogInfo(HTTP_LOG, httpLogInfoStart); var httpLogInfoEnd = new HttpLogInfo(HTTP_LOG, simpleUrl, url, status, statusText, "請求返回", responseText, currentTime, loadTime); httpLogInfoEnd.handleLogInfo(HTTP_LOG, httpLogInfoEnd); // 當前請求成功后就,就將該對象的uploadFlag設置為true, 代表已經上傳了 timeRecordArray[i].uploadFlag = true; } var timeRecordArray = []; window.XMLHttpRequest = newXHR; window.addEventListener('ajaxLoadStart', function(e) { var tempObj = { timeStamp: new Date().getTime(), event: e, simpleUrl: window.location.href.split('?')[0].replace('#', ''), uploadFlag: false, } timeRecordArray.push(tempObj) }); window.addEventListener('ajaxLoadEnd', function() { for (var i = 0; i < timeRecordArray.length; i ++) { // uploadFlag == true 代表這個請求已經被上傳過了 if (timeRecordArray[i].uploadFlag === true) continue; if (timeRecordArray[i].event.detail.status > 0) { var rType = (timeRecordArray[i].event.detail.responseType + "").toLowerCase() if (rType === "blob") { (function(index) { var reader = new FileReader(); reader.onload = function() { var responseText = reader.result;//內容就在這里 handleHttpResult(index, responseText); } try { reader.readAsText(timeRecordArray[i].event.detail.response, 'utf-8'); } catch (e) { handleHttpResult(index, timeRecordArray[i].event.detail.response + ""); } })(i); } else { var responseText = timeRecordArray[i].event.detail.responseText; handleHttpResult(i, responseText); } } } }); }
一個頁面上會有很多個請求,當一個頁面發出多個請求的時候,ajaxLoadStart事件被監聽到,但是卻無法區分出來到底發送的是哪個請求,只返回了一個內容超多的事件對象,而且事件對象的內容幾乎完全一樣。當ajaxLoadEnd事件被監聽到的時候,也會返回一個內容超多的時間對象,這個時候事件對象里包含了接口請求的所有信息。幸運的是,兩個對象是同一個引用,也就意味着,ajaxLoadStart和ajaxLoadEnd事件被捕獲的時候,他們作用的是用一個對象。那我們就有辦法分析出來了。
當ajaxLoadStart事件發生的時候,我們將回調方法中的事件對象全都放進數組timeRecordArray里,當ajaxLoadEnd發生的時候,我們就去遍歷這個數據,遇到又返回結果的事件對象,說明接口請求已經完成,記錄下來,並從數組中將該事件對象的uploadFlag屬性設置為true, 代表請求已經被記錄。這樣我們就能夠逐一分析出接口請求的內容了。
2.如何監聽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; // 接口請求耗時 }
所有工作准備完畢,如果把收集到的日志從不同的維度展現出來,我就不細說了,直接上圖了。如此,便能夠對前端接口報錯的情況有一個清晰的了解,也能夠快速的發現線上的問題。