前端实现监控SDK的实战指南

 更新时间:2024年10月21日 10:41:11   作者:棋丶  
本文讨论了前端监控和数据统计的设计思路,包括错误监控、用户行为日志、PV/UV统计等方面,介绍了数据采集、日志上报、日志查询的流程,以及监控错误的类型和用户埋点统计的手段,同时提到了PV和UV的统计方法,需要的朋友可以参考下

监控内容

  • 错误监控
    如:浏览器兼容问题、代码bug、后端接口挂掉等问题
  • 行为日志
    如常用的电商app,通过分析用户浏览时间较长页面有哪些、常点击按钮有哪些等行为,通过分析用户的行为定制不同策略引导用户进行购买
  • PV/UV统计
    如:统计用户访问页面次数,每天有多少用户访问系统

 围绕以上三点进行设计,主要流程如下:

数据采集:采集前端监控的相关数据,包括PV/UV、用户行为、报错信息。

日志上报:将采集到的数据发送给服务端。

日志查询:在后台页面中查询采集到的数据,进行系统分析。

功能拆分

初始化

获取用户传递的参数,调用初始化函数,在初始化函数中可以注入一些监听事件来实现数据统计的功能。

/**
 * 初始化配置
 * @param {*} options 配置信息
 */
function init(options) {
  // ------- 加载配置 ----------
  loadConfig(options);
}
/**
 * 加载配置
 * @param {*} options 
 */
export function loadConfig(options) {
  const { 
    appId,  // 系统id
    userId, // 用户id
    reportUrl, // 后端url
    autoTracker, // 自动埋点
    delay, // 延迟和合并上报的功能
    hashPage, // 是否hash录有
    errorReport // 是否开启错误监控
  } = options;

  // --------- appId ----------------
  if (appId) {
    window['_monitor_app_id_'] = appId;
  }

  // --------- userId ----------------
  if (userId) {
    window['_monitor_user_id_'] = userId;
  }

  // --------- 服务端地址 ----------------
  if (reportUrl) {
    window['_monitor_report_url_'] = reportUrl;
  }

  // -------- 合并上报的间隔 ------------
  if (delay) {
    window['_monitor_delay_'] = delay;
  }

  // --------- 是否开启错误监控 ------------
  if (errorReport) {
    errorTrackerReport();
  }

  // --------- 是否开启无痕埋点 ----------
  if (autoTracker) {
    autoTrackerReport();
  }

  // ----------- 路由监听 --------------
  if (hashPage) {
    hashPageTrackerReport(); // hash路由上报
  } else {
    historyPageTrackerReport(); // history路由上报
  }
}

错误监控

前端是直接和用户打交道的,页面报错是特别影响用户体验的,即使在测试充分上线后也会因用户操作行为和操作环境出现各种错误,所以不光是后端需要加报警监控,前端的错误监控也很重要。

错误类型

  • 语法错误
    语法错误一般在开发阶段就可以发现,如拼写错误、符号错误等,语法错误无法被try{}catch{}捕获,因为在开发阶段就能发现,也不会发布到线上。
              try {
                const name = 'wsjyq;
                console.log(name);
              } catch (error) {
                console.log('--- 语法错误 --')
              }
  • 同步错误
    指在js同步执行过程中发生的错误,如变量未定义,可被try-catch捕获
     try {
       const name = 'wsjy';
       console.log(nam);   
       } catch (error) {
       // console.log('--- 同步错误 ---- ')
     }
  • 异步错误
    指在setTimeout等函数中发生的错误,无法被try-catch捕获

    异步错误也可以用Window.onerror捕获处理,比try-catch方便很多

          {/* 异步错误 */}
          <button
            style={{ marginRight: 20 }}
            onClick={() => {
              // 异步错误无法被trycatch捕获
              try {
                setTimeout(() => {
                  let name = 'wsjyq';
                  name.map();
                })
              } catch (error) {
                console.log('--- 异步错误---- ')
              }
            }}
          >异步错误</button>
     // ----- 异步错误捕获 --------
        /** 
         * @param {String} msg   错误描述 
         * @param {String} url   报错文件
         * @param {Number} row   行号
         * @param {Number} col   列号
         * @param {Object} error 错误Error对象
        */
        window.onerror = function (msg, url, row, col, error) {
          console.log('---- 捕获到js执行错误 ----');
          console.log(msg);
          console.log(url);
          console.log(row);
          console.log(col);
          console.log(error);
          return true;
        };
  • Promise错误
    在Promise中使用catch可以捕获到异步错误,但如果没写catch的话在Window.onerror中是捕获不到错误的,或者可以在全局加上unhandledrejection监听没被捕获到的Promise错误。

          {/* promise错误 */}
          <button
            style={{ marginRight: 20 }}
            onClick={() => {
              Promise.reject('promise error').catch(err => {
                console.log('----- promise error -----');
              });
    
              Promise.reject('promise error');
            }}
          >promise错误</button>
        // ------ promise error -----
        window.addEventListener('unhandledrejection', (error) => {
          console.log('---- 捕获到promise error ---')
        }, true);
  • 资源加载错误
    指一些资源文件获取失败,一般用Window.addEventListener来捕获。

          {/* resource错误 */}
          <button 
            style={{ marginRight: 20 }}
            onClick={() => {
              setShow(true);
            }}
          >resource错误</button>
          {
            show && <img src="localhost:8000/images/test.png" /> // 资源不存在
          }
        </div>
     // ------ resource error ----
        window.addEventListener('error', (error) => {
          console.log('---- 捕获到resource error ---')
        }, true);

SDK监控错误就是围绕这几种错误实现的,try-catch用来在可预见情况下监控特定错误 ,Window.onerror主要来捕获预料之外的错误,比如异步错误。但对于Promise错误和网络错误是无法进行捕获的,所以需要用到Window.unhandledrejection监听捕获Promise错误,通过error监听捕获资源加载错误,从而达到各类型错误全覆盖。

用户埋点统计

埋点是监控用户在应用上的一些动作表现,如在淘宝某商品页面上停留了几分钟,就会有一条某用户在某段时间内搜索了某商品并停留了几分钟的记录,后台根据这些记录去分析用户行为,并在指定之后推送或产品迭代优化等,对于产品后续的发展起重要作用。

埋点又分为手动埋点自动埋点

手动埋点

手动在代码中添加相关埋点代码,如用户点击某按钮或者提交一个表单,会在按钮点击事件中和提交事件中添加相关埋点代码。

      {/* 手动埋点 */}
      <button
        onClick={() => {
          tracker('submit', '提交表单');
          tracker('click', '用户点击');
          tracker('visit', '访问新页面');
        }}
      >按钮1</button>

      {/* 属性埋点 */}
      <button  data-target="按钮2被点击了">按钮2</button>
  • 优点:可控性强,可以自定义上报具体数据。
  • 缺点:对业务代码入侵性强,若需要很多地方进行埋点需要一个个进行添加。

自动埋点

自动埋点解决了手动埋点缺点,实现了不用侵入业务代码就能在应用中添加埋点监控的埋点方式。

      {/* 自动埋点 */}
      <button 
        style={{ marginRight: 20 }}
        onClick={(e) => {
          //业务代码
        }}
      >按钮3</button>
/**
 * 自动上报
 */
export function autoTrackerReport() {
  // 自动上报
  document.body.addEventListener('click', function (e) {
    const clickedDom = e.target;

    // 获取标签上的data-target属性的值
    let target = clickedDom?.getAttribute('data-target');

    // 获取标签上的data-no属性的值
    let no = clickedDom?.getAttribute('data-no');
    // 避免重复上报
    if (no) {
      return;
    }

    if (target) {
      lazyReport('action', {
        actionType: 'click',
        data: target
      });
    } else {
      // 获取被点击元素的dom路径
      const path = getPathTo(clickedDom);
      lazyReport('action', {
        actionType: 'click',
        data: path
      });
    }
  }, false);
}

需要注意的是:无痕埋点是通过全局监听click事件的冒泡行为实现的,如果在click事件中阻止了冒泡行为,是不会冒泡到click监听里的,所以,对于加了冒泡行为的click事件需要进行手动埋点上报,从而保证上报全覆盖。

      {/* 自动埋点 */}
      <button 
        style={{ marginRight: 20 }}
        onClick={(e) => {
          e.stopPropagation(); // 阻止事件冒泡
          tracker('submit', '按钮1被点击了'); //手动上报
        }}
      >按钮3</button>
  • 优点:不用入侵代码就可以实现全局埋点上报。
  • 缺点:只能上报基本的行为交互信息,无法上报自定义数据。只要在页面中点击了,就会上报至服务器,导致上报次数会太多,服务器压力大。

PV统计 

PV即页面浏览量,表示页面的访问次数 
非SPA页面只需通过监听onload事件即可统计页面的PV,在SPA页面中,路由的切换主要由前端来实现,而单页面切换又分为hash路由和history路由,两种路由的实现原理不一样,本文针对这两种路由分别实现不同的数据采集方式

 history路由

history路由依赖全局对象history实现的

  • history.back(): 返回上一页 (浏览器回退)
  • history.forward():前进一页 (浏览器前进)
  • history.go():跳转历史中某一页
  • history.pushState():添加新记录
  • history.replaceState():修改当前记录

history路由的实现主要由pushStatereplaceState实现,但这两个方法不能被popstate监听到,所以需要对这两个方法进行重写并进行自定义事件监听来实现数据采集。

import { lazyReport } from './report';

/**
 * history路由监听
 */
export function historyPageTrackerReport() {
  let beforeTime = Date.now(); // 进入页面的时间
  let beforePage = ''; // 上一个页面

  // 获取在某个页面的停留时间
  function getStayTime() {
    let curTime = Date.now();
    let stayTime = curTime - beforeTime;
    beforeTime = curTime;
    return stayTime;
  }

  /**
   * 重写pushState和replaceState方法
   * @param {*} name 
   * @returns 
   */
  const createHistoryEvent = function (name) {
    // 拿到原来的处理方法
    const origin = window.history[name];
    return function(event) {
      // if (name === 'replaceState') {
      //   const { current } = event;
      //   const pathName = location.pathname;
      //   if (current === pathName) {
      //     let res = origin.apply(this, arguments);
      //     return res;
      //   }
      // }

  
      let res = origin.apply(this, arguments);
      let e = new Event(name);
      e.arguments = arguments;
      window.dispatchEvent(e);
      return res;
    };
  };

  // history.pushState
  window.addEventListener('pushState', function () {
    listener()
  });

  // history.replaceState
  window.addEventListener('replaceState', function () {
    listener()
  });

  window.history.pushState = createHistoryEvent('pushState');
  window.history.replaceState = createHistoryEvent('replaceState');

  function listener() {
    const stayTime = getStayTime(); // 停留时间
    const currentPage = window.location.href; // 页面路径
    lazyReport('visit', {
      stayTime,
      page: beforePage,
    })
    beforePage = currentPage;
  }

  // 页面load监听
  window.addEventListener('load', function () {
    // beforePage = location.href;
    listener()
  });

  // unload监听
  window.addEventListener('unload', function () {
    listener()
  });

  // history.go()、history.back()、history.forward() 监听
  window.addEventListener('popstate', function () {
    listener()
  });
}

hash路由

url中的hash值变化会引起hashChange的监听,所以只需在全局添加一个监听函数,在函数中实现数据采集上报即可。但在react和vue中hash路由的跳转是通过pushState实现的,所以还需加上对pushState的监听。

/**
 * hash路由监听
 */
export function hashPageTrackerReport() {
  let beforeTime = Date.now(); // 进入页面的时间
  let beforePage = ''; // 上一个页面

  function getStayTime() {
    let curTime = Date.now();
    let stayTime = curTime - beforeTime; //当前时间 - 进入时间
    beforeTime = curTime;
    return stayTime;
  }

  function listener() {
    const stayTime = getStayTime();
    const currentPage = window.location.href;
    lazyReport('visit', {
      stayTime,
      page: beforePage,
    })
    beforePage = currentPage;
  }

  // hash路由监听
  window.addEventListener('hashchange', function () {
    listener()
  });

  // 页面load监听
  window.addEventListener('load', function () {
    listener()
  });

  const createHistoryEvent = function (name) {
    const origin = window.history[name];
    return function(event) {
      //自定义事件
      // if (name === 'replaceState') {
      //   const { current } = event;
      //   const pathName = location.pathname;
      //   if (current === pathName) {
      //     let res = origin.apply(this, arguments);
      //     return res;
      //   }
      // }
      
      let res = origin.apply(this, arguments);
      let e = new Event(name);
      e.arguments = arguments;
      window.dispatchEvent(e);
      return res;
    };
  };

  window.history.pushState = createHistoryEvent('pushState');

  // history.pushState
  window.addEventListener('pushState', function () {
    listener()
  });
}

UV统计

统计一天内访问网站的用户数
UV统计只需在SDK初始化时上报一条消息即可。

/**
 * 初始化配置
 * @param {*} options 配置信息
 */
function init(options) {
  // ------- 加载配置 ----------

  // -------- uv统计 -----------
  lazyReport('user', '加载应用');
}

数据上报

  • xhr接口请求
    采用接口请求的方式,就像其他业务请求一样,知识传递的数据是埋点的数据。通常情况下,公司里处理埋点的服务器和处理业务逻辑的服务器不是同一台,所以需要手动解决跨域问题。另一方面,如果在上报过程中刷新或者重新打开页面,可能会造成埋点数据的缺失,所以传统xhr接口请求方式并不能很好适应埋点的需求。
  • img标签
    img标签的方式是将埋点数据伪装成图片url的请求方式,避免了跨域问题,但浏览器对url长度会有限制,所以不适合大数据量上报,也会存在刷新或重新打开页面的数据丢失问题。
  • sendBeacon
    这种方式不会出现跨域问题,也不糊存在刷新或重新打开页面的数据丢失问题,缺点是存在兼容性问题。在日常开发中,通常采用sendBeacon上报和img标签上报结合的方式。
/**
 * 上报
 * @param {*} type 
 * @param {*} params 
 */
export function report(data) {
  const url = window['_monitor_report_url_'];

  // ------- fetch方式上报 -------
  // 跨域问题
  // fetch(url, {
  //   method: 'POST',
  //   body: JSON.stringify(data),
  //   headers: {
  //     'Content-Type': 'application/json',
  //   },
  // }).then(res => {
  //   console.log(res);
  // }).catch(err => {
  //   console.error(err);
  // })

  // ------- navigator/img方式上报 -------
  // 不会有跨域问题
  if (navigator.sendBeacon) { // 支持sendBeacon的浏览器
    navigator.sendBeacon(url, JSON.stringify(data));
  } else { // 不支持sendBeacon的浏览器
    let oImage = new Image();
    oImage.src = `${url}?logs=${data}`;
  }
  clearCache();
}

总结 

到此这篇关于前端实现监控SDK的文章就介绍到这了,更多相关前端监控SDK内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • uniapp小程序和h5如何使用three.js详解

    uniapp小程序和h5如何使用three.js详解

    Three.js是通过对WebGL接口的封装与简化而形成的一个易用的图形库,下面这篇文章主要给大家介绍了关于uniapp小程序和h5如何使用three.js的相关资料,需要的朋友可以参考下
    2022-12-12
  • 如何防止回车(enter)键提交表单

    如何防止回车(enter)键提交表单

    这篇文章主要介绍了如何防止回车(enter)键提交表单的方法,需要的朋友可以参考下
    2014-05-05
  • 深入浅析JavaScript中的arguments对象(强力推荐)

    深入浅析JavaScript中的arguments对象(强力推荐)

    这篇文章主要介绍了JavaScript中的arguments对象(强力推荐)的相关资料,非常不错具有参考借鉴价值,需要的朋友可以参考下
    2016-06-06
  • 学习javascript面向对象 理解javascript原型和原型链

    学习javascript面向对象 理解javascript原型和原型链

    这篇文章主要介绍了javascript原型和原型链,学习javascript面向对象,感兴趣的小伙伴们可以参考一下
    2016-01-01
  • javascript关于“时间”的一次探索

    javascript关于“时间”的一次探索

    这篇文章主要介绍了javascript关于“时间”的一次探索,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-07-07
  • js getBoundingClientRect() 来获取页面元素的位置

    js getBoundingClientRect() 来获取页面元素的位置

    该方法已经不再是IE Only了,FF3.0+和Opera9.5+已经支持了该方法,可以说在获得页面元素位置上效率能有很大的提高,在以前版本的Opera和Firefox中必须通过循环来获得元素在页面中的绝对位置。
    2010-11-11
  • js指定日期增加指定月份的实现方法

    js指定日期增加指定月份的实现方法

    这篇文章主要给大家介绍了关于js指定日期增加指定月份的实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2018-12-12
  • JavaScript WeakMap使用详解

    JavaScript WeakMap使用详解

    这篇文章主要介绍了JavaScript WeakMap使用的详细介绍,帮助大家更好的理解和使用JavaScript,感兴趣的朋友可以了解下
    2021-02-02
  • js保留两位小数使用toFixed实现

    js保留两位小数使用toFixed实现

    直接使用Math.round(x*100)存在一个问题,有时会有很小的误差,显示很多位的小数位,如0.9996*100,就会变成99.96000000000001,我想要的在下面,感兴趣的朋友可以参考下哈
    2013-07-07
  • 深入分析javascript中console命令

    深入分析javascript中console命令

    console对象是JavaScript的原生对象,它有点像Unix系统的标准输出stdout和标准错误stderr,可以输出各种信息用来调试程序,而且还提供了很多额外的方法,供开发者调用。它的常见用途有两个。显示网页代码运行时的错误信息。提供了一个命令行接口,用来与网页代码互动。
    2016-08-08

最新评论