前端 监控

Posted Jay_帅小伙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端 监控相关的知识,希望对你有一定的参考价值。

概览

  • 为什么要做前端监控
  • 前端监控目标
  • 前端监控流程
  • 编写采集脚本
    • 日志系统监控
    • 错误监控
    • 接口异常
    • 白屏监控
    • 加载时间
    • 性能指标
    • 卡顿
    • pv
  • 扩展问题
    1. 性能监控指标
    2. 前端怎么做性能监控
    3. 线上错误监控怎么做
    4. 导致内存泄漏的方法,怎么监控内存泄漏
    5. Node 怎么做性能监控

1. 为什么要做前端监控

  • 更快的发现问题和解决问题
  • 做产品的决策依据
  • 为业务扩展提供了更多可能性
  • 提升前端工程师的技术深度和广度,打造简历亮点

2. 前端监控目标

2.1 稳定性 stability

  • js错误:js执行错误、promise异常
  • 资源错误:js、css资源加载异常
  • 接口错误:ajax、fetch请求接口异常
  • 白屏:页面空白

2.2 用户体验 experience

2.3 业务 business

  • pv:页面浏览量和点击量
  • uv:访问某个站点的不同ip的人数
  • 用户在每一个页面的停留时间

3. 前端监控流程

  1. 前端埋点
  2. 数据上报
  3. 加工汇总
  4. 可视化展示
  5. 监控报警

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GXA5Dhh3-1665996889919)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bd91043540624db19d7bb390ee9752e4~tplv-k3u1fbpfcp-watermark.image?)]

3.1 常见的埋点方案

3.1.1 代码埋点

  • 嵌入代码的形式
  • 优点:精确(任意时刻,数据量全面)
  • 缺点:代码工作量点

3.1.2 可视化埋点

  • 通过可视化交互的手段,代替代码埋点
  • 将业务代码和埋点代码分离,提供一个可视化交互的页面,输入为业务代码,通过这个系统,可以在业务代码中自定义的增加埋点事件等等,最后输出的代码耦合了业务代码和埋点代码
  • 用系统来代替手工插入埋点代码

3.1.3 无痕埋点

  • 前端的任意一个事件被绑定一个标识,所有的事件都被记录下来
  • 通过定期上传记录文件,配合文件解析,解析出来我们想要的数据,并生成可视化报告供专业人员分析
  • 无痕埋点的优点是采集全量数据,不会出现漏埋和误埋等现象
  • 缺点是给数据传输和服务器增加压力,也无法灵活定制数据结构

4. 编写采集脚本

4.1 接入日志系统

  • 各公司一般都有自己的日志系统,接收数据上报,例如:阿里云

4.2 监控错误

4.2.1 错误分类

  • js错误(js执行错误,promise异常)
  • 资源加载异常:监听error

4.2.2 数据结构分析

1. jsError

    "title": "前端监控系统", // 页面标题
    "url": "http://localhost:8080/", // 页面URL
    "timestamp": "1590815288710", // 访问时间戳
    "userAgent": "Chrome", // 用户浏览器类型
    "kind": "stability", // 大类
    "type": "error", // 小类
    "errorType": "jsError", // 错误类型
    "message": "Uncaught TypeError: Cannot set property 'error' of undefined", // 类型详情
    "filename": "http://localhost:8080/", // 访问的文件名
    "position": "0:0", // 行列信息
    "stack": "btnClick (http://localhost:8080/:20:39)^htmlInputElement.onclick (http://localhost:8080/:14:72)", // 堆栈信息
    "selector": "HTML BODY #container .content INPUT" // 选择器

2. promiseError

    ...
    "errorType": "promiseError",//错误类型 
    "message": "someVar is not defined",//类型详情
    "stack": "http://localhost:8080/:24:29^new Promise (<anonymous>)^btnPromiseClick (http://localhost:8080/:23:13)^HTMLInputElement.onclick (http://localhost:8080/:15:86)",//堆栈信息 
    "selector": "HTML BODY #container .content INPUT"//选择器

3. resourceError
    ...
    "errorType": "resourceError",//错误类型
    "filename": "http://localhost:8080/error.js",//访问的文件名
    "tagName": "SCRIPT",//标签名
    "timeStamp": "76",//时间 

4.2.3 实现

1. 资源加载错误 + js执行错误
//一般JS运行时错误使用window.onerror捕获处理
window.addEventListener(
  "error",
  function (event) 
    let lastEvent = getLastEvent();
    // 有 e.target.src(href) 的认定为资源加载错误
    if (event.target && (event.target.src || event.target.href)) 
      tracker.send(
        //资源加载错误
        kind: "stability", //稳定性指标
        type: "error", //resource
        errorType: "resourceError",
        filename: event.target.src || event.target.href, //加载失败的资源
        tagName: event.target.tagName, //标签名
        timeStamp: formatTime(event.timeStamp), //时间
        selector: getSelector(event.path || event.target), //选择器
      );
     else 
      tracker.send(
        kind: "stability", //稳定性指标
        type: "error", //error
        errorType: "jsError", //jsError
        message: event.message, //报错信息
        filename: event.filename, //报错链接
        position: (event.lineNo || 0) + ":" + (event.columnNo || 0), //行列号
        stack: getLines(event.error.stack), //错误堆栈
        selector: lastEvent
          ? getSelector(lastEvent.path || lastEvent.target)
          : "", //CSS选择器
      );
    
  ,
  true
); // true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以
2. promise异常
//当Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件
window.addEventListener(
  "unhandledrejection",
  function (event) 
    let lastEvent = getLastEvent();
    let message = "";
    let line = 0;
    let column = 0;
    let file = "";
    let stack = "";
    if (typeof event.reason === "string") 
      message = event.reason;
     else if (typeof event.reason === "object") 
      message = event.reason.message;
    
    let reason = event.reason;
    if (typeof reason === "object") 
      if (reason.stack) 
        var matchResult = reason.stack.match(/at\\s+(.+):(\\d+):(\\d+)/);
        if (matchResult) 
          file = matchResult[1];
          line = matchResult[2];
          column = matchResult[3];
        
        stack = getLines(reason.stack);
      
    
    tracker.send(
      //未捕获的promise错误
      kind: "stability", //稳定性指标
      type: "error", //jsError
      errorType: "promiseError", //unhandledrejection
      message: message, //标签名
      filename: file,
      position: line + ":" + column, //行列
      stack,
      selector: lastEvent
        ? getSelector(lastEvent.path || lastEvent.target)
        : "",
    );
  ,
  true
); // true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以

4.3 接口异常采集脚本

4.3.1 数据设计


  "title": "前端监控系统", //标题
  "url": "http://localhost:8080/", //url
  "timestamp": "1590817024490", //timestamp
  "userAgent": "Chrome", //浏览器版本
  "kind": "stability", //大类
  "type": "xhr", //小类
  "eventType": "load", //事件类型
  "pathname": "/success", //路径
  "status": "200-OK", //状态码
  "duration": "7", //持续时间
  "response": "\\"id\\":1", //响应内容
  "params": "" //参数


  "title": "前端监控系统",
  "url": "http://localhost:8080/",
  "timestamp": "1590817025617",
  "userAgent": "Chrome",
  "kind": "stability",
  "type": "xhr",
  "eventType": "load",
  "pathname": "/error",
  "status": "500-Internal Server Error",
  "duration": "7",
  "response": "",
  "params": "name=zhufeng"

4.3.2 实现

使用webpack devServer模拟请求

  • 重写xhr的open、send方法
  • 监听load、error、abort事件
import tracker from "../util/tracker";
export function injectXHR() 
  let XMLHttpRequest = window.XMLHttpRequest;
  let oldOpen = XMLHttpRequest.prototype.open;
  XMLHttpRequest.prototype.open = function (
    method,
    url,
    async,
    username,
    password
  ) 
    // 上报的接口不用处理
    if (!url.match(/logstores/) && !url.match(/sockjs/)) 
      this.logData = 
        method,
        url,
        async,
        username,
        password,
      ;
    
    return oldOpen.apply(this, arguments);
  ;
  let oldSend = XMLHttpRequest.prototype.send;
  let start;
  XMLHttpRequest.prototype.send = function (body) 
    if (this.logData) 
      start = Date.now();
      let handler = (type) => (event) => 
        let duration = Date.now() - start;
        let status = this.status;
        let statusText = this.statusText;
        tracker.send(
          //未捕获的promise错误
          kind: "stability", //稳定性指标
          type: "xhr", //xhr
          eventType: type, //load error abort
          pathname: this.logData.url, //接口的url地址
          status: status + "-" + statusText,
          duration: "" + 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);
    
    oldSend.apply(this, arguments);
  ;

4.4 白屏

  • 白屏就是页面上什么都没有

4.4.1 数据设计


  "title": "前端监控系统",
  "url": "http://localhost:8080/",
  "timestamp": "1590822618759",
  "userAgent": "chrome",
  "kind": "stability", //大类
  "type": "blank", //小类
  "emptyPoints": "0", //空白点
  "screen": "2049x1152", //分辨率
  "viewPoint": "2048x994", //视口
  "selector": "HTML BODY #container" //选择器

4.4.2 实现

  • elementsFromPoint方法可以获取到当前视口内指定坐标处,由里到外排列的所有元素
  • 根据 elementsFromPoint api,获取屏幕水平中线和竖直中线所在的元素
import tracker from "../util/tracker";
import onload from "../util/onload";
function getSelector(element) 
  var selector;
  if (element.id) 
    selector = `#$element.id`;
   else if (element.className && typeof element.className === "string") 
    selector =
      "." +
      element.className
        .split(" ")
        .filter(function (item) 
          return !!item;
        )
        .join(".");
   else 
    selector = element.nodeName.toLowerCase();
  
  return selector;

export function blankScreen() 
  const wrapperSelectors = ["body", "html", "#container", ".content"];
  let emptyPoints = 0;
  function isWrapper(element) 
    let selector = getSelector(element);
    if (wrapperSelectors.indexOf(selector) >= 0) 
      emptyPoints++;
    
  
  onload(function () 
    let xElements, yElements;
    debugger;
    for (let i = 1; i <= 9; i++) 
      xElements = document.elementsFromPoint(
        (window.innerWidth * i) / 10,
        window.innerHeight / 2
      );
      yElements = document.elementsFromPoint(
        window.innerWidth / 2,
        (window.innerHeight * i) / 10
      );
      isWrapper(xElements[0]);
      isWrapper(yElements[0]);
    
    if (emptyPoints >= 0) 
      let centerElements = document.elementsFromPoint(
        window.innerWidth / 2,
        window.innerHeight / 2
      );
      tracker.send(
        kind: "stability",
        type: "blank",
        emptyPoints: "" + emptyPoints,
        screen: window.screen.width + "x" + window.screen.height,
        viewPoint: window.innerWidth + "x" + window.innerHeight,
        selector: getSelector(centerElements[0]),
      );
    
  );

//screen.width  屏幕的宽度   screen.height 屏幕的高度
//window.innerWidth 去除工具条与滚动条的窗口宽度 window.innerHeight 去除工具条与滚动条的窗口高度

4.5 加载时间

4.5.1 阶段含义

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5HB2c44P-1665996889921)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7d30801313694b8bbf1e8977b8d8bd45~tplv-k3u1fbpfcp-watermark.image?)]

字段含义
navigationStart初始化页面,在同一个浏览器上下文中前一个页面unload的时间戳,如果没有前一个页面的unload,则与fetchStart值相等
redirectStart第一个HTTP重定向发生的时间,有跳转且是同域的重定向,否则为0
redirectEnd最后一个重定向完成时的时间,否则为0
fetchStart浏览器准备好使用http请求获取文档的时间,这发生在检查缓存之前
domainLookupStartDNS域名开始查询的时间,如果有本地的缓存或keep-alive则时间为0
domainLookupEndDNS域名结束查询的时间
connectStartTCP开始建立连接的时间,如果是持久连接,则与fetchStart值相等
secureConnectionStarthttps 连接开始的时间,如果不是安全连接则为0
connectEndTCP完成握手的时间,如果是持久连接则与fetchStart值相等
requestStartHTTP请求读取真实文档开始的时间,包括从本地缓存读取
requestEndHTTP请求读取真实文档结束的时间,包括从本地缓存读取
responseStart返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的Unix毫秒时间戳
responseEnd返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时的Unix毫秒时间戳
unloadEventStart前一个页面的unload的时间戳 如果没有则为0
unloadEventEndunloadEventStart相对应,返回的是unload函数执行完成的时间戳
domLoading返回当前网页DOM结构开始解析时的时间戳,此时document.readyState变成loading,并将抛出readyStateChange事件
domInteractive返回当前网页DOM结构结束解析、开始加载内嵌资源时时间戳,document.readyState 变成interactive,并将抛出readyStateChange事件(注意只是DOM树解析完成,这时候并没有开始加载网页内的资源)
domContentLoadedEventStart网页domContentLoaded事件发生的时间
domContentLoadedEventEnd网页domContentLoaded事件脚本执行完毕的时间,domReady的时间
domCompleteDOM树解析完成,且资源也准备就绪的时间,document.readyState变成complete.并将抛出readystatechange事件
loadEventStartload 事件发送给文档,也即load回调函数开始执行的时间
loadEventEndload回调函数执行完成的时间

4.5.2 阶段计算

字段描述计算方式意义
unload前一个页面卸载耗时unloadEventEnd – unloadEventStart-
redirect重定向耗时redirectEnd – redirectStart重定向的时间
appCache缓存耗时domainLookupStart – fetchStart读取缓存的时间
dnsDNS 解析耗时domainLookupEnd – domainLookupStart可观察域名解析服务是否正常
tcpTCP 连接耗时connectEnd – connectStart建立连接的耗时
sslSSL 安全连接耗时connectEnd – secureConnectionStart反映数据安全连接建立耗时
ttfbTime to First Byte(TTFB)网络请求耗时responseStart – requestStartTTFB是发出页面请求到接收到应答数据第一个字节所花费的毫秒数
response响应数据传输耗时responseEnd – responseStart观察网络是否正常
domDOM解析耗时domInteractive – responseEnd观察DOM结构是否合理,是否有JS阻塞页面解析
dclDOMContentLoaded 事件耗时domContentLoadedEventEnd – domContentLoadedEventStart当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载
resources资源加载耗时domComplete – domContentLoadedEventEnd可观察文档流是否过大
domReadyDOM阶段渲染耗时domContentLoadedEventEnd – fetchStartDOM树和页面资源加载完成时间,会触发domContentLoaded事件
首次渲染耗时首次渲染耗时responseEnd-fetchStart加载文档到看到第一帧非空图像

以上是关于前端 监控的主要内容,如果未能解决你的问题,请参考以下文章

前端 监控

前端异常监控解决方案研究

前端最实用全面的工具类方法

Sentry 前端监控系列

2017年第一波JavaScript前端面试题

前端代码异常监控总结

(c)2006-2024 SYSTEM All Rights Reserved IT常识