搭建前端監控系統(四)接口請求監控篇

  怎樣定位前端線上問題,一直以來,都是很頭疼的問題,由於它發生於用戶的一系列操做以後。錯誤的緣由可能源於機型,網絡環境,接口請求,複雜的操做行爲等等,在咱們想要去解決的時候很難復現出來,天然也就沒法解決。 固然,這些問題並不是不能克服,讓咱們來一塊兒看看如何去監控並定位線上的問題吧。 javascript

 

  背景:市面上的前端監控系統有不少,功能齊全,種類繁多,無論你用或是不用,它都在那裏,密密麻麻。每每我須要的功能都在別人家的監控系統裏,手動無奈,罷了,怎麼才能擁有一個私人定製的前端監控系統呢?作一個自帶前端監控系統的前端工程獅是一種怎樣的體驗呢?html

 

  這是搭建前端監控系統的第四章,主要是介紹如何統計靜態資源加載報錯,跟着我一步步作,你也能搭建出一個屬於本身的前端監控系統。前端

  若是感受有幫助,或者有興趣,請關注 or Star Me 。 java

 

  請移步線上: 前端監控系統jquery

 

  上一章介紹瞭如何統計靜態資源加載報錯,今天要說的是前端接口請求監控的問題。git

  可能有人會認爲接口的報錯應該由後臺來關注,統計,並修復。 確實如此,並且後臺服務有了不少成熟完善的統計工具,徹底可以應對大部分的異常狀況, 那麼爲何還須要前端對接口請求進行監控呢。緣由很簡單,由於前端是bug的第一發現位置,在你幫後臺背鍋以前怎麼快速把過甩出去呢,這時候,咱們就須要有一個接口的監控系統,哈哈 :)那麼,咱們須要哪些監控數據纔可以把鍋甩出去呢?github

  1. 咱們要監控全部的接口請求web

  2. 咱們要監控並記錄全部接口請求的返回狀態和返回結果ajax

  3. 咱們要監控接口的報錯狀況,及時定位線上問題產生的緣由數組

  4. 咱們要分析接口的性能,以輔助咱們對前端應用的優化。

好了, 進入正題吧:

 

如何監控前端接口請求呢

  通常前端請求都是用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;
    }
    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; // 接口請求耗時
  }

  全部工做準備完畢,若是把收集到的日誌從不一樣的維度展示出來,我就不細說了,直接上圖了。如此,便可以對前端接口報錯的狀況有一個清晰的瞭解,也可以快速的發現線上的問題。

 下一章:  搭建前端監控系統(五)Nodejs + RabbitMq 搭建消息隊列,處理高併發問題

 上一章:搭建前端監控系統(三)靜態資源加載監控篇

相關文章
相關標籤/搜索