前端监控SDK设计与实现:错误捕获、用户行为与PV/UV统计

作者:橙子

时间:2026-10-31

浏览:215

本文详细解析前端监控SDK的核心实现,涵盖错误监控、用户行为埋点及PV/UV统计三大模块。深入探讨同步、异步、Promise及资源加载错误的捕获机制,对比手动与自动埋点的优劣,并分析history与hash路由下的数据采集方案。最后总结xhr、img标签及sendBeacon等数据上报技术的适用场景与兼容性处理,为构建稳定高效的前端监控系统提供完整技术指南。

文章目录

前端监控SDK的核心功能主要围绕监控内容展开。

  • 首先是错误监控,旨在捕捉浏览器兼容性问题、代码Bug以及后端接口异常等影响用户体验的因素。
  • 其次是行为日志分析,以电商App为例,通过追踪用户停留时间较长的页面及高频点击按钮,分析用户行为以制定个性化引导策略,促进购买转化。
  • 最后是PV/UV统计,用于量化用户访问规模,包括统计页面访问次数及每日独立访客数量。

基于上述三大核心功能,监控SDK的整体设计流程如下:

前端监控SDK的核心功能主要围绕监控内容展开。

数据采集阶段,负责收集前端监控所需的关键数据,涵盖PV/UV指标、用户行为轨迹以及各类报错信息。

日志上报阶段,将采集到的原始数据发送至服务端进行存储与处理。

日志查询阶段,通过后台管理界面查询已采集的数据,支持系统层面的深度分析与可视化展示。

为实现上述功能,SDK需进行功能模块拆分。

前端监控SDK的核心功能主要围绕监控内容展开。

初始化模块是SDK运行的基础。

该模块负责接收用户传递的配置参数,并调用初始化函数。在初始化过程中,可注入各类监听事件,从而启动数据统计功能。

/**
 * 初始化配置
 * @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('--- 语法错误 --')
              }
  • 同步错误类型分析。
    同步错误指在JavaScript同步执行过程中发生的错误,例如变量未定义等情况。此类错误可被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事件中阻止了冒泡,事件将无法传递至全局监听器,因此涉及冒泡行为的事件需配合手动埋点上报,以确保数据覆盖的完整性。

      {/* 自动埋点 */}
      <button 
        style={{ marginRight: 20 }}
        onClick={(e) => {
          e.stopPropagation(); // 阻止事件冒泡
          tracker('submit', '按钮1被点击了'); //手动上报
        }}
      >按钮3</button>
  • 自动埋点的优点在于无需侵入代码即可实现全局埋点上报。
  • 其缺点在于仅能上报基础的行为交互信息,无法支持自定义数据上报。此外,页面中任何点击操作均会上报至服务器,可能导致上报频率过高,增加服务器压力。

PV统计模块用于计算页面浏览量。

PV(Page View)即页面浏览量,表示页面的访问次数。对于非SPA页面,只需监听onload事件即可统计PV;而在SPA应用中,路由切换由前端控制,单页面切换分为hash路由和history路由,两者实现原理不同,因此需针对这两种路由分别实现数据采集方案。

针对history路由的实现机制进行分析。

history路由依赖于全局对象history实现。

  • history.back():用于返回上一页(模拟浏览器回退操作)。
  • history.forward():用于前进一页(模拟浏览器前进操作)。
  • history.go():用于跳转至历史列表中的指定页面。
  • history.pushState():用于向历史栈中添加新记录。
  • history.replaceState():用于修改当前历史记录。

history路由的数据采集主要依赖pushState和replaceState方法,但由于这两个方法无法触发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(Unique Visitor)统计一天内访问网站的用户数量。该统计只需在SDK初始化时上报一条消息即可完成。

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

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

数据上报模块负责将采集到的数据发送至服务端。

  • xhr接口请求方式采用标准的接口请求,类似于其他业务请求,仅传递数据为埋点信息。通常情况下,处理埋点的服务器与处理业务逻辑的服务器并非同一台,因此需手动解决跨域问题。此外,若在上报过程中刷新或重新打开页面,可能导致埋点数据丢失,故传统xhr接口请求方式难以完全适应埋点需求。
  • 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的介绍已结束。如需了解更多前端监控SDK相关内容,请搜索本站历史文章或继续浏览后续相关文章,感谢大家的支持。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章