【javascript】前端監控學習筆記(二)

前言

  • 上一篇主要說了腳本錯誤捕獲,資源加載錯誤捕獲,和promise捕獲。本篇記錄下接口異常捕獲、白屏監測、加載時間、性能指標,卡頓指標,pv。

接口異常捕獲

原理

  • 重寫xmlhttprequest的open和send方法,使其在上報前進行標記和計時,監聽其load、error、about事件,當發生相應的事件進行上報。
  • 注意!open事件中進行標記xhr需要排除上報url,否則會發生無限循環。
  • axios同理,可以進行自行封裝。排除上報地址後,對應的計時監測和上報。
export default function injectXHR() {
  let xhr = window.XMLHttpRequest;
  let open = xhr.prototype.open;
  xhr.prototype.open = function (method, url, async, user, password) {
    if (!url.match(/logstores/) && !url.match(/sockjs/)) {
      //排除上報地址,其他地址加個屬性,改寫open和send
      this.logData = { method, url, async, user, password };
    }
    return open.apply(this, arguments);
  };
  let send = xhr.prototype.send;
  xhr.prototype.send = function (body) {
    if (this.logData) {
      let startTime = Date.now();
      let handler = (type) => (event) => {
        let duration = Date.now() - startTime;
        let status = this.status;
        let statusText = this.statusText;
        tracker.send({
          kind: "stability",
          type: "xhr",
          eventType: type,
          pathname: this.logData.url,
          status: status + "-" + statusText,
          duration,
          response: this.response ? JSON.stringify(this.response) : "",
          params: body || "",
        });
      };
      this.addEventListener("load", handler("load"), false);
      this.addEventListener("error", handler("error"), false);
      this.addEventListener("abort", handler("abort"), false);
    }
    return send.apply(this, arguments);
  };
}

白屏監測

原理

  • 這個白屏需要和首屏渲染那些指標區分出來,這個是頁面異常了白屏的反饋。
  • 通過document.elementsFromPoint,進行取點,這個取點一般情況是根據頁面設計搞得,爲了方便也可以橫着豎着取個十字。然後可以獲取到裏面的元素,因爲這個能獲取最裏面的元素,所以就通過這個進行判斷,如果排除html,body等容器標籤,仍有標籤存在,那就不是空白點,否則是空白點,根據業務需要設定空白點比值,大於這個值就是白屏。
export default function blankscreen() {
  let wrapperElements = ["html", "body", "#container", ".content"]; //需要排除的容器
  let emptyPoints = 0;
  function getSelector(element) {
    if (element.id) {
      return "#" + id;
    } else if (element.className) {
      return (
        "." +
        element.className
          .split(" ")
          .filter((i) => !!i)
          .join(".")
      );
    } else {
      return element.nodeName.toLowerCase();
    }
  }
  function iswrapper(element) {
    let selector = getSelector(element);
    if (wrapperElements.indexOf(selector) != -1) {
      emptyPoints++;
    }
  }
  onload(function () {
    for (let i = 1; i <= 9; i++) {
      let xele = document.elementsFromPoint(
        (window.innerWidth * i) / 10,
        window.innerHeight / 2
      );
      let yele = document.elementsFromPoint(
        window.innerWidth / 2,
        (window.innerHeight * i) / 10
      );
      iswrapper(xele[0]);
      iswrapper(yele[0]);
    }
    if (emptyPoints >= 10) {
      //看空白點個數,需要修改
      let centerElement = document.elementsFromPoint(
        window.innerWidth / 2,
        window.innerHeight / 2
      );
      tracker.send({
        kind: "stability",
        type: "blank",
        emptyPoints,
        screen: window.screen.width + "X" + window.screen.height,
        viewPoint: window.innerWidth + "X" + window.innerHeight,
        selector: getSelector(centerElement[0]),
      });
    }
  });
}

加載時間

原理

  • 主要利用瀏覽器的api performance.timing製作上報即可。
字段 含義
navigationStart 初始化頁面,在同一個瀏覽器上下文中前一個頁面unload的時間戳,如果沒有前一個頁面的unload,則與fetchStart值相等
redirectStart 第一個HTTP重定向發生的時間,有跳轉且是同域的重定向,否則爲0
redirectEnd 最後一個重定向完成時的時間,否則爲0
fetchStart 瀏覽器準備好使用http請求獲取文檔的時間,這發生在檢查緩存之前
domainLookupStart DNS域名開始查詢的時間,如果有本地的緩存或keep-alive則時間爲0
domainLookupEnd DNS域名結束查詢的時間
connectStart TCP開始建立連接的時間,如果是持久連接,則與fetchStart值相等
secureConnectionStart https 連接開始的時間,如果不是安全連接則爲0
connectEnd TCP完成握手的時間,如果是持久連接則與fetchStart值相等
requestStart HTTP請求讀取真實文檔開始的時間,包括從本地緩存讀取
requestEnd HTTP請求讀取真實文檔結束的時間,包括從本地緩存讀取
responseStart 返回瀏覽器從服務器收到(或從本地緩存讀取)第一個字節時的Unix毫秒時間戳
responseEnd 返回瀏覽器從服務器收到(或從本地緩存讀取,或從本地資源讀取)最後一個字節時的Unix毫秒時間戳
unloadEventStart 前一個頁面的unload的時間戳 如果沒有則爲0
unloadEventEnd 與unloadEventStart相對應,返回的是unload函數執行完成的時間戳
domLoading 返回當前網頁DOM結構開始解析時的時間戳,此時document.readyState變成loading,並將拋出readyStateChange事件
domInteractive 返回當前網頁DOM結構結束解析、開始加載內嵌資源時時間戳,document.readyState 變成interactive,並將拋出readyStateChange事件(注意只是DOM樹解析完成,這時候並沒有開始加載網頁內的資源)
domContentLoadedEventStart 網頁domContentLoaded事件發生的時間
domContentLoadedEventEnd 網頁domContentLoaded事件腳本執行完畢的時間,domReady的時間
domComplete DOM樹解析完成,且資源也準備就緒的時間,document.readyState變成complete.並將拋出readystatechange事件
loadEventStart load 事件發送給文檔,也即load回調函數開始執行的時間
loadEventEnd load回調函數執行完成的時間
階段名 描述 計算方式 意義
unload 前一個頁面卸載耗時 unloadEventEnd – unloadEventStart-redirect 重定向耗時redirectEnd– redirectStart 重定向的時間
appCache 緩存耗時 domainLookupStart – fetchStart 讀取緩存的時間
dns DNS 解析耗時 domainLookupEnd – domainLookupStart 可觀察域名解析服務是否正常
tcp TCP 連接耗時 connectEnd – connectStart 建立連接的耗時
ssl SSL 安全連接耗時 connectEnd – secureConnectionStart 反映數據安全連接建立耗時
ttfb Time to First Byte(TTFB)網絡請求耗時 responseStart – requestStart TTFB是發出頁面請求到接收到應答數據第一個字節所花費的毫秒數
response 響應數據傳輸耗時 responseEnd – responseStart 觀察網絡是否正常
dom DOM解析耗時 domInteractive – responseEnd 觀察DOM結構是否合理,是否有JS阻塞頁面解析
dcl DOMContentLoaded 事件耗時 domContentLoadedEventEnd – domContentLoadedEventStart 當 HTML 文檔被完全加載和解析完成之後,DOMContentLoaded 事件被觸發,無需等待樣式表、圖像和子框架的完成加載
resources 資源加載耗時 domComplete – domContentLoadedEventEnd 可觀察文檔流是否過大
domReady DOM階段渲染耗時 domContentLoadedEventEnd – fetchStart DOM樹和頁面資源加載完成時間,會觸發domContentLoaded事件
首次渲染耗時 首次渲染耗時 responseEnd-fetchStart 加載文檔到看到第一幀非空圖像的時間,也叫白屏時間
首次可交互時間 首次可交互時間 domInteractive-fetchStart DOM樹解析完成時間,此時document.readyState爲interactive
首包時間耗時 首包時間 responseStart-domainLookupStart DNS解析到響應返回給瀏覽器第一個字節的時間
頁面完全加載時間 頁面完全加載時間 loadEventStart - fetchStart -onLoad
onLoad 事件耗時 loadEventEnd – loadEventStart
export default function timing() {
  onload(function () {
    setTimeout(() => {
      const {
        fetchStart,
        connectStart,
        connectEnd,
        requestStart,
        responseStart,
        responseEnd,
        domLoading,
        domInteractive,
        domContentLoadedEventStart,
        domContentLoadedEventEnd,
        loadEventStart,
      } = performance.timing;
      tracker.send({
        kind: "experience",
        type: "timing",
        connectTime: connectEnd - connectStart, //連接時間
        ttfbTime: responseStart - requestStart, //首字節到達時間
        responseTime: responseEnd - responseStart, //響應讀取時間
        parseDomTime: loadEventStart - domLoading, //dom解析時間
        //dom完成時間
        domContentLoadedTime:
          domContentLoadedEventEnd - domContentLoadedEventStart,
        timeToInteractive: domInteractive - fetchStart, //首次可交互時間
        loadTime: loadEventStart - fetchStart, //完整加載時間
      });
    }, 3000);
  });
}

性能指標

原理

  • 主要利用瀏覽器api performanceObserver。
<html>
<head>
  <script>
    var observer = new PerformanceObserver(list => {
      list.getEntries().forEach(entry => {
        // Display each reported measurement on console
        if (console) {
          console.log("Name: "       + entry.name      +
                      ", Type: "     + entry.entryType +
                      ", Start: "    + entry.startTime +
                      ", Duration: " + entry.duration  + "\n");
        }
      })
    });
    observer.observe({entryTypes: ['resource', 'mark', 'measure']});
    performance.mark('registered-observer');
    function clicked(elem) {
      performance.measure('button clicked');
    }
  </script>
</head>
<body>
  <button onclick="clicked(this)">Measure</button>
</body>
</html>
  • 這個api可以很方便的對某些特定元素進行計算時間。比如你的某個dom是異步加載上來的,那麼只要在這個dom上添加屬性elementtiming,值隨意,那麼就能計算這個元素渲染到頁面的FMP。
<img... elementtiming='foobar'/>
<p elementtiming='yehuozhili'>This is text I care about.</p>
...
<script>
const observer = new PerformanceObserver((list) => {
  let perfEntries = list.getEntries();
  // Process the entries by iterating over them.
});
observer.observe({type: 'element', buffered: true});
</script>
  • 下面這個表的字段可以在chrome裏看見,點擊錄製之後刷新網站在性能裏。
字段 描述 備註
FP First Paint(首次繪製) 包括了任何用戶自定義的背景繪製,它是首先將像素繪製到屏幕的時刻
FCP First Content Paint(首次內容繪製) 是瀏覽器將第一個 DOM 渲染到屏幕的時間,可能是文本、圖像、SVG等,這其實就是白屏時間
FMP First Meaningful Paint(首次有意義繪製) 頁面有意義的內容渲染的時間
LCP (Largest Contentful Paint)(最大內容渲染) 代表在viewport中最大的頁面元素加載的時間
DCL (DomContentLoaded)(DOM加載完成) 當 HTML 文檔被完全加載和解析完成之後,DOMContentLoaded 事件被觸發,無需等待樣式表、圖像和子框架的完成加載
L (onLoad) 當依賴的資源全部加載完畢之後纔會觸發
TTI (Time to Interactive) 可交互時間 用於標記應用已進行視覺渲染並能可靠響應用戶輸入的時間點
FID First Input Delay(首次輸入延遲) 用戶首次和頁面交互(單擊鏈接,點擊按鈕等)到頁面響應交互的時間
  • 這裏面第一次交互時間就是first-input,它並不是input輸入框,而是用戶的第一次交互,比如第一次點擊頁面之類的那一下就會產生。
  if (window.PerformanceObserver) {
    let FMP, LCP;
    let p1 = new Promise((res) => {
      new PerformanceObserver((entryList, observer) => {
        let perf = entryList.getEntries();
        FMP = perf[0];
        observer.disconnect();
        res(FMP);
      }).observe({ entryTypes: ["element"] });
    });
    let p2 = new Promise((res) => {
      new PerformanceObserver((entryList, observer) => {
        let perf = entryList.getEntries();
        LCP = perf[0];
        observer.disconnect();
        res(LCP);
      }).observe({ entryTypes: ["largest-contentful-paint"] });
    });
    new Promise((res) => {
      new PerformanceObserver((entryList, observer) => {
        let lastevnet = getLastEvent();
        let perf = entryList.getEntries()[0];
        if (perf) {
          let inputDelay = perf.processingStart - perf.startTime; //這個是輸入延遲
          let duration = perf.duration; //處理耗時
          if (inputDelay > 0 || duration > 0) {
            tracker.send({
              kind: "experience",
              type: "fistInputDelay",
              inputDelay,
              duration,
              startTime: perf.startTime,
              selector: lastevnet
                ? getSelector(lastevnet.path || lastevnet.target)
                : "",
            });
          }
        }
        observer.disconnect();
        res(perf);
      }).observe({ type: "first-input", buffered: true }); //第一次交互
    });

    Promise.all([p1, p2]).then(() => {
      let FP = performance.getEntriesByName("first-paint")[0];
      let FCP = performance.getEntriesByName("first-contentful-paint")[0];
      tracker.send({
        kind: "experience",
        type: "paint",
        firstPaint: FP.startTime,
        firstContentFulPaint: FCP.startTime,
        firstMeaningFulPaint: FMP.startTime,
        largestContentFulPaint: LCP.startTime,
      });
    }); //失敗可以被捕捉
  }

卡頓指標

原理

  • 跟上面差不多,但是需要observe longtask。50ms以上的事件會被longtask給捕捉上,這樣得到這個事件花了多少時間。
   new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        if (entry.duration > 100) {
          let lastEvent = getLastEvent();
          requestIdleCallback(() => {
            tracker.send({
              kind: "experience",
              type: "longTask",
              eventType: lastEvent.type,
              startTime: entry.startTime, // 開始時間
              duration: entry.duration, // 持續時間
              selector: lastEvent
                ? getSelector(lastEvent.path || lastEvent.target)
                : "",
            });
          });
        }
      });
    }).observe({ entryTypes: ["longtask"] });
  • 頁面上故意寫個耗時較多的,然後就可以觸發它了。

PV

原理

  • 這個ip方面可以通過sohu腳本獲取,其他方面有個navigator.connection,可以獲取到網絡環境,往返時間之類。
  • 用戶停留時間就是需要監聽unload事件,然後減去開始時間即可。
  • 卸載頁面會有幾個問題,就是卸載頁面不能異步發送請求,否則發送不到,所以需要同步發送請求。
  • 爲了減少同步發送請求所造成的性能問題使得用戶下一次頁面跳轉過於墨跡,有個api叫Navigator.sendBeacon可以解決這個問題。這api就等於同步發送,只是用戶體驗更好。
export default function pv() {
  var connection = navigator.connection;
  tracker.send({
    kind: "business",
    type: "pv",
    effectiveType: connection.effectiveType, //網絡環境
    rtt: connection.rtt, //往返時間
    screen: `${window.screen.width}x${window.screen.height}`, //設備分辨率
    ip: window.userip,
  });
  let startTime = Date.now();
  window.addEventListener(
    "unload",
    () => {
      let stayTime = Date.now() - startTime;
      tracker.send({
        kind: "business",
        type: "stayTime",
        stayTime,
        ip: window.userip,
      });
    },
    false
  );
}

完整demo代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章