前端监控学习-实现简易埋点上报SDK

为什么要有前端监控?

  1. 上线前可能没办法将所有的bug测试出来,没办法将所有问题暴露出来
  2. 监控系统可以帮我们及时识别出风险,进行优化
  3. 可以获取用户行为和跟踪产品使用情况,为后续产品优化提供参考

仓库地址在文末

前端监控需要监控哪些方面?

  • 性能
  • 异常
  • 用户行为

监控系统包含哪些功能?

  1. 错误收集上报--搜集上报端
  2. 聚合过滤--采集聚合端
  3. 错误分析--可视分析端
  4. 异常报警--监控告警端

如何捕获性能指标或异常错误

性能监控

常见性能指标

核心性能指标
FP(First Paint,首次绘制时间)

用于衡量页面从开始加载到浏览器首次将像素渲染到屏幕上的时间点;FP 是浏览器首次将任何内容(如背景色、边框、占位符等)渲染到屏幕上的时间点。即使内容非常简单(例如纯背景色),只要屏幕上不再是空白,就触发FP。有一定局限性,现在更推荐使用FCPLCP 作为核心指标。

FCP(First Contentful Paint,首次内容绘制)

页面开始加载到第一个可见内容(文本、图片、<canvas>等)渲染完成的时间;衡量用户看到内容的"白屏时间"

大致标准

  • 快:≤ 2秒
  • 中等:2 - 4秒
  • 慢:> 4秒

可优化方向:

  • 减少首屏资源加载(如压缩图片、合并CSS/JS)
  • 使用<noscript>或骨架屏(Skeleton)提前展示基础内容
LCP(Largest Contentful paint,最大内容绘制)

页面加载过程中,用户视口内最大内存元素(如:主图、视频、大段文字)的渲染时间。反映用户感知的页面"加载完成"事件

大致标准

  • 快:≤ 2.5秒
  • 中等:2.5 - 4秒
  • 慢:> 4秒

可优化方向:

  • 预加载关键资源(如<link rel="preload">
  • 延迟加载非首屏内容(如懒加载图片)
  • 使用loading="lazy"属性
FID(First Input Delay,首次输出延迟)

用户首次与页面交互(如点击按钮)到浏览器响应之间的延迟时间。衡量页面交互的"响应速度"

大致标注

  • 快:≤ 100ms
  • 中等:100-300ms
  • 慢:>300ms

可优化方向

  • 减少主线程任务(避免长任务阻塞)
  • 使用Web Workers处理复杂计算
  • 减少JavaScript执行时间
TTI(Time to Interactive,可交互时间)

页面从加载开始到能够可靠响应用户输入(如:点击、输入)的时间

条件:FCP之后,持续5秒内无长任务(>50ms)且无多个并发请求

大致标准

  • 快:≤ 3秒
  • 中等:3-4秒
  • 慢:> 4秒

可优化方向

  • 减少阻塞渲染的JavaScript/CSS
  • 按需加载非关键资源
CLS(Cumulactive Layout Shift,累计布局偏移)

页面加载过程,可见元素因异步资源(如:图片、广告)加载导致的意外移动的总和;衡量页面"视觉稳定"

大致标准

  • 快:≤ 0.1
  • 中等:0.1-0.25
  • 慢:> 0.25

可优化方向

  • 为图片/视频设置固定宽高(width/height
  • 预留广告位空间,避免动态内容挤压布局
  • 使用loading="lazy"并指定sizes属性
资源相关指标
资源加载时间

通过Performance API(如resource类型条目)测量资源加载耗时

关键阶段计算

  • DNS查询:domainLookupEnd - domainLookupStart
  • TCP链接:connectEnd - connentStart
  • 请求响应:responseEnd - requestStart

可优化方向

  • 启用 HTTP/2 或 HTTP/3 以减少请求延迟,多路复用
  • 合并静态资源(如CSS/JS打包)
  • 使用CDN加速静态资源
  • 减少HTTP请求(如合并图片为Sprites)
HTTP请求数量

页面加载过程中发起的HTTP请求总数

可优化方向

  • 合并小资源
  • 启用浏览器缓存
TTFB(Time to FIrst Byte,首字节时间)

从发起请求到收到服务器第一个字节的时间

优秀标准: ≤ 200ms

可优化方向:

  • 优化服务器响应速度(使用CDN、减少后端计算)
  • 减少服务器端渲染耗时
用户体验指标
FPS(帧率)

页面动画或滚动时每秒渲染的帧数

可优化方向:

  • 减少复杂计算(如使用WebGL/CSS动画代替JS动画)
  • 避免在requestAnimationFrame中执行耗时任务
长任务(Long Task)

阻塞主线程超过50ms的任务

可优化方向:

  • 分割多个大任务为多个小任务
  • 使用Web Worker 处理后台任务
其他重要指标
内存占用

页面加载前后浏览器内存的使用量

可优化方向:

  • 及时释放不再使用的对象(如null化变量)
  • 减少DOM节点数量(避免复杂嵌套结构)
CPU占用率

页面渲染或交互时CPU的使用率

可优化方向:

  • 减少复杂计算(如优化循环、避免递归)
  • 减少DOM操作频率(批量更新DOM)
DOMContentLoaded 时间

DOM解析完成的时间(domContentLoadedEventEnd),前端脚本可执行的最早时间点。

可优化方向

  • 减少阻塞DOM解析的脚本(将JS放在底部或使用async/defer
页面加载完整时间(Load Event)

所有资源(包含图片、CSS、JS)加载完成的时间(loadEventEnd)

性能指标捕获方式

自定义
Performance API

Performance API 是浏览器提供的一组接口,用于测量和分析网页的性能表现。它可以帮助开发者监控页面加载时间、资源加载效率、用户交互延迟等关键指标,从而优化用户体验

示例: 使用 PerformanceObserver 监听 paint 类型的性能条目

js 复制代码
/**
 * 监控首次内容绘制(FCP)性能指标 observerFCP.js
 * 使用 PerformanceObserver 监听 paint 类型的性能条目
 * 当检测到 first-contentful-paint 条目时收集性能数据并上报
 *
 * 返回值:无
 */
import { lazyReportBatch } from "../report.js";
export default function observerFCP() {
  /**
   * 处理性能观察条目
   * @param {PerformanceObserverEntryList} list - 性能观察条目列表
   */
  const entryHandler = (list) => {
    // 遍历所有性能条目寻找 FCP 指标
    for (const entry of list.getEntries()) {
      if (entry.name === "first-contentful-paint") {
        // 停止观察避免重复上报
        observerr.disconnect();
​
        // 准备上报数据结构
        const json = entry.toJSON();
        const reportData = {
          ...json,
          type: "performance",
          pageUrl: window.location.href,
          subType: entry.name,
          pageUrl: window.location.href,
        };
​
        // 发送数据 todo
        lazyReportBatch(reportData);
      }
    }
  };
​
  /* 创建性能观察者实例,用于监听浏览器性能事件 */
  const observerr = new PerformanceObserver(entryHandler);
​
  /* 开始观察 paint 类型的性能条目,buffered 模式确保能捕获已发生的条目 */
  observerr.observe({ type: "paint", buffered: true });
}
​
​

示例: 监听页面静态资源加载性能数据

js 复制代码
import { lazyReportBatch } from "../report";
​
/**
 * 监听页面静态资源加载性能数据的主函数
 *
 * 功能说明:
 * 1. 在页面完成加载后立即执行性能监控
 * 2. 若页面尚未加载完成,则通过监听load事件延迟执行
 * 3. 最终会调用observerEvent启动性能数据采集
 */
export default function observerEntries() {
  if (document.readyState === "complete") {
    observerEvent();
  } else {
    const onLoad = () => {
      observerEvent();
      window.removeEventListener("load", onLoad, true);
    };
    // 在捕获阶段开启事件监听,避免错过任何资源加载的最终状态
    window.addEventListener("load", onLoad, true);
  }
}
​
/**
 * 初始化性能观察者并收集资源加载数据
 *
 * 功能说明:
 * 1. 创建PerformanceObserver实例监听"resource"类型的性能条目
 * 2. 处理性能数据并格式化成标准上报结构
 * 3. 每次收到新数据后断开旧观察者(可能有潜在问题,需注意)
 */
export function observerEvent() {
  /**
   * 性能条目处理函数
   * @param {PerformanceObserverEntryList} list - 性能条目集合
   */
  const entryHandler = (list) => {
    const data = list.getEntries();
    for (const entry of data) {
      if (observerr) {
        observerr.disconnect();
      }
​
      // 构造包含22个关键指标的性能报告对象
      const reportData = {
        name: entry.name, // 资源完整URL
        type: "performance", // 监控类型标识
        subType: entry.entryType, // 条目类型(resource)
        sourceType: entry.initiatorType, // 资源类型(img/script/css等)
        duration: entry.duration, // 资源加载总耗时
        dns: entry.domainLookupEnd - entry.domainLookupStart, // DNS查询耗时
        tcp: entry.connectEnd - entry.connectStart, // TCP连接耗时
        redirect: entry.redirectEnd - entry.redirectStart, // 重定向耗时
        ttfb: entry.responseStart, // Time to First Byte(首字节时间)
        protocol: entry.nextHopProtocol, // 使用协议(h2/http/1.1等)
        responseBodySize: entry.encodedBodySize, // 压缩后响应体大小
        responseHeaderSize: entry.transferSize - entry.encodedBodySize, // 响应头大小
        transferSize: entry.transferSize, // 网络传输总大小(含头信息)
        resourceSize: entry.decodedBodySize, // 解压后资源实际大小
        startTime: performance.now(), // 数据采集时间戳
      };
      lazyReportBatch(reportData);
    }
  };
​
  // 创建性能观察者实例,监听所有资源类型条目
  let observerr = new PerformanceObserver(entryHandler);
  observerr.observe({ type: ["resource"], buffered: true });
}
​

示例: 重写XMLHttpRequest的open和send方法,用于收集请求性能数据并上报

js 复制代码
/**
 * 重写XMLHttpRequest的open和send方法,用于收集请求性能数据并上报
 */
function overwriteOpenAndSend() {
  /**
   * 重写XMLHttpRequest.prototype.open方法
   * @param {...any} args - 原open方法的参数数组
   *   args[0] {string} method - HTTP请求方法(GET/POST等)
   *   args[1] {string} url - 请求的URL地址
   */
  orginalProto.open = function newOpen(...args) {
    // 存储请求方法和URL到XMLHttpRequest实例上
    this.method = args[0];
    this.url = args[1];
    // 调用原始open方法保持原有功能
    orginalOpen.apply(this, args);
  };
​
  /**
   * 重写XMLHttpRequest.prototype.send方法
   * @param {...any} args - 原send方法的参数数组
   */
  orginalProto.send = function newSend(...args) {
    // 记录请求开始时间
    this.startTime = Date.now();
​
    const onLoaded = () => {
      // 计算请求耗时
      this.endTime = Date.now();
      this.duration = this.startTime - this.endTime;
​
      // 构建性能上报数据对象
      const { url, method, startTime, endTime, duration, status } = this;
      const reportData = {
        type: "performance",
        subType: "xhr",
        status,
        success: status >= 200 && status < 400,
        url,
        method,
        startTime,
        endTime,
        duration,
      };
​
      // 上报性能数据
      lazyReportBatch(reportData);
      // 清理事件监听
      this.removeEventListener("loaded", onLoaded, true);
    };
​
    // 监听请求完成事件
    this.addEventListener("loaded", onLoaded, true);
  };
}
​
/**
 * 初始化XMLHttpRequest监控的入口函数
 */
export default function xhr() {
  overwriteOpenAndSend();
}
​

关于其他性能指标如,就不一一展示了,详细可以看文末的github地址

第三方
web-vitals

web-vitals 是一个由 Google 官方维护的轻量级 JavaScript 库,用于标准化和简化核心 Web Vitals 指标的收集 。它封装了浏览器原生的 Performance API,并提供了统一的接口,方便开发者捕获关键性能指标(如 LCP、FID、CLS 等),并上报到分析工具(如 Google Analytics、Sentry 等)

兼容性:支持现代浏览器(Chrome、Firefox、Safari 等),但部分指标在旧版浏览器可能不兼容

js 复制代码
import { getLCP, getFID, getCLS } from 'web-vitals';
​
// 监听 LCP 指标
getLCP((metric) => {
  console.log('LCP:', metric.name, metric.value, metric.entries);
  // 将数据上报到服务器
  reportMetricToServer(metric);
});
​
// 监听 FID 指标
getFID((metric) => {
  console.log('FID:', metric.name, metric.value);
  reportMetricToServer(metric);
});
​
// 监听 CLS 指标
getCLS((metric) => {
  console.log('CLS:', metric.name, metric.value);
  reportMetricToServer(metric);
});
Sentry

Sentry 是一个开源的实时错误监控与性能分析平台,主要用于捕获、追踪和分析应用程序(Web、移动、后端等)中的异常和性能问题。它通过客户端SDK收集错误信息,并将数据上报到服务端进行聚合、分析和展示,帮助开发者快速定位和修复问题。

这是目前最火的一个监控平台

还有其他如:神策SDK;有需要了解第三方监控平台的再仔细去了解一下,这里主要介绍一些自定义监控Sdk的知识。

行为监控

常见行为

PV(页面浏览量)

用户访问页面的次数(刷新页面也会计数),评估页面流量热度

点击率(CTR)

某个元素被点击的次数 / 其曝光次数,评估按钮、广告等元素的吸引力

平均停留时间

用户在页面上的平均停留时间(离开时间 - 进入时间),评估页面内容质量或用户兴趣

行为捕获方式

示例:页面路由变化监控函数,可计算用户在页面上的平均停留时间

js 复制代码
import { lazyReportBatch } from "../report";
import { generateUniqueId } from "../utils";
​
/**
 * 页面路由变化监控函数
 * 
 * 功能说明:
 * - 监听 hashchange 和 popstate 两种路由变化事件
 * - 收集路由变更信息并通过懒上报机制批量报告
 * - 记录路由变化的起始时间并生成唯一事件ID
 * 
 * 事件类型说明:
 * - hashchange: 监控 hash 路由变化(#锚点变化)
 * - popstate: 监控 history API 路由变化
 * 
 * 上报数据结构:
 * {
 *   type: 'behavior',        // 固定为行为类型
 *   subType: 'hashchange',   // 子类型区分路由类型
 *   from: 'oldUrl',          // 变更前URL
 *   to: 'newUrl',            // 变更后URL
 *   startTime: 时间戳,       // 事件发生时间
 *   uuid: '唯一标识'         // 事件唯一标识
 * }
 */
export default function pageChange() {
  // 哈希路由监控实现
  let oldUrlHash = "";
  window.addEventListener(
    "hashchange",
    (event) => {
      const newUrl = event.newURL;
      // 构造哈希路由变更上报数据
      const reportData = {
        type: "behavior",
        subType: "hashchange",
        from: oldUrlHash,
        to: newUrl,
        startTime: performance.now(),  // 使用高精度时间戳
        uuid: generateUniqueId(),      // 生成唯一事件ID
      };
      lazyReportBatch(reportData);     // 延迟批量上报
      oldUrlHash = newUrl;             // 更新旧URL记录
    },
    true
  );
​
  // history路由监控实现 
  let oldUrlHistory = "";
  window.addEventListener(
    "popstate",
    (event) => {
      const newUrl = event.newURL;
      // 构造history路由变更上报数据
      const reportData = {
        type: "behavior",
        subType: "popstate",
        from: oldUrlHistory,
        to: newUrl,
        startTime: performance.now(),
        uuid: generateUniqueId(),
      };
      lazyReportBatch(reportData);
      oldUrlHistory = newUrl;
    },
    true
  );
}

一般行为监听,都是对一些事件的监听,比如点击、跳转、滚动等行为的监听上报。

异常监控(JS执行错误)

Js常见错误

标准基础错误
  • Error,基础错误类型,其他错误类型都继承自它

    js 复制代码
    throw new Error('通用错误信息');
  • SyntaxError,语法解析失败(通常在代码加载阶段抛出),这个在开发或构建的时候就能发现。

    js 复制代码
    const a
    // SyntaxError: Missing initializer in const declaration
  • TypeError ,操作类型不匹配 const a = 0 ;

    js 复制代码
    const a = 0
    a()
    // TypeError: a is not a function
  • ReferenceError,引用未声明的变量

    js 复制代码
    undefinedVar
    // ReferenceError: nodefined is not defined
  • RangeError,数值超出有效范围

    js 复制代码
    new Array(-1); 
    // RangeError: Invalid array length
  • URIError,URI 处理函数参数不合法

    js 复制代码
    decodeURIComponent('%'); 
    // URIError: URI malformed
其他特殊错误
  • InternalError,JavaScript引擎内部错误(非标准,如递归栈溢出)

    js 复制代码
    function stackOverflow() {
      stackOverflow();
    }
    stackOverflow(); 
    // InternalError: too much recursion
  • AggregateError,(ES2021+)

    js 复制代码
    Promise.any([Promise.reject(new Error('fail1')), 
                Promise.reject(new Error('fail2'))])
      .catch(e => console.log(e)); 
    // AggregateError: All promises were rejected

Js错误捕获方式

局部捕获
  • try {} catch (err) {} (同步代码)

    js 复制代码
    try {
      // 可能出错的代码
      JSON.parse('invalid json');
    } catch (error) {
      console.error('捕获到错误:', error.message);
    }
  • Promise 错误处理

    js 复制代码
    fetch('url')
      .then(res => res.json())
      .catch(error => console.error('请求失败:', error));
    ​
    async function fetchData() {
      try {
        const res = await fetch('url');
        return await res.json();
      } catch (error) {
        console.error('请求失败:', error);
      }
    }
全局捕获

window.onerror

js 复制代码
window.onerror = function(message, source, lineno, colno, error) {
  // 参数说明:
  // - message: 错误信息(字符串)
  // - source: 发生错误的脚本URL(字符串)
  // - lineno: 错误所在行号(数字)
  // - colno: 错误所在列号(数字)
  // - error: Error 对象(较新浏览器支持)
  
  console.error('全局错误捕获:', {
    message,
    source,
    lineno,
    colno,
    error
  });
  
  return true; // 返回 true 可阻止浏览器默认错误提示
};
​
// 捕获范围:同步代码的运行时错误
// 无法捕获:资源加载错误,Promise 未处理的 rejection
// 全局只有一个

addEventListener('error')

js 复制代码
window.addEventListener('error', (e) => {
  if (e.target.tagName === 'IMG') {
    console.error('图片加载失败:', e.target.src);
  }
  console.error('全局错误:', e.error);
}, true); // 注意使用捕获阶段
​
// 捕获范围:资源加载错误、JS 错误均可捕获
// 无法捕获:Promise 未处理的 rejection
// 可添加多个监听器

addEventListener('unhandledrejection')

js 复制代码
// 浏览器环境
window.addEventListener('unhandledrejection', (event) => {
  console.error('未处理的 Promise 错误:', event.reason);
});
​
// Node.js环境
process.on('uncaughtException', (error) => {
  console.error('未捕获异常:', error);
});
​
process.on('unhandledRejection', (reason, promise) => {
  console.error('未处理的 Promise rejection:', reason);
  // 可以通过 throw new error()  向外抛出代码 被 error 监听到
});

示例:前端JS错误监控

js 复制代码
import { lazyReportBatch } from "../report";
​
/**
 * 前端错误监控主函数,包含三类错误捕获:
 * 1. 资源加载错误(js/css/img等)
 * 2. JavaScript运行时错误
 * 3. 未处理的Promise拒绝(async/await错误)
 * 
 * @function error
 * @returns {void}
 */
export default function error() {
  // 资源加载错误监控(事件捕获阶段监听)
  window.addEventListener(
    "error",
    function (e) {
      const target = e.target;
      // 过滤非资源加载错误(如JS运行时错误)
      if (!target) {
        return;
      }
​
      // 仅处理带资源路径的加载错误(src/href属性)
      if (target.src || target.href) {
        const reportData = {
          type: "error",
          subType: "resource",  // 错误子类型:资源加载错误
          url: target.src || target.href,  // 资源路径
          html: target.outerHTML,  // 出错元素的完整HTML
          pageUrl: window.location.href,  // 当前页面URL
          paths: e.path,  // 事件冒泡路径
          startTime: performance.now()  // 错误发生时间戳
        };
        // todo 上报错误信息
        lazyReportBatch(reportData);
      }
    },
    true  // 使用捕获模式监听错误
  );
​
  /**
   * JavaScript运行时错误处理函数
   * 
   * @param {string} msg - 错误信息
   * @param {string} url - 出错脚本URL
   * @param {number} lineNo - 错误行号
   * @param {number} columnNo - 错误列号
   * @param {Error} error - Error对象
   */
  window.onerror = function (msg, url, lineNo, columnNo, error) {
    const reportData = {
      type: "error",
      subType: "js",  // 错误子类型:JS运行时错误
      msg,            // 原始错误信息
      url,            // 出错文件路径
      lineNo,         // 错误行号
      columnNo,       // 错误列号
      error,          // Error对象实例
      stack: error.stack,  // 完整错误堆栈
      pageUrl: window.location.href,  // 当前页面URL
      startTime: performance.now()  // 错误发生时间戳
    };
    // todo 上报
    lazyReportBatch(reportData);
  };
​
  // Promise未处理拒绝监控
  window.addEventListener("unhandledrejection", function (e) {
    const reportData = {
      type: "error",
      subType: "promise",  // 错误子类型:Promise错误
      msg: e.reason,       // 拒绝原因(通常为Error对象)
      pageUrl: window.location.href,  // 当前页面URL
      startTime: performance.now()  // 错误发生时间戳
    };
    // todo 上报
    lazyReportBatch(reportData);
  });
}

框架捕获

Vue

Vue.config.errorHandler
js 复制代码
Vue.config.errorHandler = (err, vm, info) => {
  console.error('Vue 错误:', err, info);
};

示例:针对vue项目的错误捕获

js 复制代码
window.__webEyeSDK__ = {
  version: "0.0.1",
};
​
​
// 针对vue项目的错误捕获
function install(Vue, options) {
  // SDK重复注册检查(保证单例)
  if (__webEyeSDK__.vue) return; // 防止重复注册
  __webEyeSDK__.vue = true;
  
  // 初始化SDK配置
  setConfig(options);
  
  // 保存原始错误处理器以便链式调用
  const handler = Vue.config.errorHandler;
​
  // 重写Vue全局错误处理器
  Vue.config.errorHandler = function (err, vm, info) {
    // 构造错误上报数据结构
    const reportData = {
      type: "error",
      subType: "vue",       // 标识错误来源为Vue框架
      error: err.stack,     // 完整的错误堆栈信息
      info,                 // Vue提供的错误上下文信息(如生命周期钩子名称)
      pageUrl: window.location.href,
      // paths: [vm],       // 保留的组件追踪路径字段
      startTime: window.performance.now() // 用于性能追踪的时间戳
    };
    
    // 批量延迟上报错误信息
    lazyReportBatch(reportData);
​
    // 调用原始错误处理器保持框架原有行为
    if (handler) {
      handler(err, vm, info);
    }
  };
}

React

自行摸搜,ToDo

跨域问题

一般情况,如果window.onerror出现Script Error,这样的错误,基本上可以确定是出现了跨域问题。解决办法有两个。

  • 后端配置Access-Control-Allow-Origin、前端script加crossorigin

    js 复制代码
    <script src="http://good.cn/test.js" crossorigin></script>
  • 如果不能修改服务端的请求头,可以考虑通过使用 try/catch将错误抛出

    js 复制代码
    try {
        foo(); // 调用test.js中定义的foo方法
      } catch (e) {
        throw e;
     }

如何进行上报

上报方式

原理 :使用 navigator.sendBeacon() 发送异步 POST 请求,专门设计用于统计和错误上报

  • 优点

    • 无阻塞:异步发送,不阻塞页面卸载。
    • 支持页面卸载时上报:即使页面关闭,数据也能成功发送。
    • 兼容性:现代浏览器支持(不支持 IE)。
    • 数据限制:最大约 1MB(不同浏览器可能有差异)。
  • 缺点

    • 不支持 IE 浏览器
    • 无法自定义 Content-Type :需将数据转换为 BlobFormData

Image 对象(GET 请求)

  • 原理 :通过创建一个 1x1 的图片元素,将错误信息编码到图片的 URL 查询参数中,触发 GET 请求。(同样尺寸,gif格式的图最小)

  • 优点

    • 无跨域问题:浏览器对图片资源的跨域限制较宽松。
    • 异步非阻塞:不会阻塞页面加载。
    • 兼容性好:支持所有浏览器。
  • 缺点

    • 数据量限制:不同浏览器对 URL 长度限制不同(通常 2KB-8KB),可能导致数据截断。
    • 仅支持 GET 请求:无法自定义请求头或内容类型

XMLHttpRequest(不推荐直接使用)

  • 原理 :通过 XMLHttpRequestfetch 发送 POST 请求,将错误信息以 JSON 格式发送到服务器。

  • 优点

    • 支持完整的 HTTP 协议,可自定义请求头和内容类型(如 Content-Type: application/json)。
  • 缺点

    • 跨域限制:需要服务器配置 CORS。
    • 阻塞性:可能阻塞页面加载或交互。
    • 页面卸载时易丢失:页面关闭时未完成的请求会被浏览器取消

第三方监控工具(如 Sentry、神策)

  • 原理:集成第三方 SDK(如 Sentry、神策),自动或手动上报错误。

  • 优点

    • 自动捕获:无需手动处理,自动捕获 JS 错误、资源加载错误、Promise 错误等。
    • 丰富功能:聚合错误、性能监控、用户行为分析等。
    • 开箱即用:提供完整的后台分析和告警功能

上报策略

避免数据重复冗余的策略

唯一标识
  • 错误指纹(Error Fingerprint)

    • 为每个错误生成唯一标识,基于错误类型,堆栈信息,URL,行号、列号等关键字段生成

      js 复制代码
      const generateFingerprint = (error) => {
        const { message, stack, url } = error;
        return crypto.createHash('md5').update(`${message}-${stack}-${url}`).digest('hex');
      };
  • 已上报缓存

    使用 SetMap 缓存已上报的错误指纹,避免短时间内重复上报

    js 复制代码
    const reportedErrors = new Set();
    function reportError(error) {
      const fingerprint = generateFingerprint(error);
      if (reportedErrors.has(fingerprint)) return;
      reportedErrors.add(fingerprint);
      // 执行上报逻辑
    }
错误聚合(Aggregation)

服务端聚合

通过服务端(如Sentry)的聚合功能,将相同指纹的错误合并为一个事件,展示总次数和首次/最近发生时间

客户端预聚合

客户端暂存错误数据,按指纹合并后批量上报,减少请求次数

控制上报频率的策略

限制上报频率

在一定时间窗口内(如1分钟)只上报一次相同错

js 复制代码
let lastReportTime = 0;
const throttleTime = 60 * 1000; // 1分钟
function reportError(error) {
  const now = Date.now();
  if (now - lastReportTime < throttleTime) return;
  lastReportTime = now;
  // 执行上报逻辑
}
采样率(Sampling)

对非关键错误(如资源加载失败)设置采样率(如10%),减少上报量;其实就是一个随机数

js 复制代码
const samplingRate = 0.1; // 10%采样
function reportError(error) {
  if (Math.random() > samplingRate) return;
  // 执行上报逻辑
}
优先级区分

按错误严重程度分级

  • 致命错误(如页面崩溃):立即上报,无节流。
  • 普通错误(如接口超时):按节流策略上报。
  • 低优先级错误(如非关键资源加载失败):采样或延迟上报

数据压缩与优化

数据瘦身
  • 关键信息优先:仅上报必要字段(如错误信息、堆栈、URL、时间戳),避免冗余日志¨

  • 堆栈截断:截取堆栈的前N层(如前10层),减少数据量

    js 复制代码
    const MAX_STACK_LINES = 10;
    const stackLines = error.stack.split('\n').slice(0, MAX_STACK_LINES);
    const truncatedStack = stackLines.join('\n');
数据编码
  • Base64压缩 :对JSON数据进行压缩(如使用 lz-string)和Base64编码,减少传输体积
js 复制代码
import { compressToBase64 } from 'lz-string';
const data = JSON.stringify(errorData);
const compressedData = compressToBase64(data);
客户端缓存与重试机制

临时存储未上报数据

将错误数据暂存到 localStorageIndexedDB,在网络恢复时批量上报

js 复制代码
function cacheError(error) {
  const cachedErrors = JSON.parse(localStorage.getItem('cachedErrors')) || [];
  cachedErrors.push(error);
  localStorage.setItem('cachedErrors', JSON.stringify(cachedErrors));
}

失败重试策略

上报失败时,按指数退避算法(如1s、2s、4s...)重试,避免频繁请求

js 复制代码
function retryOnError(retryCount = 0) {
  const delay = Math.pow(2, retryCount) * 1000;
  setTimeout(() => {
    // 重新尝试上报
    if (/* 上报成功 */) {
      // 清除缓存
    } else {
      retryOnError(retryCount + 1);
    }
  }, delay);
}

核心流程

捕获错误生成唯一指纹本地缓存节流/采样压缩上报服务端聚合

结语

除了捕获上报,后面还需要学习一下以下只是

  • 如何定位线上错误,也就是如何还原Source Map;
  • 如何记录和回放DOM操作
  • 如何实现大屏数据展示
  • 如何实现错误警报等

最后通过学习,我也是实现了一个简易的埋点上报sdk,如果有需要学习的前往查看。

祝好运

gitee地址:gitee.com/zhuang_quan...

相关推荐
Version几秒前
深入理解JavaScript 中的 this
前端
李鸿耀13 分钟前
前端包管理工具:npm/Yarn/pnpm 特性解析与实战指南
前端
Enti7c20 分钟前
页面重构过程中如何保证良好的跨浏览器一致性?
前端·其他
海狸鼠27 分钟前
几行代码实现MCP服务端/客户端(接入DeepSeek)
前端·后端
木木黄木木33 分钟前
基于HTML5的拖拽排序功能实现详解
前端·html·html5
Stupid36 分钟前
[学习笔记] 工程化的浅入了解
前端
阿航hang37 分钟前
Reactivity 模块
前端
PineSongCN39 分钟前
nginx 反向代理后SSE连接无效的问题
前端
还是鼠鼠39 分钟前
Node.js 路由 - 初识 Express 中的路由
前端·vscode·前端框架·npm·node.js·express
Moment42 分钟前
岗位急招,算法实习、音乐生成、全栈、flutter 都有,早十晚六 😍😍😍
前端·后端·面试