【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代码

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