javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > 前端监控SDK

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

作者:棋丶

本文讨论了前端监控和数据统计的设计思路,包括错误监控、用户行为日志、PV/UV统计等方面,介绍了数据采集、日志上报、日志查询的流程,以及监控错误的类型和用户埋点统计的手段,同时提到了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路由上报
  }
}

错误监控

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

错误类型

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路由的实现主要由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', '加载应用');
}

数据上报

/**
 * 上报
 * @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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

阅读全文