https://web.developers.google.cn/articles/vitals?hl=zh-cn&name=76433&img_url=https%3A%2F%2Fthirdwx.qlogo.cn%2Fmmopen%2Fvi_32%2FMfrdbsWT9PMG2CvrGxvMhAA1M9icqz9CVyVHAqdq5Bm0T51vpyicASIzRQofqiaB9pSYFzEJv4eXJoKiaXvfdMHXR6y7bMxicxZntQfJuAlWXjmI%2F132&key=896e2aec333237d1b71d4988172b4ad05f7393ab9853d3234bf45b344432b7a1&redirected=truehttps://segmentfault.com/a/1190000041264521https://github.com/juwenzhang/error-monitor-sdk

核心流程

  • 第一步进行获取数据信息:FP/FCP/LCP/CLS/INP 这些性能指标设计,以及利用performanceobserver 来实现对应的类似功能吧

  • 第二步获取具体归因结果:具体的结果反馈,知道具体的性能指标慢的具体原因统计

  • 第三步理解当下前端开发的SPA:在不刷新切换路由的情况下,进行分段的统计,实现数据不打架的实现吧

核心维度

  • 加载性能的指标

    • 实现衡量的是页面能不能快速的打开的指标

    • 核心的监控页面的是从输入 URL 到首次渲染、完整渲染的全流程的耗时,聚焦于用户的等待体验,关键的指标是以 Google Core Web Vitals 为核心,搭建传统的辅佐指标,直接影响用户的留存(加载耗时如果每增加1s,那么存在的问题就是用户的留存率下降 10% 以上)

    • 核心指标类型有

      • FP First Paint 首次绘制指标,浏览器开始渲染任何东西的时刻,也就是白屏阶段吧

      • FCP First Contentful Paint 首次内容绘制,浏览器渲染出第一个内容的的时刻(文字、图片、logo 吧),1.8s为基准衡量

      • LCP largetst Contentful Paint 最大内容绘制,视口内可见的最大图片或文本块渲染完成的时刻。这是 Google 最看重的加载指标,2.5s 为基准来衡量

  • 交互性能

    • 实现衡量的是页面的流畅度的信息

    • 核心监控的是用户和页面交互的相应速度,聚焦于用户的操作体验,避免出现热点击区域的失效,也就是点击后无响应,滑动卡顿等问题出现吧,关键的指标的关注点是进行用户的交互延迟和主线程阻塞的问题,尤其是针对对应的复杂的交互逻辑场景吧

    • 核心指标类型有

      • FIP First Input Delay 首次出入延迟指标,用户第一次与页面交互(点击按钮、链接)到浏览器真正开始处理这个事件的时间差

      • INP Interaction To Next Paint 就是FIP 的升级版本吧,它不仅看第一下,还看你浏览全程中所有交互的延迟,取最慢的那几次

      • Long Task 任何执行时间超过 50ms 的 JavaScript 任务。

      • FID < 100ms,INP < 200ms,Long Task < 50ms

  • 视觉稳定性指标

    • CLS 累计布局偏移的指标吧,页面布局在加载过程中发生意外移动的程度,核心是 < 0.1

  • 稳定性指标

    • 实现衡量的是页面能不能正常运行的指标信息吧

    • 核心是监控页面运行过程中的异常情况,聚焦“可用体验”,是前端监控的底线——页面再快,若频繁崩溃、报错,用户也会直接离开,关键指标包括错误率、资源加载失败率等

  • 自定义的一些业务指标

    • 结合具体的业务进行自定义统计指标来实现吧,将性能数据和业务指标进行联动,来实现自定义的统计,提升转化率

监控维度

指标名称

核心定义

达标阈值(良好)

采集逻辑(核心)

不达标原因

加载性能

最大内容绘制(LCP)

页面开始加载到视口中最大内容元素(如首屏banner、大标题)完成绘制的时间,最能反映首屏加载体验

≤2.5秒

通过PerformanceObserver监听largest-contentful-paint事件,取最后一个触发的entry(排除极端值)

LCP资源未优化、JS/CSS阻塞渲染、服务器响应延迟

首字节时间(TTFB)

浏览器发送请求到接收服务器返回第一个字节的时间,反映服务器响应速度

≤600ms

通过Performance.timing计算(responseStart - navigationStart)

服务器负载高、网络延迟、接口优化不足

首次内容绘制(FCP)

页面开始加载到首次绘制文本、图片等内容的时间,标志“白屏结束”

≤1.8秒

通过PerformanceObserver监听paint事件,筛选type为first-contentful-paint的entry

关键CSS未内联、首屏资源加载缓慢

累积布局偏移(CLS)

页面加载过程中,元素意外偏移的累积分数(偏移距离×偏移影响范围),反映页面稳定性

≤0.1

通过PerformanceObserver监听layout-shift事件,累积所有有效偏移分数

媒体元素未设宽高比、动态插入DOM、字体加载导致文字重排

交互性能

交互下一步延迟(INP)

用户触发交互(点击、输入、滚动等)到浏览器完成下一次绘制的时间,取第98百分位值(排除极端值),2024年替代FID成为核心交互指标

≤200ms

通过PerformanceObserver监听interaction-record事件,统计所有交互的延迟并取第98百分位

主线程被长任务占用、交互回调含复杂DOM操作

长任务指标(Long Task)

主线程执行时间≥50ms的任务,会阻塞渲染和交互,导致卡顿

无长任务或占比≤5%

通过PerformanceObserver监听longtask事件,统计长任务数量和耗时占比

JS代码冗余、复杂计算未拆分、高频事件未做防抖节流

稳定性性能

JS错误率

页面运行过程中JS语法错误、运行时错误的发生率(错误次数/页面访问次数)

≤0.1%

通过window.onerror监听普通JS错误,window.addEventListener('unhandledrejection')监听Promise错误

代码逻辑漏洞、参数异常、框架使用不当、第三方脚本报错

资源加载失败率

页面中JS、CSS、图片、字体等资源加载失败的比例(失败资源数/总资源数)

≤0.5%

通过window.addEventListener('error', fn, true)在捕获阶段监听资源加载错误

资源路径错误、CDN故障、跨域配置异常、服务器返回404/500

核心准则

  • 用户为中心:优先监控影响用户体验的指标(如LCP、INP),而非单纯的技术指标(如CPU使用率),避免“指标好看但用户体验差”;

  • 真实场景优先:实验室环境(如本地测试)数据仅作参考,重点采集真实用户监控(RUM)数据,覆盖不同设备、网络、浏览器场景;

  • 轻量化采集:监控SDK体积需控制在30KB以内,采集逻辑不阻塞主线程,避免“监控本身拖慢页面性能”,如采用异步采集、批量上报;

  • 可落地可优化:每个监控指标需对应明确的优化方向,避免监控“只报数不解决问题”;

  • 数据可靠合规:采集数据需脱敏(避免用户敏感信息),采用HTTPS加密上报,避免localStorage存储埋点数据(存在同步阻塞、容量不足等缺陷)。

核心方案

web-vitals

轻量级指标采集库,专注于Core Web Vitals(LCP、INP、CLS)及其他关键性能指标的采集,无多余功能,是所有监控方案的“基础依赖”

  • 核心原理:基于浏览器原生Performance API、PerformanceObserver API封装,自动处理指标采集的兼容性问题(如低版本浏览器降级),简化手动采集的复杂逻辑,可直接获取标准化的指标数据,支持自定义上报逻辑。

  • 核心优势:体积极小(≈3KB gzip)、无侵入、兼容性好(支持Chrome 60+、Firefox 63+等主流浏览器)、指标权威(完全贴合Google Core Web Vitals标准),可灵活集成到自有监控系统。

  • 适用场景:需要自定义监控系统、仅需采集核心性能指标,无需复杂的分析和告警功能;可搭配其他工具(如Sentry、自研上报服务)使用。

  • 关键补充:支持组件级性能监控,可结合Web Components实现单个组件的渲染、交互性能追踪,通过mark()、measure()方法标记性能节点。

sentry

开源的全链路监控工具,最初专注于错误监控,目前已完善性能监控能力,实现“错误+性能”一体化追踪,支持前端、后端、移动端全端监控

  • 核心原理:

    • 性能采集:通过浏览器原生API采集加载、交互、资源等指标,结合自定义埋点采集业务性能指标;

    • 错误采集:通过window.onerror、unhandledrejection、框架错误钩子(如Vue.config.errorHandler、React.useErrorBoundary)捕获所有类型错误,结合sourcemap还原压缩后代码的报错位置;

    • 数据处理:采集的数据经过清洗、聚合后存储,支持按版本、浏览器、设备、地区等维度筛选分析;

    • 上报机制:采用异步批量上报,网络不稳定时利用IndexedDB缓存数据,待网络恢复后自动重传,避免数据丢失。

  • 核心优势:功能全面(错误追踪+性能监控+用户行为回放)、可定制化程度高、社区活跃(问题解决快)、支持插件化扩展,可集成rrweb实现用户操作录屏,快速复现问题场景。

  • 适用场景:中大型团队、需要一体化监控(错误+性能)、希望快速定位问题根源(如“卡顿伴随报错”场景),支持自主部署和云服务两种模式

rrweb

开源的用户行为录屏工具,并非单纯的性能监控工具,但却是性能/错误定位的“神器”,可记录用户在页面上的所有操作(点击、输入、滚动、跳转),还原页面运行状态

  • 核心原理:通过监听DOM变化、鼠标/键盘事件、页面跳转等,将用户操作转化为可序列化的事件数据,存储或上报后,可通过rrweb-player还原整个操作流程,搭配性能/错误数据,能清晰看到“用户操作→性能卡顿/错误”的关联关系。

  • 核心优势:轻量化(不影响页面性能)、无侵入(无需修改业务代码)、还原度高,可结合Sentry、自研监控系统使用,将行为数据与性能、错误数据关联,大幅提升问题定位效率。

  • 适用场景:难以复现的性能/错误问题(如“部分用户卡顿”“偶发报错”),需要还原用户真实操作场景,辅助排查瓶颈

pinpoint

开源的分布式全链路监控工具,重点关注“前端→后端→数据库”的全链路性能,前端性能监控作为其中一部分,适合前后端联动排查性能问题

  • 核心原理:通过前端SDK采集页面加载、接口请求等性能数据,后端通过Agent采集接口、数据库耗时,利用Trace ID将前端请求与后端接口关联,形成全链路性能链路图,定位“前端慢还是后端慢”。

  • 核心优势:全链路联动、适合微服务架构、可定位跨端性能瓶颈,支持按Trace ID追踪整个请求流程,明确性能瓶颈在前端、接口还是数据库。

  • 适用场景:微服务架构、前后端联动的性能问题排查(如“页面加载慢是因为接口耗时久,还是前端渲染慢”),中大型分布式项目

lighthouse

Google官方开源的自动化性能检测工具,专注于实验室环境下的性能评估,提供详细的优化建议,是性能优化的“辅助工具”,而非线上实时监控工具

  • 核心原理:模拟真实浏览器加载页面,采集Core Web Vitals及其他性能指标,生成标准化的性能报告(0-100分,80分以上为良好),标注不达标指标,并给出具体优化建议(如“压缩未优化图片可节省200KB”)。

  • 核心优势:免费、权威、自动化程度高,支持DevTools、命令行、CI/CD集成三种使用方式,可集成到研发流程中,实现性能劣化自动化拦截。

  • 适用场景:性能优化前期评估、研发流程中性能校验(如CI/CD集成,阻止性能劣化代码上线),不能替代线上RUM监控(数据为实验室环境,与真实用户场景有差异)

方案类型

代表方案

核心优势

核心劣势

适用团队/场景

开源方案

web-vitals

轻量、权威、灵活,贴合Core Web Vitals

仅支持指标采集,无分析、告警功能

所有团队,作为指标采集基础依赖

Sentry

错误+性能一体化,可定制,社区活跃

自主部署维护成本高,海量数据处理需优化

中大型团队,需要一体化监控

rrweb

行为回放,辅助问题定位,轻量化

非独立监控工具,需搭配其他方案使用

所有团队,用于复现偶发性能/错误问题

Pinpoint

全链路联动,适合微服务架构

部署复杂,学习成本高

中大型分布式团队,前后端联动排查

Lighthouse

免费权威,自动化检测,优化建议详细

仅实验室环境,无线上实时监控

所有团队,性能优化评估、CI/CD集成

商业方案

New Relic

全端一体化,enterprise级支持,功能强大

收费高,国内访问速度一般

大型企业,预算充足,全端监控需求

Datadog

云原生适配好,多维度分析,灵活

收费中等,国内生态适配一般

云原生项目,中小型团队

腾讯云RUM/阿里云ARMS

国内场景适配好,集成简单,性价比高

定制化程度不如开源方案

国内中小型团队,快速落地,省心高效

SDK 流程

  • 「采集层」:Loading / Interaction / VisualStability / Network(各司其职)

  • 「处理层」:数据清洗、格式化、核心指标归因(找到卡顿的 DOM 或脚本)

  • 「上报层」:支持 sendBeacon + fetch keepalive 双保险,确保数据不丢失

  • 「配置中心」:环境区分、采样率控制、日志开关

监控类型

  • 性能监控

    • 就是本文正在阐述的一个吧

    • 核心定位:监控页面 “加载快不快、操作流不流畅”,量化用户体验

    • 关键监控内容:对应文档中「加载性能」「交互性能」,包括 LCP、INP、CLS、TTFB、长任务等指标,以及页面渲染、资源加载、接口请求耗时等。

  • 异常监控

    • 核心定位:监控页面 “能不能正常运行”,排查线上故障,对应文档中「稳定性性能」

    • 关键监控内容:JS 语法 / 运行时错误、Promise 错误、资源加载失败(JS/CSS/ 图片)、框架错误(Vue/React)、页面崩溃等,文档中 Sentry 方案重点覆盖此类监控。

  • 用户行为监控

    • 核心定位:还原用户真实操作场景,辅助排查性能 / 异常问题(“为什么出问题、怎么触发的问题”)

    • 关键监控内容:用户点击、输入、滚动、页面跳转、路由切换,以及自定义行为(如按钮点击、表单提交),文档中 rrweb 方案就是此类监控的核心工具,可与性能 / 异常数据联动

  • 业务监控

    • 核心定位:结合具体业务,将监控与业务目标绑定,让监控有 “业务价值”

    • 关键监控内容:文档中「补充:业务性能」相关内容,比如电商的 “加入购物车响应耗时”“支付页面加载耗时”,工具类产品的 “功能执行耗时”,以及业务埋点相关的转化、跳转类指标

落地准备

  • 架构选型:基于 monorepo 来开发不同类型的 SDK 开发吧,以及进行拆分子包进行操作实现,实现统一的调度,降低开发成本,适配多项目的使用以及单独的使用细节吧

  • 监控范围:覆盖四大核心的监控类型,性能监控、异常监控实现,行为监控实现,业务自定义监控实现

  • 公共部分依赖:核心负责的是数据采集,数据清洗,可视化面板反馈

frontend-monitor-sdk/  # 监控SDK根目录(monorepo根目录)
├── packages/          # 所有子包存放目录
│   ├── monitor-core/  # 核心入口子包(统一初始化、调度所有子包)
│   ├── performance-monitor/  # 性能监控子包(加载+交互性能)
│   ├── error-monitor/        # 异常监控子包(JS错误+资源错误)
│   ├── behavior-monitor/     # 行为监控子包(用户操作采集)
│   ├── business-monitor/     # 业务自定义监控子包(自定义埋点)
│   ├── data-collect/         # 公共子包1:数据采集(统一采集逻辑)
│   ├── data-clean/           # 公共子包2:数据清洗(统一清洗规则)
│   └── visual-panel/         # 公共子包3:可视化面板(数据展示+归因)
├── pnpm-workspace.yaml       # monorepo包管理配置
└── package.json              # 根目录配置(脚本、依赖管理)

monitor-core

// 引入所有监控子包和公共子包
import { PerformanceMonitor } from '@monitor/performance-monitor';
import { ErrorMonitor } from '@monitor/error-monitor';
import { BehaviorMonitor } from '@monitor/behavior-monitor';
import { BusinessMonitor } from '@monitor/business-monitor';
import { DataClean } from '@monitor/data-clean';

// 监控SDK核心类(暴露给业务项目)
export class MonitorSDK {
  // 子包实例存储
  private performanceMonitor: PerformanceMonitor;
  private errorMonitor: ErrorMonitor;
  private behaviorMonitor: BehaviorMonitor;
  private dataClean: DataClean;

  // 初始化配置(业务项目集成时传入)
  constructor(private config: {
    reportUrl: string; // 数据上报地址
    sampleRate: number; // 采样率(0-1,避免全量上报压垮服务器)
    env: 'dev' | 'test' | 'prod'; // 环境区分
  }) {
    // 初始化公共子包(数据清洗)
    this.dataClean = new DataClean();
    // 初始化所有监控子包,传入公共依赖和配置
    this.initSubPackages();
  }

  // 初始化所有监控子包
  private initSubPackages() {
    this.performanceMonitor = new PerformanceMonitor({
      reportUrl: this.config.reportUrl,
      sampleRate: this.config.sampleRate,
      dataClean: this.dataClean // 注入数据清洗实例
    });
    this.errorMonitor = new ErrorMonitor(this.config);
    this.behaviorMonitor = new BehaviorMonitor(this.config);
    
    // 启动所有子包(开始采集数据)
    this.startAllSubPackages();
  }

  // 启动所有监控子包
  private startAllSubPackages() {
    this.performanceMonitor.start();
    this.errorMonitor.start();
    this.behaviorMonitor.start();
  }

  // 暴露给业务项目的API:自定义业务埋点(调用业务监控子包)
  public trackBusinessEvent(eventName: string, data: Record<string, any>) {
    BusinessMonitor.track(eventName, data, this.config, this.dataClean);
  }

  // 暴露给业务项目的API:停止监控
  public stop() {
    this.performanceMonitor.stop();
    this.errorMonitor.stop();
    this.behaviorMonitor.stop();
  }
}

// 业务项目集成时,直接初始化即可
// 示例:const monitor = new MonitorSDK({ reportUrl: 'xxx', sampleRate: 0.8, env: 'prod' });
  • 核心负责的是初始化子包 init 过程,启动子包 start,定义自定义的业务子包,最终暴露停止 stop 实现吧

performance-monitor

  • 核心的业务场景是:

    • 收集加载的性能指标

    • 收集交互的性能指标

    • 收集可视化稳定性的性能指标

    • 收集长任务的性能指标

  • 具体采集

    • 加载性能:TTFB(首字节时间)、LCP(最大内容绘制)、FCP(首次内容绘制)、FP(首次绘制);

    • 交互性能:INP(交互下一步延迟,替代旧版FID)、长任务监测(Long Task);

    • 渲染性能:CLS(累积布局偏移);

    • 补充指标:资源加载耗时(JS、CSS、图片等)、接口请求耗时(前端采集,关联性能感知)

架构设计

performance-monitor/  # monorepo下的性能监控子包
├── src/
│   ├── index.ts        # 子包入口(暴露核心API,供monitor-core调度)
│   ├── collector/      # 指标采集核心目录(分文件实现每个指标采集)
│   │   ├── load.ts     # 加载性能指标采集(TTFB、LCP等)
│   │   ├── interaction.ts # 交互性能指标采集(INP、长任务)
│   │   ├── render.ts   # 渲染性能指标采集(CLS)
│   │   └── resource.ts # 资源加载耗时采集
│   ├── handler/        # 数据处理目录(清洗、格式化、上报)
│   │   ├── data-clean.ts # 前端数据清洗(过滤异常、标准化)
│   │   └── report.ts   # 数据上报(前端侧实现,对接微量后端)
│   └── utils/          # 工具函数目录(兼容性、辅助函数)
│       ├── compatible.ts # 浏览器兼容性处理(核心工具)
│       └── helper.ts   # 辅助函数(如时间格式化、异常捕获)
├── package.json        # 子包配置(声明依赖、入口)
└── rollup.config.js    # 打包配置(适配SDK打包,体积优化)

兼容性工具

主要是判断浏览器的支持性吧,因为这里的话需要使用一些比较新的 API 类似于:PerformanceObserver 和 performance ,来实现后续的降级处理避免采集代码报错的问题出现吧

/**
 * 前端性能监控兼容性处理工具
 * 核心:判断浏览器是否支持性能相关API,提供降级方案
 */
export const CompatibleUtil = {
  // 判断是否支持Performance API(核心,所有指标采集依赖)
  isSupportPerformance(): boolean {
    return !!window.performance && typeof window.performance === 'object';
  },

  // 判断是否支持PerformanceObserver(LCP、INP、CLS等指标依赖)
  isSupportPerformanceObserver(): boolean {
    if (!this.isSupportPerformance()) return false;
    return !!window.PerformanceObserver && typeof window.PerformanceObserver === 'function';
  },

  // 判断是否支持PerformanceEntry相关类型(如largest-contentful-paint、layout-shift等)
  isSupportPerformanceEntry(type: string): boolean {
    if (!this.isSupportPerformanceObserver()) return false;
    // 测试是否能监听对应类型的entry
    try {
      new PerformanceObserver(() => {}).observe({ type, buffered: true });
      return true;
    } catch (err) {
      console.warn(`浏览器不支持${type}类型的性能指标采集`, err);
      return false;
    }
  },

  // 降级处理:获取当前时间戳(兼容低版本浏览器)
  getNow(): number {
    if (this.isSupportPerformance()) {
      return window.performance.now(); // 高精度时间戳
    }
    return new Date().getTime(); // 低版本降级为普通时间戳
  }
};
import { CompatibleUtil } from './compatible';

/**
 * 辅助工具函数
 * 核心:简化采集逻辑,处理通用操作(异常捕获、时间格式化等)
 */
export const HelperUtil = {
  // 异常捕获包装器:避免采集代码报错影响业务代码
  tryCatchWrapper<T extends (...args: any[]) => any>(fn: T, fallback?: ReturnType<T>): ReturnType<T> | undefined {
    try {
      return fn();
    } catch (err) {
      console.warn('性能指标采集异常', err);
      return fallback;
    }
  },

  // 时间格式化:将时间戳(ms)格式化为可读格式(用于调试、日志)
  formatTime(ms: number): string {
    if (ms < 1000) return `${ms.toFixed(2)}ms`;
    if (ms < 60 * 1000) return `${(ms / 1000).toFixed(2)}s`;
    return `${(ms / (60 * 1000)).toFixed(2)}min`;
  },

  // 数据去重:基于指标类型+时间戳,避免重复采集(如LCP多次触发)
  deduplicateData(key: string): boolean {
    if (!window.localStorage) return false; // 低版本浏览器不做去重
    const cacheKey = `perf_monitor_${key}`;
    if (window.localStorage.getItem(cacheKey)) {
      return false; // 已采集,跳过
    }
    // 缓存10分钟(避免同一会话内重复采集,过期自动失效)
    window.localStorage.setItem(cacheKey, '1');
    setTimeout(() => {
      window.localStorage.removeItem(cacheKey);
    }, 10 * 60 * 1000);
    return true;
  },

  // 获取当前页面URL(脱敏处理,避免采集敏感信息)
  getPageUrl(): string {
    const url = window.location.href;
    // 脱敏:过滤URL中的query参数(如userId、token等敏感信息)
    return url.replace(/\?.*/, '');
  },

  // 判断是否为首次加载(避免路由跳转后重复采集加载指标)
  isFirstLoad(): boolean {
    if (!CompatibleUtil.isSupportPerformance()) return true;
    // 基于performance.navigation判断(低版本)或navigation timing v2判断(高版本)
    const navigation = window.performance.navigation || (window.performance.getEntriesByType('navigation')[0] as any);
    // 0:首次加载,1:刷新,2:后退/前进
    return navigation?.type === 0;
  }
};

性能指标采集

加载指标

加载指标核心反应的是是页面从请求到首次渲染的耗时,重点采集的是 TTFB,LCP,FCP,FP 等性能指标,其中 LCP FCP FP 都是依赖于 performanceObserver 来实现的讷,TTFP 就是基于时间来实现吧

import { CompatibleUtil } from '../utils/compatible';
import { HelperUtil } from '../utils/helper';

/**
 * 加载性能指标采集(TTFB、LCP、FCP、FP)
 * 技术要点:
 * 1. TTFB:利用performance.timing(低版本)或navigation timing v2(高版本)采集,反映服务器响应速度
 * 2. LCP/FCP/FP:利用PerformanceObserver监听对应entry,采集渲染时间,注意LCP可能多次触发(需去重)
 * 3. 兼容性:低版本浏览器不支持PerformanceObserver时,降级不采集LCP/FCP/FP
 */
export const LoadPerformanceCollector = {
  // 采集TTFB(首字节时间):从请求发送到接收第一个字节的时间
  collectTTFB(): { type: string; value: number; time: number; page: string } | null {
    return HelperUtil.tryCatchWrapper(() => {
      if (!CompatibleUtil.isSupportPerformance()) return null;

      // 优先使用navigation timing v2(高版本浏览器,更精准)
      const navigationEntry = window.performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
      if (navigationEntry) {
        // TTFB = responseStart - requestStart(请求发送到接收首字节的时间)
        const ttfb = navigationEntry.responseStart - navigationEntry.requestStart;
        return {
          type: 'performance_ttfb',
          value: ttfb, // 单位:ms
          time: Date.now(),
          page: HelperUtil.getPageUrl()
        };
      }

      // 降级使用performance.timing(低版本浏览器)
      const timing = window.performance.timing;
      const ttfb = timing.responseStart - timing.requestStart;
      return {
        type: 'performance_ttfb',
        value: ttfb,
        time: Date.now(),
        page: HelperUtil.getPageUrl()
      };
    }, null);
  },

  // 采集LCP(最大内容绘制):页面最大内容元素渲染完成的时间,反映页面加载完成度
  collectLCP(callback: (data: { type: string; value: number; time: number; page: string; element: string } | null) => void): void {
    HelperUtil.tryCatchWrapper(() => {
      if (!CompatibleUtil.isSupportPerformanceObserver() || !CompatibleUtil.isSupportPerformanceEntry('largest-contentful-paint')) {
        callback(null);
        return;
      }

      // 监听largest-contentful-paint事件,LCP可能多次触发(如图片加载完成后替换文字)
      const observer = new PerformanceObserver((entries) => {
        // 取最后一个entry(最终的最大内容元素)
        const lcpEntry = entries.getEntries().pop() as PerformanceLargestContentfulPaint;
        if (!lcpEntry) {
          callback(null);
          return;
        }

        // 去重:避免同一页面多次采集LCP
        const deduplicateKey = `lcp_${HelperUtil.getPageUrl()}_${lcpEntry.startTime}`;
        if (!HelperUtil.deduplicateData(deduplicateKey)) {
          return;
        }

        // 组装数据:包含元素标签(辅助归因,如img、div)
        const data = {
          type: 'performance_lcp',
          value: lcpEntry.startTime, // 单位:ms(相对于页面加载开始的时间)
          time: Date.now(),
          page: HelperUtil.getPageUrl(),
          element: lcpEntry.element?.tagName.toLowerCase() || 'unknown' // 最大内容元素标签
        };

        callback(data);
      });

      // 监听LCP事件,buffered: true表示监听页面加载过程中已触发的事件
      observer.observe({ type: 'largest-contentful-paint', buffered: true });

      // 页面卸载时销毁观察者,避免内存泄漏
      window.addEventListener('beforeunload', () => {
        observer.disconnect();
      });
    });
  },

  // 采集FCP(首次内容绘制)和FP(首次绘制):反映页面首次渲染的时间
  collectFPAndFCP(callback: (data: Array<{ type: string; value: number; time: number; page: string }> | null) => void): void {
    HelperUtil.tryCatchWrapper(() => {
      if (!CompatibleUtil.isSupportPerformanceObserver() || !CompatibleUtil.isSupportPerformanceEntry('paint')) {
        callback(null);
        return;
      }

      const result: Array<{ type: string; value: number; time: number; page: string }> = [];
      const observer = new PerformanceObserver((entries) => {
        entries.getEntries().forEach((entry: PerformancePaintTiming) => {
          // 过滤FP(first-paint)和FCP(first-contentful-paint)
          if (entry.name !== 'first-paint' && entry.name !== 'first-contentful-paint') {
            return;
          }

          // 去重:避免重复采集
          const deduplicateKey = `${entry.name}_${HelperUtil.getPageUrl()}_${entry.startTime}`;
          if (!HelperUtil.deduplicateData(deduplicateKey)) {
            return;
          }

          result.push({
            type: entry.name === 'first-paint' ? 'performance_fp' : 'performance_fcp',
            value: entry.startTime, // 单位:ms
            time: Date.now(),
            page: HelperUtil.getPageUrl()
          });
        });

        // 采集完成后触发回调(FP和FCP均采集完成)
        if (result.length === 2) {
          callback(result);
          observer.disconnect(); // 采集完成后销毁观察者
        }
      });

      // 监听paint事件
      observer.observe({ type: 'paint', buffered: true });

      // 页面卸载时销毁观察者
      window.addEventListener('beforeunload', () => {
        observer.disconnect();
      });
    });
  },

  // 批量采集所有加载性能指标(供外部调用)
  collectAllLoadPerformance(callback: (data: Array<any>) => void): void {
    const loadData: Array<any> = [];

    // 采集TTFB
    const ttfbData = this.collectTTFB();
    if (ttfbData) {
      loadData.push(ttfbData);
    }

    // 采集LCP
    this.collectLCP((lcpData) => {
      if (lcpData) {
        loadData.push(lcpData);
      }
    });

    // 采集FP和FCP
    this.collectFPAndFCP((fpFcpData) => {
      if (fpFcpData) {
        loadData.push(...fpFcpData);
      }
      // 所有加载指标采集完成后触发回调
      callback(loadData);
    });
  }
};
交互性能指标

交互性能指标核心反映“用户操作到页面响应的耗时”,重点实现INP(替代FID)和长任务监测,其中INP依赖PerformanceObserver监听interaction-record,长任务依赖PerformanceObserver监听longtask

import { CompatibleUtil } from '../utils/compatible';
import { HelperUtil } from '../utils/helper';

/**
 * 交互性能指标采集(INP、长任务)
 * 技术要点:
 * 1. INP:交互下一步延迟,监听interaction-record事件,取第98百分位值(最接近用户真实体验)
 * 2. 长任务:耗时超过50ms的任务,会阻塞主线程,导致页面卡顿,监听longtask事件采集
 * 3. 注意:INP需在页面卸载前计算最终值,长任务需实时采集、实时上报
 */
export const InteractionPerformanceCollector = {
  // 存储所有交互事件的延迟时间,用于计算INP第98百分位值
  private interactionDelays: number[] = [];

  // 采集INP(交互下一步延迟):用户操作后,页面响应的延迟时间
  collectINP(callback: (data: { type: string; value: number; time: number; page: string } | null) => void): void {
    HelperUtil.tryCatchWrapper(() => {
      if (!CompatibleUtil.isSupportPerformanceObserver() || !CompatibleUtil.isSupportPerformanceEntry('interaction-record')) {
        callback(null);
        return;
      }

      const observer = new PerformanceObserver((entries) => {
        entries.getEntries().forEach((entry: PerformanceInteractionTiming) => {
          // 仅采集有效交互(hasResponded为true,且延迟时间大于0)
          if (entry.hasResponded && entry.processingStart - entry.startTime > 0) {
            // 交互延迟时间 = processingStart - startTime(用户操作到开始处理的时间)
            const delay = entry.processingStart - entry.startTime;
            this.interactionDelays.push(delay);
          }
        });
      });

      // 监听interaction-record事件
      observer.observe({ type: 'interaction-record', buffered: true });

      // 页面卸载前,计算INP(第98百分位值),触发回调
      window.addEventListener('beforeunload', () => {
        observer.disconnect();
        if (this.interactionDelays.length === 0) {
          callback(null);
          return;
        }

        // 计算第98百分位值(排序后,取对应索引的值)
        this.interactionDelays.sort((a, b) => a - b);
        const index = Math.ceil(this.interactionDelays.length * 0.98) - 1;
        const inpValue = this.interactionDelays[index];

        const data = {
          type: 'performance_inp',
          value: inpValue, // 单位:ms
          time: Date.now(),
          page: HelperUtil.getPageUrl()
        };

        callback(data);
      });
    });
  },

  // 采集长任务(Long Task):耗时超过50ms的任务,实时上报
  collectLongTask(callback: (data: { type: string; value: number; time: number; page: string; taskName: string } | null) => void): void {
    HelperUtil.tryCatchWrapper(() => {
      if (!CompatibleUtil.isSupportPerformanceObserver() || !CompatibleUtil.isSupportPerformanceEntry('longtask')) {
        callback(null);
        return;
      }

      const observer = new PerformanceObserver((entries) => {
        entries.getEntries().forEach((entry: PerformanceLongTaskTiming) => {
          // 长任务:耗时超过50ms(标准阈值)
          const taskDuration = entry.duration;
          if (taskDuration <= 50) return;

          // 去重:避免同一长任务重复采集
          const deduplicateKey = `longtask_${entry.startTime}_${taskDuration}`;
          if (!HelperUtil.deduplicateData(deduplicateKey)) return;

          // 获取长任务名称(辅助归因,如JS执行、样式计算等)
          const taskName = entry.name || 'unknown_longtask';

          const data = {
            type: 'performance_longtask',
            value: taskDuration, // 单位:ms
            time: Date.now(),
            page: HelperUtil.getPageUrl(),
            taskName
          };

          // 实时触发回调,上报长任务数据
          callback(data);
        });
      });

      // 监听longtask事件,需指定buffered: true(捕获页面加载过程中的长任务)
      observer.observe({ type: 'longtask', buffered: true });

      // 页面卸载时销毁观察者
      window.addEventListener('beforeunload', () => {
        observer.disconnect();
      });
    });
  },

  // 初始化交互性能采集(供外部调用)
  initInteractionCollector(callback: (data: any) => void): void {
    // 采集INP
    this.collectINP((inpData) => {
      if (inpData) {
        callback(inpData);
      }
    });

    // 采集长任务(实时上报)
    this.collectLongTask((longTaskData) => {
      if (longTaskData) {
        callback(longTaskData);
      }
    });
  }
};
渲染性能指标采集

渲染性能重点实现CLS(累积布局偏移),反映页面布局稳定性,避免布局抖动导致的用户体验变差。CLS依赖PerformanceObserver监听layout-shift事件,累积计算偏移值

import { CompatibleUtil } from '../utils/compatible';
import { HelperUtil } from '../utils/helper';

/**
 * 渲染性能指标采集(CLS:累积布局偏移)
 * 技术要点:
 * 1. CLS:累积布局偏移,每次布局偏移(layout-shift)都会产生一个偏移值,累积求和
 * 2. 有效布局偏移:shiftScore > 0.05,且不是由于用户交互(如点击、滚动)触发的
 * 3. 页面卸载前,上报最终的CLS值(累积和)
 */
export const RenderPerformanceCollector = {
  // 存储累积布局偏移值
  private cumulativeCLS: number = 0;

  // 采集CLS(累积布局偏移)
  collectCLS(callback: (data: { type: string; value: number; time: number; page: string } | null) => void): void {
    HelperUtil.tryCatchWrapper(() => {
      if (!CompatibleUtil.isSupportPerformanceObserver() || !CompatibleUtil.isSupportPerformanceEntry('layout-shift')) {
        callback(null);
        return;
      }

      const observer = new PerformanceObserver((entries) => {
        entries.getEntries().forEach((entry: PerformanceLayoutShift) => {
          // 过滤无效布局偏移:偏移值太小(<=0.05)、用户交互触发的偏移
          if (entry.value <= 0.05 || entry.hadRecentInput) {
            return;
          }

          // 累积布局偏移值
          this.cumulativeCLS += entry.value;
        });
      });

      // 监听layout-shift事件
      observer.observe({ type: 'layout-shift', buffered: true });

      // 页面卸载前,上报最终的CLS值
      window.addEventListener('beforeunload', () => {
        observer.disconnect();
        // CLS值保留3位小数,避免精度问题
        const clsValue = Math.round(this.cumulativeCLS * 1000) / 1000;

        // 过滤无效CLS(累积值为0,说明无明显布局偏移)
        if (clsValue <= 0) {
          callback(null);
          return;
        }

        const data = {
          type: 'performance_cls',
          value: clsValue, // 无单位,累积偏移值(0-1,越小越好)
          time: Date.now(),
          page: HelperUtil.getPageUrl()
        };

        callback(data);
      });
    });
  },

  // 初始化渲染性能采集(供外部调用)
  initRenderCollector(callback: (data: any) => void): void {
    this.collectCLS((clsData) => {
      if (clsData) {
        callback(clsData);
      }
    });
  }
};
资源加载性能指标

补充采集页面核心资源(JS、CSS、图片、接口)的加载耗时,辅助性能归因(如某个JS加载过慢导致页面卡顿),资源采集依赖performance.getEntriesByType('resource')

import { CompatibleUtil } from '../utils/compatible';
import { HelperUtil } from '../utils/helper';

/**
 * 资源加载耗时采集(JS、CSS、图片、接口)
 * 技术要点:
 * 1. 利用performance.getEntriesByType('resource')获取所有资源加载记录
 * 2. 过滤核心资源(排除无关资源,如广告、第三方统计),采集加载耗时
 * 3. 接口耗时:前端采集请求开始到响应结束的时间,关联资源加载记录
 */
export const ResourcePerformanceCollector = {
  // 定义需要采集的核心资源类型
  private coreResourceTypes: Array<'script' | 'link' | 'img' | 'xmlhttprequest' | 'fetch'> = [
    'script', 'link', 'img', 'xmlhttprequest', 'fetch'
  ];

  // 采集核心资源加载耗时
  collectResourcePerformance(): Array<{
    type: string;
    resourceType: string;
    resourceUrl: string;
    duration: number;
    time: number;
    page: string;
  }> | null {
    return HelperUtil.tryCatchWrapper(() => {
      if (!CompatibleUtil.isSupportPerformance()) return null;

      // 获取所有资源加载记录
      const resources = window.performance.getEntriesByType('resource') as PerformanceResourceTiming[];
      if (resources.length === 0) return null;

      // 过滤核心资源,采集加载耗时
      const coreResources = resources.filter((resource) => {
        // 过滤核心资源类型
        if (!this.coreResourceTypes.includes(resource.initiatorType as any)) return false;
        // 过滤无关资源(广告、第三方统计,可根据实际业务调整)
        const excludeKeywords = ['ad', 'analytics', 'stat', 'track'];
        return !excludeKeywords.some(keyword => resource.name.includes(keyword));
      });

      // 组装资源加载数据
      const resourceData = coreResources.map((resource) => {
        // 资源加载耗时 = responseEnd - requestStart(请求开始到响应结束的时间)
        const duration = resource.responseEnd - resource.requestStart;

        return {
          type: 'performance_resource',
          resourceType: resource.initiatorType, // 资源类型(script、img等)
          resourceUrl: HelperUtil.getPageUrl(), // 资源URL(脱敏处理)
          duration: duration, // 加载耗时(ms)
          time: Date.now(),
          page: HelperUtil.getPageUrl()
        };
      });

      return resourceData;
    }, null);
  },

  // 采集接口请求耗时(单独封装,可结合axios、fetch拦截器使用)
  collectApiPerformance(apiUrl: string, startTime: number, endTime: number): {
    type: string;
    apiUrl: string;
    duration: number;
    time: number;
    page: string;
  } | null {
    return HelperUtil.tryCatchWrapper(() => {
      // 接口耗时 = 响应结束时间 - 请求开始时间
      const duration = endTime - startTime;

      // 过滤短耗时接口(小于10ms,无采集意义)
      if (duration < 10) return null;

      return {
        type: 'performance_api',
        apiUrl: apiUrl.replace(/\?.*/, ''), // 接口URL(脱敏处理)
        duration: duration, // 接口耗时(ms)
        time: Date.now(),
        page: HelperUtil.getPageUrl()
      };
    }, null);
  }
};

数据清洗

import { HelperUtil } from '../utils/helper';

/**
 * 前端性能数据处理(清洗+格式化)
 * 核心作用:
 * 1. 过滤异常值(如耗时为负数、超出合理范围)
 * 2. 数据格式化(统一单位、时间格式)
 * 3. 敏感信息脱敏(URL、资源地址等)
 * 4. 数据校验(确保数据格式正确,便于后端存储)
 */
export const PerformanceDataCleaner = {
  // 清洗单条性能数据
  cleanSingleData(data: any): any | null {
    if (!data || typeof data !== 'object' || !data.type) {
      return null; // 数据格式错误,过滤
    }

    // 1. 过滤异常值(根据不同指标设置合理范围)
    const cleanedData = this.filterAbnormalData({ ...data });
    if (!cleanedData) return null;

    // 2. 敏感信息脱敏(URL、资源地址等)
    this.desensitizeData(cleanedData);

    // 3. 数据格式化(统一单位、时间格式)
    this.standardizeData(cleanedData);

    // 4. 补充公共字段(如设备信息、浏览器信息,辅助归因)
    this.addCommonFields(cleanedData);

    return cleanedData;
  },

  // 批量清洗性能数据
  cleanBatchData(dataList: any[]): any[] {
    if (!Array.isArray(dataList) || dataList.length === 0) return [];
    return dataList.map((data) => this.cleanSingleData(data)).filter(Boolean);
  },

  // 过滤异常值(核心:根据指标类型设置合理范围)
  private filterAbnormalData(data: any): any | null {
    const { type, value } = data;

    // 数值类型指标,过滤负数、超出合理范围的值
    if (value !== undefined && value !== null && typeof value === 'number') {
      switch (type) {
        case 'performance_ttfb':
          // TTFB合理范围:0-10000ms(超过10s视为异常)
          if (value < 0 || value > 10000) return null;
          break;
        case 'performance_lcp':
        case 'performance_fcp':
        case 'performance_fp':
          // 加载类指标合理范围:0-30000ms(超过30s视为异常)
          if (value < 0 || value > 30000) return null;
          break;
        case 'performance_inp':
          // INP合理范围:0-500ms(超过500ms视为卡顿)
          if (value < 0 || value > 500) return null;
          break;
        case 'performance_longtask':
          // 长任务合理范围:50-10000ms(超过10s视为异常)
          if (value< 50 || value > 10000) return null;
          break;
        case 'performance_cls':
          // CLS合理范围:0-1(超过1视为严重布局偏移)
          if (value < 0 || value > 1) return null;
          break;
        case 'performance_resource':
        case 'performance_api':
          // 资源、接口耗时合理范围:0-30000ms(超过30s视为异常)
          if (value < 0 || value > 30000) return null;
          break;
      }
    }

    return data;
  },

  // 敏感信息脱敏
  private desensitizeData(data: any): void {
    // 1. URL脱敏(过滤query参数)
    if (data.page) {
      data.page = HelperUtil.getPageUrl();
    }
    if (data.resourceUrl) {
      data.resourceUrl = data.resourceUrl.replace(/\?.*/, '');
    }
    if (data.apiUrl) {
      data.apiUrl = data.apiUrl.replace(/\?.*/, '');
    }

    // 2. 设备信息脱敏(如需采集,过滤敏感字段)
    if (data.deviceInfo) {
      delete data.deviceInfo.imei;
      delete data.deviceInfo.imsi;
    }
  },

  // 数据格式化
  private standardizeData(data: any): void {
    // 1. 时间格式化(统一为ISO格式)
    if (data.time && typeof data.time === 'number') {
      data.time = new Date(data.time).toISOString();
    }

    // 2. 数值格式化(保留2位小数,统一单位)
    if (data.value && typeof data.value === 'number') {
      // CLS保留3位小数,其他指标保留2位小数
      data.value = data.type === 'performance_cls' 
        ? Math.round(data.value * 1000) / 1000 
        : Math.round(data.value * 100) / 100;
    }

    // 3. 统一字段类型(避免字符串、数字混用)
    data.page = String(data.page || 'unknown');
    data.type = String(data.type || 'unknown');
  },

  // 补充公共字段(辅助后端分析、问题归因)
  private addCommonFields(data: any): void {
    // 补充浏览器信息
    data.browser = {
      name: navigator.userAgent.includes('Chrome') ? 'Chrome' :
            navigator.userAgent.includes('Firefox') ? 'Firefox' :
            navigator.userAgent.includes('Safari') ? 'Safari' : 'unknown',
      version: navigator.userAgent.match(/Chrome\/(\d+)/)?.[1] ||
               navigator.userAgent.match(/Firefox\/(\d+)/)?.[1] || 'unknown'
    };

    // 补充设备信息(屏幕尺寸)
    data.device = {
      screenWidth: window.screen.width,
      screenHeight: window.screen.height,
      isMobile: /Mobile|Android|iOS/.test(navigator.userAgent)
    };

    // 补充页面加载类型(首次加载/刷新/后退)
    data.loadType = HelperUtil.isFirstLoad() ? 'first_load' : 'reload';
  }
};

数据上报逻辑

核心技术要点:采用“批量上报+离线上报+失败重试”的逻辑,避免数据丢失,同时优化上报性能,不影响页面加载和用户交互

import { HelperUtil } from '../utils/helper';

/**
 * 前端性能数据上报逻辑
 * 技术要点:
 * 1. 批量上报:积累一定数量的数据后批量上报,减少接口请求次数
 * 2. 离线上报:网络异常时,用IndexedDB缓存数据,网络恢复后重传
 * 3. 失败重试:上报失败后,重试3次,每次间隔1s,避免数据丢失
 * 4. 低侵入:用keepalive保证页面卸载时上报成功,不阻塞主线程
 */
export const PerformanceReporter = {
  // 批量上报阈值(积累5条数据后上报)
  private batchThreshold: number = 5;
  // 数据缓存队列(用于批量上报)
  private reportQueue: any[] = [];
  // 上报地址(后端接口,可配置)
  private reportUrl: string = '/api/performance/report'; // 极简后端接口
  // 重试次数上限
  private maxRetryCount: number = 3;

  // 初始化上报器(传入后端上报地址,可配置)
  init(reportUrl?: string): void {
    if (reportUrl) {
      this.reportUrl = reportUrl;
    }

    // 监听网络恢复事件,重传离线缓存的数据
    window.addEventListener('online', () => {
      this.reportOfflineData();
    });

    // 页面卸载前,上报缓存队列中的所有数据
    window.addEventListener('beforeunload', () => {
      if (this.reportQueue.length > 0) {
        this.reportBatchData(this.reportQueue, true);
      }
    });
  },

  // 单个数据上报(加入缓存队列,达到阈值后批量上报)
  reportData(data: any): void {
    if (!data) return;

    // 加入缓存队列
    this.reportQueue.push(data);

    // 达到批量上报阈值,触发批量上报
    if (this.reportQueue.length >= this.batchThreshold) {
      this.reportBatchData([...this.reportQueue]);
      // 清空缓存队列(上报成功后清空,失败后保留)
      this.reportQueue = [];
    }
  },

  // 批量上报数据
  private async reportBatchData(dataList: any[], isUnload: boolean = false): Promise<void> {
    if (dataList.length === 0) return;

    try {
      // 构造上报参数(批量数据,统一格式)
      const reportParams = {
        data: dataList,
        reportTime: new Date().toISOString(),
        page: HelperUtil.getPageUrl()
      };

      // 上报配置:isUnload时,用keepalive保证上报成功(页面卸载时不被中断)
      const fetchConfig: RequestInit = {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(reportParams),
        credentials: 'include', // 携带cookie(如需用户关联)
        keepalive: isUnload // 页面卸载时启用keepalive
      };

      // 发送上报请求
      const response = await fetch(this.reportUrl, fetchConfig);
      const result = await response.json();

      // 上报失败,重试(未达到重试上限,且不是页面卸载场景)
      if (!result.success && !isUnload) {
        await this.retryReport(dataList);
      }
    } catch (err) {
      console.warn('性能数据上报失败,缓存到离线存储', err);
      // 上报失败,缓存到IndexedDB(离线上报)
      this.cacheOfflineData(dataList);
    }
  },

  // 失败重试逻辑
  private async retryReport(dataList: any[], retryCount: number = 1): Promise<void> {
    if (retryCount > this.maxRetryCount) {
      console.warn('性能数据上报重试失败,缓存到离线存储');
      this.cacheOfflineData(dataList);
      return;
    }

    // 每次重试间隔1s
    await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));

    try {
      await this.reportBatchData(dataList);
    } catch (err) {
      // 重试失败,继续重试
      await this.retryReport(dataList, retryCount + 1);
    }
  },

  // 缓存离线数据(IndexedDB,兼容低版本浏览器)
  private cacheOfflineData(dataList: any[]): void {
    // 简化实现:低版本浏览器用localStorage缓存,高版本用IndexedDB
    if (!window.indexedDB && !window.localStorage) {
      console.warn('浏览器不支持离线存储,数据丢失');
      return;
    }

    try {
      // 高版本:IndexedDB缓存(容量大,支持批量存储)
      if (window.indexedDB) {
        const request = window.indexedDB.open('PerformanceMonitorDB', 1);

        request.onupgradeneeded = (event) => {
          const db = (event.target as IDBOpenDBRequest).result;
          // 创建存储对象(存储离线上报数据)
          if (!db.objectStoreNames.contains('offlineReportData')) {
            db.createObjectStore('offlineReportData', { keyPath: 'id', autoIncrement: true });
          }
        };

        request.onsuccess = (event) => {
          const db = (event.target as IDBOpenDBRequest).result;
          const transaction = db.transaction('offlineReportData', 'readwrite');
          const store = transaction.objectStore('offlineReportData');

          // 批量添加缓存数据
          dataList.forEach(data => {
            store.add({ data, cacheTime: new Date().getTime() });
          });

          transaction.oncomplete = () => {
            db.close();
          };
        };
      } else {
        // 低版本:localStorage缓存(容量小,简化实现)
        const existingData = window.localStorage.getItem('perf_offline_data') || '[]';
        const offlineData = JSON.parse(existingData);
        offlineData.push(...dataList);
        // 限制缓存数量(最多100条,避免localStorage溢出)
        if (offlineData.length > 100) {
          offlineData.splice(0, offlineData.length - 100);
        }
        window.localStorage.setItem('perf_offline_data', JSON.stringify(offlineData));
      }
    } catch (err) {
      console.warn('离线数据缓存失败', err);
    }
  },

  // 上报离线缓存的数据(网络恢复时调用)
  private async reportOfflineData(): Promise<void> {
    // 简化实现:分别处理IndexedDB和localStorage缓存
    if (window.indexedDB) {
      const request = window.indexedDB.open('PerformanceMonitorDB', 1);

      request.onsuccess = async (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        const transaction = db.transaction('offlineReportData', 'readwrite');
        const store = transaction.objectStore('offlineReportData');
        const cursor = store.openCursor();

        const offlineDataList: any[] = [];
        cursor.onsuccess = (e) => {
          const cur = (e.target as IDBCursorWithValue).result;
          if (cur) {
            offlineDataList.push(cur.value.data);
            cur.delete(); // 上报成功后删除缓存
            cur.continue();
          }
        };

        transaction.oncomplete = async () => {
          db.close();
          // 批量上报离线数据
          if (offlineDataList.length > 0) {
            await this.reportBatchData(offlineDataList);
          }
        };
      };
    } else if (window.localStorage) {
      const existingData = window.localStorage.getItem('perf_offline_data') || '[]';
      const offlineDataList = JSON.parse(existingData);
      if (offlineDataList.length > 0) {
        // 批量上报离线数据
        await this.reportBatchData(offlineDataList);
        // 上报成功后清空缓存
        window.localStorage.removeItem('perf_offline_data');
      }
    }
  }
};
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose'); // 用于连接MongoDB存储数据
const app = express();

// 中间件:解析JSON请求体
app.use(bodyParser.json());

// 1. 连接MongoDB数据库(简化,实际项目需配置用户名密码、集群)
mongoose.connect('mongodb://localhost:27017/performance-monitor')
  .then(() => console.log('MongoDB连接成功'))
  .catch(err => console.error('MongoDB连接失败', err));

// 2. 定义性能数据模型(简化,仅存储核心字段)
const PerformanceDataSchema = new mongoose.Schema({
  data: Array, // 前端上报的批量性能数据
  reportTime: String, // 上报时间
  page: String, // 上报页面
  createTime: { type: Date, default: Date.now } // 数据创建时间
});

const PerformanceData = mongoose.model('PerformanceData', PerformanceDataSchema);

// 3. 性能数据上报接口(前端调用此接口)
app.post('/api/performance/report', async (req, res) => {
  try {
    const { data, reportTime, page } = req.body;

    // 验证请求参数
    if (!data || !Array.isArray(data) || data.length === 0) {
      return res.json({ success: false, message: '上报数据不能为空' });
    }

    // 存储数据到数据库(仅存储,无复杂处理)
    await PerformanceData.create({ data, reportTime, page });

    // 返回成功响应
    res.json({ success: true, message: '数据上报成功' });
  } catch (err) {
    console.error('性能数据存储失败', err);
    res.json({ success: false, message: '数据上报失败,请重试' });
  }
});

// 启动后端服务
const port = 3000;
app.listen(port, () => {
  console.log(`后端服务启动成功,监听端口${port}`);
});

统一适配器层


// 子包入口index.ts(优化后,低侵入初始化)
import { LoadPerformanceCollector } from './collector/load';
import { InteractionPerformanceCollector } from './collector/interaction';
import { RenderPerformanceCollector } from './collector/render';
import { ResourcePerformanceCollector } from './collector/resource';
import { PerformanceDataCleaner } from './handler/data-clean';
import { PerformanceReporter } from './handler/report';
import { CompatibleUtil } from './utils/compatible';

/**
 * 性能监控入口(低侵入优化版)
 * 优化点:
 * 1. 延迟初始化:等页面load事件触发后,再初始化监控(避免阻塞页面首次渲染)
 * 2. 异步执行:采集、上报逻辑用requestIdleCallback或setTimeout异步执行,不占用主线程
 * 3. 按需加载:可传入配置,按需启用不同指标的采集(避免无用采集消耗性能)
 */
export const initPerformanceMonitor = (options?: {
  reportUrl?: string; // 后端上报地址(可选,默认使用report.ts中的配置)
  enableLoad?: boolean; // 是否启用加载指标采集(默认启用)
  enableInteraction?: boolean; // 是否启用交互指标采集(默认启用)
  enableRender?: boolean; // 是否启用渲染指标采集(默认启用)
  enableResource?: boolean; // 是否启用资源指标采集(默认启用)
}) => {
  // 默认配置
  const config = {
    enableLoad: true,
    enableInteraction: true,
    enableRender: true,
    enableResource: true,
    ...options
  };

  // 1. 延迟初始化:等页面核心资源加载完成(load事件),再启动监控
  window.addEventListener('load', () => {
    // 2. 异步执行:利用requestIdleCallback,在浏览器空闲时初始化,不阻塞主线程
    if (window.requestIdleCallback) {
      window.requestIdleCallback(() => {
        initMonitorCore(config);
      }, { timeout: 3000 }); // 超时兜底:3s内若浏览器无空闲,也强制初始化(避免监控失效)
    } else {
      // 低版本浏览器降级:用setTimeout异步执行(延迟100ms,避开页面渲染高峰)
      setTimeout(() => {
        initMonitorCore(config);
      }, 100);
    }
  });
};

// 监控核心初始化(内部函数,不对外暴露)
const initMonitorCore = (config: any) => {
  // 初始化上报器(对接后端接口)
  PerformanceReporter.init(config.reportUrl);

  // 统一回调:采集到数据后,清洗→上报(复用逻辑,降低冗余)
  const handleCollectedData = (data: any) => {
    if (!data) return;
    // 异步清洗、上报(进一步避免阻塞主线程)
    Promise.resolve().then(() => {
      // 数据清洗
      const cleanedData = Array.isArray(data) 
        ? PerformanceDataCleaner.cleanBatchData(data) 
        : PerformanceDataCleaner.cleanSingleData(data);
      if (!cleanedData || (Array.isArray(cleanedData) && cleanedData.length === 0)) {
        return;
      }
      // 数据上报(单个/批量统一处理)
      if (Array.isArray(cleanedData)) {
        cleanedData.forEach(item => PerformanceReporter.reportData(item));
      } else {
        PerformanceReporter.reportData(cleanedData);
      }
    });
  };

  // 按需初始化各个指标采集器
  if (config.enableLoad) {
    // 加载指标采集
    LoadPerformanceCollector.collectAllLoadPerformance(handleCollectedData);
  }

  if (config.enableInteraction) {
    // 交互指标采集
    InteractionPerformanceCollector.initInteractionCollector(handleCollectedData);
  }

  if (config.enableRender) {
    // 渲染指标采集
    RenderPerformanceCollector.initRenderCollector(handleCollectedData);
  }

  if (config.enableResource) {
    // 资源指标采集(延迟1s,确保所有核心资源加载完成,采集更全面)
    setTimeout(() => {
      const resourceData = ResourcePerformanceCollector.collectResourcePerformance();
      handleCollectedData(resourceData);
    }, 1000);
  }
};

// 对外暴露唯一入口,业务项目仅需调用该函数即可启用监控(低侵入)
export default { initPerformanceMonitor };

打包优化


// rollup.config.js(完善后,体积优化配置)
import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default {
  input: 'src/index.ts', // 子包入口
  output: [
    // 1. ES模块格式(供ES模块项目、monorepo子包引用)
    {
      file: 'dist/performance-monitor.esm.js',
      format: 'esm',
      sourcemap: false // 生产环境关闭sourcemap,减少体积
    },
    // 2. UMD格式(供非ES模块项目、脚本标签引用,低侵入)
    {
      file: 'dist/performance-monitor.umd.js',
      format: 'umd',
      name: 'PerformanceMonitor', // 全局变量名,window.PerformanceMonitor
      sourcemap: false
    }
  ],
  plugins: [
    // 解析Node.js模块(如导入的工具函数)
    resolve(),
    // 转换CommonJS模块为ES模块
    commonjs(),
    // 转换TypeScript为JavaScript
    typescript({
      exclude: 'node_modules/**',
      compilerOptions: {
        target: 'ES5', // 兼容低版本浏览器
        module: 'ESNext',
        removeComments: true // 移除注释,减少体积
      }
    }),
    // 代码压缩(精简变量名、剔除无用代码,核心优化)
    terser({
      compress: {
        drop_console: true, // 剔除console语句(避免监控代码输出日志占用资源)
        drop_debugger: true,
        pure_funcs: ['console.warn'] // 剔除console.warn,进一步精简体积
      },
      mangle: true // 混淆变量名,减少体积
    })
  ],
  external: [], // 不打包外部依赖,确保体积最小
  treeshake: true // 开启tree-shaking,剔除未使用的代码(如未启用的指标采集逻辑)
};

具体使用


// 2. 业务项目入口(如src/main.ts)
import { initPerformanceMonitor } from 'performance-monitor';

// 初始化性能监控
initPerformanceMonitor({
  reportUrl: 'http://localhost:3001/api/performance/report', // 后端上报地址
  enableLoad: true, // 启用加载指标采集
  enableInteraction: true, // 启用交互指标采集
  enableRender: true, // 启用渲染指标采集
  enableResource: true // 启用资源指标采集
});

// 3. 接口耗时采集(结合axios拦截器,补充示例)
import axios from 'axios';
import { ResourcePerformanceCollector } from 'performance-monitor/dist/collector/resource';

// 请求拦截器:记录请求开始时间
axios.interceptors.request.use((config) => {
  config.meta = config.meta || {};
  config.meta.requestStartTime = Date.now();
  return config;
});

// 响应拦截器:记录请求结束时间,采集接口耗时
axios.interceptors.response.use((response) => {
  const { config } = response;
  const requestStartTime = config.meta?.requestStartTime;
  if (requestStartTime) {
    const requestEndTime = Date.now();
    // 采集接口耗时
    const apiData = ResourcePerformanceCollector.collectApiPerformance(
      config.url || '',
      requestStartTime,
      requestEndTime
    );
    // 上报接口耗时数据
    if (apiData) {
      import('performance-monitor/dist/handler/report').then(({ PerformanceReporter }) => {
        PerformanceReporter.reportData(apiData);
      });
    }
  }
  return response;
}, (error) => {
  // 异常请求也采集耗时(可选)
  const { config } = error;
  if (config?.meta?.requestStartTime) {
    const requestEndTime = Date.now();
    const apiData = ResourcePerformanceCollector.collectApiPerformance(
      config.url || '',
      config.meta.requestStartTime,
      requestEndTime
    );
    if (apiData) {
      import('performance-monitor/dist/handler/report').then(({ PerformanceReporter }) => {
        PerformanceReporter.reportData(apiData);
      });
    }
  }
  return Promise.reject(error);
});

error-monitor

核心职责:采集JS语法/运行时错误、Promise错误、资源加载错误、框架错误,还原报错位置,清洗后上报。

架构设计

exception-monitor/  # monorepo下的异常监控子包
├── src/
│   ├── index.ts        # 子包入口(暴露核心API,供业务项目调用)
│   ├── collector/      # 异常采集核心目录(分文件实现每种异常采集)
│   │   ├── js-error.ts # JS语法/运行时异常采集
│   │   ├── resource-error.ts # 资源加载异常采集
│   │   ├── request-error.ts # 接口请求异常采集
│   │   └── promise-error.ts # Promise异常采集
│   ├── handler/        # 数据处理目录(分类、清洗、格式化、上报)
│   │   ├── error-classify.ts # 异常分类与等级判定
│   │   ├── data-clean.ts # 前端数据清洗(过滤无效数据、脱敏)
│   │   └── report.ts   # 数据上报(前端侧实现,对接微量后端)
│   └── utils/          # 工具函数目录(兼容性、辅助函数)
│       ├── compatible.ts # 浏览器兼容性处理(核心工具)
│       └── helper.ts   # 辅助函数(如异常上下文采集、时间格式化)
├── package.json        # 子包配置(声明依赖、入口)
└── rollup.config.js    # 打包配置(适配SDK打包,体积优化)

兼容性工具

判断浏览器是否支持异常采集相关API(如error事件、unhandledrejection事件、fetch API等),做降级处理,避免采集代码报错;同时适配不同浏览器的异常事件差异



/**
 * 前端异常监控兼容性处理工具
 * 核心:判断浏览器是否支持异常采集相关API,提供降级方案,适配浏览器差异
 */
export const CompatibleUtil = {
  // 判断是否支持核心异常采集API(error、unhandledrejection事件)
  isSupportCoreErrorApi(): boolean {
    return !!window && typeof window.addEventListener === 'function' &&
           typeof window.onerror === 'function' &&
           typeof window.onunhandledrejection === 'function';
  },

  // 判断是否支持fetch API(用于监听fetch请求异常)
  isSupportFetch(): boolean {
    return !!window.fetch && typeof window.fetch === 'function';
  },

  // 判断是否支持XMLHttpRequest(用于监听AJAX请求异常)
  isSupportXHR(): boolean {
    return !!window.XMLHttpRequest;
  },

  // 判断是否支持Performance API(用于采集异常发生时的页面性能上下文)
  isSupportPerformance(): boolean {
    return !!window.performance && typeof window.performance === 'object';
  },

  // 降级处理:获取当前时间戳(兼容低版本浏览器)
  getNow(): number {
    if (this.isSupportPerformance()) {
      return window.performance.now(); // 高精度时间戳(相对于页面加载)
    }
    return new Date().getTime(); // 低版本降级为普通时间戳
  },

  // 判断是否为低版本浏览器(IE11及以下),做特殊适配
  isLowVersionBrowser(): boolean {
    const userAgent = navigator.userAgent;
    return userAgent.includes('MSIE') || userAgent.includes('Trident/7.0');
  }
};


import { CompatibleUtil } from './compatible';

/**
 * 辅助工具函数
 * 核心:简化异常采集、处理逻辑,采集异常上下文,处理通用操作
 */
export const HelperUtil = {
  // 异常捕获包装器:避免采集代码报错影响业务代码
  tryCatchWrapper<T extends (...args: any[]) => any>(fn: T, fallback?: ReturnType<T>): ReturnType<T> | undefined {
    try {
      return fn();
    } catch (err) {
      console.warn('异常指标采集异常', err);
      return fallback;
    }
  },

  // 时间格式化:将时间戳(ms)格式化为可读格式(用于调试、日志、上报)
  formatTime(ms: number): string {
    if (ms < 1000) return `${ms.toFixed(2)}ms`;
    if (ms< 60 * 1000) return `${(ms / 1000).toFixed(2)}s`;
    return `${(ms / (60 * 1000)).toFixed(2)}min`;
  },

  // 格式化时间为ISO格式(用于上报,统一时间标准)
  formatIsoTime(ms: number): string {
    return new Date(ms).toISOString();
  },

  // 采集异常上下文信息(辅助问题定位,核心)
  collectErrorContext(): {
    page: string; // 当前页面URL(脱敏)
    location: { pathname: string; hash: string }; // 页面路径、哈希
    userAgent: string; // 浏览器UA
    browser: { name: string; version: string }; // 浏览器信息
    device: { screenWidth: number; screenHeight: number; isMobile: boolean }; // 设备信息
    performance?: { loadTime: number }; // 页面加载时间(可选)
    timestamp: number; // 异常发生时间戳
  } {
    const url = window.location.href;
    const userAgent = navigator.userAgent;

    // 浏览器信息解析
    const browserInfo = this.parseBrowserInfo(userAgent);
    // 设备信息采集
    const deviceInfo = {
      screenWidth: window.screen.width,
      screenHeight: window.screen.height,
      isMobile: /Mobile|Android|iOS/.test(userAgent)
    };
    // 页面加载时间(可选,辅助判断是否因加载问题导致异常)
    const loadTime = CompatibleUtil.isSupportPerformance() 
      ? window.performance.getEntriesByType('navigation')[0]?.loadEventEnd || 0
      : 0;

    return {
      page: url.replace(/\?.*/, ''), // URL脱敏,过滤query参数(避免敏感信息)
      location: {
        pathname: window.location.pathname,
        hash: window.location.hash
      },
      userAgent,
      browser: browserInfo,
      device: deviceInfo,
      performance: { loadTime: loadTime > 0 ? loadTime : 0 },
      timestamp: CompatibleUtil.getNow()
    };
  },

  // 解析浏览器信息(从UA中提取浏览器名称和版本)
  private parseBrowserInfo(userAgent: string): { name: string; version: string } {
    if (userAgent.includes('Chrome')) {
      const version = userAgent.match(/Chrome\/(\d+)/)?.[1] || 'unknown';
      return { name: 'Chrome', version };
    }
    if (userAgent.includes('Firefox')) {
      const version = userAgent.match(/Firefox\/(\d+)/)?.[1] || 'unknown';
      return { name: 'Firefox', version };
    }
    if (userAgent.includes('Safari')) {
      const version = userAgent.match(/Version\/(\d+)/)?.[1] || 'unknown';
      return { name: 'Safari', version };
    }
    if (userAgent.includes('MSIE') || userAgent.includes('Trident/7.0')) {
      const version = userAgent.match(/MSIE (\d+)/)?.[1] || '11';
      return { name: 'IE', version };
    }
    return { name: 'unknown', version: 'unknown' };
  },

  // 敏感信息脱敏(过滤异常信息中的敏感字段,如token、userId)
  desensitizeSensitiveInfo(info: string): string {
    if (!info) return '';
    // 过滤URL中的query敏感参数
    info = info.replace(/token=([^&]*)/gi, 'token=***');
    info = info.replace(/userId=([^&]*)/gi, 'userId=***');
    info = info.replace(/sessionId=([^&]*)/gi, 'sessionId=***');
    // 过滤异常信息中的敏感字符串(可根据业务扩展)
    const sensitiveKeywords = ['token', 'userId', 'sessionId', 'password', 'secret'];
    sensitiveKeywords.forEach(keyword => {
      const reg = new RegExp(`(${keyword})[:=]\\s*[^,;\\s]+`, 'gi');
      info = info.replace(reg, `$1=***`);
    });
    return info;
  },

  // 异常去重:避免同一异常短时间内重复上报(如循环报错)
  deduplicateError(key: string, interval: number = 3000): boolean {
    if (!window.sessionStorage) return true; // 低版本浏览器不做去重
    const cacheKey = `exception_monitor_${key}`;
    const lastReportTime = Number(window.sessionStorage.getItem(cacheKey) || 0);
    const currentTime = CompatibleUtil.getNow();
    
    // 间隔时间内,不允许重复上报
    if (currentTime - lastReportTime < interval) {
      return false;
    }
    
    // 更新最后上报时间
    window.sessionStorage.setItem(cacheKey, currentTime.toString());
    return true;
  }
};

核心采集目标

js-error

JS异常包括语法错误和运行时错误,核心通过window.onerror事件监听(覆盖大部分JS异常),同时适配低版本浏览器的事件差异,补充异常上下文采集,确保异常信息完整。


import { CompatibleUtil } from '../utils/compatible';
import { HelperUtil } from '../utils/helper';

/**
 * JS异常采集(语法错误、运行时错误)
 * 技术要点:
 * 1. 核心监听window.onerror事件,捕获全局JS异常(语法、运行时)
 * 2. 适配低版本浏览器(IE11),处理事件参数差异
 * 3. 采集异常上下文(报错位置、设备、页面信息),辅助定位问题
 * 4. 去重处理:避免同一异常短时间内重复上报(如循环报错)
 */
export const JsErrorCollector = {
  // 存储已采集的异常标识(用于去重)
  private collectedErrorIds: Set<string> = new Set();

  // 初始化JS异常采集(监听window.onerror事件)
  initJsErrorCollector(callback: (data: any) => void): void {
    HelperUtil.tryCatchWrapper(() => {
      if (!CompatibleUtil.isSupportCoreErrorApi()) {
        console.warn('浏览器不支持JS异常采集API,降级不采集');
        return;
      }

      // 监听window.onerror事件,捕获JS异常
      window.onerror = (
        message: string | Event,
        source?: string,
        lineno?: number,
        colno?: number,
        error?: Error
      ) => {
        // 组装异常数据(兼容不同浏览器的参数差异)
        const errorData = this.assembleJsErrorData(message, source, lineno, colno, error);
        if (!errorData) return true; // 无有效数据,阻止默认行为(避免控制台重复打印)

        // 异常去重:基于异常标识,3秒内同一异常仅上报一次
        const errorId = errorData.errorId;
        if (this.collectedErrorIds.has(errorId) || !HelperUtil.deduplicateError(errorId)) {
          return true;
        }

        // 记录已采集的异常标识
        this.collectedErrorIds.add(errorId);
        // 3秒后移除标识(允许再次采集,避免长时间阻止同一种异常)
        setTimeout(() => {
          this.collectedErrorIds.delete(errorId);
        }, 3000);

        // 触发回调,将异常数据传递给处理函数
        callback(errorData);

        // 返回true,阻止浏览器默认行为(避免控制台重复打印异常)
        return true;
      };
    });
  },

  // 组装JS异常数据(兼容不同浏览器的参数差异)
  private assembleJsErrorData(
    message: string | Event,
    source?: string,
    lineno?: number,
    colno?: number,
    error?: Error
  ): any | null {
    // 处理message参数(不同浏览器可能传入Event对象)
    let errorMessage = '';
    if (typeof message === 'string') {
      errorMessage = message;
    } else if (message instanceof Event) {
      errorMessage = message.type || 'JS unknown error';
    } else {
      errorMessage = 'JS unknown error';
    }

    // 处理错误堆栈(优先使用error.stack,更详细)
    let errorStack = error?.stack || '';
    // 脱敏处理(过滤敏感信息)
    errorMessage = HelperUtil.desensitizeSensitiveInfo(errorMessage);
    errorStack = HelperUtil.desensitizeSensitiveInfo(errorStack);

    // 处理source(报错文件路径),脱敏处理
    const errorSource = HelperUtil.desensitizeSensitiveInfo(source || 'unknown');

    // 生成异常唯一标识(用于去重:报错文件+行号+列号+错误信息摘要)
    const errorSummary = errorMessage.slice(0, 50); // 错误信息摘要(前50字符)
    const errorId = `${errorSource}_${lineno || 0}_${colno || 0}_${errorSummary}`;

    // 采集异常上下文信息
    const context = HelperUtil.collectErrorContext();

    return {
      type: 'exception_js', // 异常类型:JS异常
      errorId, // 异常唯一标识(用于去重)
      errorMessage, // 异常信息
      errorSource, // 报错文件路径
      lineno: lineno || 0, // 报错行号
      colno: colno || 0, // 报错列号
      errorStack: errorStack || '无错误堆栈信息', // 错误堆栈(用于定位问题)
      errorType: error?.name || 'UnknownError', // 错误类型(如ReferenceError、TypeError)
      context, // 异常上下文信息
      timestamp: context.timestamp // 异常发生时间戳
    };
  }
};
resource-error

资源加载异常指页面依赖的JS、CSS、图片、字体等资源加载失败,核心通过监听window.addEventListener('error', ..., true)(捕获阶段监听),同时过滤非资源加载异常,确保采集精准。


import { CompatibleUtil } from '../utils/compatible';
import { HelperUtil } from '../utils/helper';

/**
 * 资源加载异常采集(JS、CSS、图片、字体等)
 * 技术要点:
 * 1. 核心:在捕获阶段监听window.error事件(第三个参数为true),捕获资源加载异常
 * 2. 过滤:区分资源加载异常与JS异常(通过target/nodeName判断)
 * 3. 采集:资源URL、资源类型、加载状态、报错信息等,辅助定位资源加载问题
 * 4. 兼容:适配低版本浏览器的事件差异,处理跨域资源加载异常
 */
export const ResourceErrorCollector = {
  // 初始化资源加载异常采集(捕获阶段监听window.error事件)
  initResourceErrorCollector(callback: (data: any) => void): void {
    HelperUtil.tryCatchWrapper(() => {
      if (!CompatibleUtil.isSupportCoreErrorApi()) {
        console.warn('浏览器不支持资源加载异常采集API,降级不采集');
        return;
      }

      // 捕获阶段监听window.error事件,捕获资源加载异常(第三个参数为true)
      window.addEventListener('error', (event: Event) => {
        // 判断是否为资源加载异常(target为Element,且是资源标签)
        const target = event.target as HTMLElement | SVGScriptElement;
        if (!target || !(target.tagName)) {
          return; // 非资源加载异常,跳过
        }

        // 过滤资源标签(script、link、img、video、audio、iframe、img等)
        const resourceTags = ['SCRIPT', 'LINK', 'IMG', 'VIDEO', 'AUDIO', 'IFRAME', 'SOURCE'];
        const tagName = target.tagName.toUpperCase();
        if (!resourceTags.includes(tagName)) {
          return; // 非资源标签,跳过(排除JS异常)
        }

        // 组装资源加载异常数据
        const errorData = this.assembleResourceErrorData(target, tagName, event);
        if (!errorData) return;

        // 异常去重:基于资源URL+标签类型,3秒内同一资源加载异常仅上报一次
        const errorId = `${tagName}_${errorData.resourceUrl}`;
        if (!HelperUtil.deduplicateError(errorId)) {
          return;
        }

        // 触发回调,将异常数据传递给处理函数
        callback(errorData);
      }, true); // 第三个参数为true,在捕获阶段监听(确保能捕获到所有资源加载异常)
    });
  },

  // 组装资源加载异常数据
  private assembleResourceErrorData(target: HTMLElement | SVGScriptElement, tagName: string, event: Event): any | null {
    // 获取资源URL(根据标签类型,获取不同的属性)
    let resourceUrl = '';
    switch (tagName) {
      case 'SCRIPT':
        resourceUrl = (target as HTMLScriptElement).src || '';
        break;
      case 'LINK':
        resourceUrl = (target as HTMLLinkElement).href || '';
        break;
      case 'IMG':
        resourceUrl = (target as HTMLImageElement).src || '';
        break;
      case 'VIDEO':
      case 'AUDIO':
        resourceUrl = (target as HTMLVideoElement | HTMLAudioElement).src || '';
        break;
      case 'SOURCE':
        resourceUrl = (target as HTMLSourceElement).src || '';
        break;
      case 'IFRAME':
        resourceUrl = (target as HTMLIFrameElement).src || '';
        break;
      default:
        resourceUrl = '';
    }

    if (!resourceUrl) return null; // 无资源URL,视为无效异常

    // 脱敏处理(过滤资源URL中的敏感参数)
    resourceUrl = HelperUtil.desensitizeSensitiveInfo(resourceUrl);

    // 确定资源类型(根据标签类型和URL后缀)
    const resourceType = this.getResourceType(tagName, resourceUrl);

    // 采集异常上下文信息
    const context = HelperUtil.collectErrorContext();

    // 组装异常信息
    return {
      type: 'exception_resource', // 异常类型:资源加载异常
      errorId: `${tagName}_${resourceUrl}`, // 异常唯一标识(用于去重)
      resourceUrl, // 资源URL(脱敏)
      resourceType, // 资源类型(如js、css、img等)
      tagName, // 资源标签(如script、img)
      errorMessage: `资源加载失败:${resourceUrl}`, // 异常信息
      timestamp: context.timestamp, // 异常发生时间戳
      context // 异常上下文信息
    };
  },

  // 根据标签类型和URL后缀,确定资源类型
  private getResourceType(tagName: string, resourceUrl: string): string {
    switch (tagName) {
      case 'SCRIPT':
        return 'js';
      case 'LINK':
        return resourceUrl.endsWith('.css') ? 'css' : 'link';
      case 'IMG':
        if (resourceUrl.endsWith('.png')) return 'png';
        if (resourceUrl.endsWith('.jpg') || resourceUrl.endsWith('.jpeg')) return 'jpg';
        if (resourceUrl.endsWith('.gif')) return 'gif';
        if (resourceUrl.endsWith('.svg')) return 'svg';
        return 'img';
      case 'VIDEO':
        return 'video';
      case 'AUDIO':
        return 'audio';
      case 'IFRAME':
        return 'iframe';
      default:
        return 'unknown';
    }
  }
};
request-error

接口请求异常包括AJAX(XMLHttpRequest)和Fetch请求失败,核心通过重写XMLHttpRequest原型方法、监听Fetch请求的catch事件,捕获请求异常,同时采集请求上下文(如请求URL、方法、参数),辅助定位接口问题


import { CompatibleUtil } from '../utils/compatible';
import { HelperUtil } from '../utils/helper';

/**
 * 接口请求异常采集(AJAX、Fetch请求)
 * 技术要点:
 * 1. AJAX请求:重写XMLHttpRequest原型的open、send方法,监听readystatechange事件,捕获异常
 * 2. Fetch请求:重写window.fetch方法,监听Promise的catch事件,捕获请求异常
 * 3. 采集请求上下文(URL、方法、参数、状态码),辅助定位接口问题
 * 4. 过滤无效请求(如取消的请求、跨域预检请求)
 */
export const RequestErrorCollector = {
  // 初始化接口请求异常采集(重写XMLHttpRequest和Fetch方法)
  initRequestErrorCollector(callback: (data: any) => void): void {
    HelperUtil.tryCatchWrapper(() => {
      // 采集AJAX请求异常(重写XMLHttpRequest原型)
      this.overrideXHR(callback);

      // 采集Fetch请求异常(重写window.fetch方法)
      this.overrideFetch(callback);
    });
  },

  // 重写XMLHttpRequest原型,采集AJAX请求异常
  private overrideXHR(callback: (data: any) => void): void {
    if (!CompatibleUtil.isSupportXHR()) {
      console.warn('浏览器不支持XMLHttpRequest,降级不采集AJAX请求异常');
      return;
    }

    // 保存原始的open、send方法
    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;

    // 重写open方法:记录请求信息(URL、方法)
    XMLHttpRequest.prototype.open = function (method: string, url: string) {
      // 给当前XHR实例添加请求信息
      (this as any).requestInfo = {
        method: method.toUpperCase(),
        url: url,
        startTime: CompatibleUtil.getNow()
      };
      // 调用原始open方法
      return originalOpen.apply(this, arguments as any);
    };

    // 重写send方法:监听readystatechange事件,捕获异常
    XMLHttpRequest.prototype.send = function (body?: Document | XMLHttpRequestBodyInit | null) {
      const xhr = this;
      const requestInfo = (xhr as any).requestInfo;
      if (!requestInfo) {
        return originalSend.apply(xhr, arguments as any);
      }

      // 记录请求参数(脱敏处理)
      let requestData = '';
      if (body) {
        requestData = typeof body === 'string' ? body : JSON.stringify(body);
        requestData = HelperUtil.desensitizeSensitiveInfo(requestData);
      }
      requestInfo.requestData = requestData;

      // 监听readystatechange事件,捕获请求完成后的异常
      xhr.addEventListener('readystatechange', () => {
        if (xhr.readyState !== 4) return; // 请求未完成,跳过

        // 判断是否为请求异常(状态码不在200-299范围内,或网络错误)
        const status = xhr.status;
        const statusText = xhr.statusText;
        if (status >= 200 && status < 300) {
          return; // 请求成功,不采集
        }

        // 组装AJAX请求异常数据
        const errorData = this.assembleRequestErrorData(
          'xhr',
          requestInfo,
          status,
          statusText,
          xhr.responseText
        );
        if (errorData) {
          callback(errorData);
        }
      });

      // 监听error事件,捕获网络错误(如断网)
      xhr.addEventListener('error', () => {
        const errorData = this.assembleRequestErrorData(
          'xhr',
          requestInfo,
          0,
          'network error',
          '网络异常,请求失败'
        );
        if (errorData) {
          callback(errorData);
        }
      });

      // 监听abort事件,捕获取消请求(可选,根据业务需求决定是否采集)
      xhr.addEventListener('abort', () => {
        const errorData = this.assembleRequestErrorData(
          'xhr',
          requestInfo,
          0,
          'request aborted',
          '请求被取消'
        );
        if (errorData) {
          callback(errorData);
        }
      });

      // 调用原始send方法
      return originalSend.apply(xhr, arguments as any);
    };
  },

  // 重写window.fetch方法,采集Fetch请求异常
  private overrideFetch(callback: (data: any) => void): void {
    if (!CompatibleUtil.isSupportFetch()) {
      console.warn('浏览器不支持Fetch API,降级不采集Fetch请求异常');
      return;
    }

    // 保存原始的fetch方法
    const originalFetch = window.fetch;

    // 重写window.fetch方法
    window.fetch = function (input: RequestInfo | URL, init?: RequestInit) {
      // 解析请求信息(URL、方法、参数)
      const requestInfo = HelperUtil.tryCatchWrapper(() => {
        let url = '';
        let method = 'GET';
        let requestData = '';

        // 处理input参数(可能是URL字符串或Request对象)
        if (typeof input === 'string') {
          url = input;
        } else if (input instanceof Request) {
          url = input.url;
          method = input.method.toUpperCase();
          // 读取Request对象的body(仅简单处理,复杂场景可扩展)
          if (input.body) {
            input.text().then(body => {
              requestData = HelperUtil.desensitizeSensitiveInfo(body);
            }).catch(() => {
              requestData = '无法读取请求参数';
            });
          }
        }

        // 处理init参数(请求配置)
        if (init) {
          method = init.method?.toUpperCase() || method;
          if (init.body) {
            requestData = typeof init.body === 'string' ? init.body : JSON.stringify(init.body);
            requestData = HelperUtil.desensitizeSensitiveInfo(requestData);
          }
        }

        return {
          method,
          url,
          requestData,
          startTime: CompatibleUtil.getNow()
        };
      }, {});

      if (!requestInfo || !requestInfo.url) {
        return originalFetch.apply(this, arguments as any);
      }

      // 调用原始fetch方法,监听Promise的catch事件,捕获异常
      return originalFetch.apply(this, arguments as any)
        .then(response => {
          // 判断是否为请求异常(状态码不在200-299范围内)
          const status = response.status;
          const statusText = response.statusText;
          if (status >= 200 && status < 300) {
            return response; // 请求成功,返回响应
          }

          // 读取响应内容(脱敏处理)
          return response.text().then(responseText => {
            responseText = HelperUtil.desensitizeSensitiveInfo(responseText);
            // 组装Fetch请求异常数据
            const errorData = RequestErrorCollector.assembleRequestErrorData(
              'fetch',
              requestInfo,
              status,
              statusText,
              responseText
            );
            if (errorData) {
              callback(errorData);
            }
            return response; // 继续返回响应,不影响业务逻辑
          });
        })
        .catch(error => {
          // 捕获Fetch请求的网络异常、取消请求等异常
          const errorMessage = error?.message || 'Fetch request failed';
          const errorData = RequestErrorCollector.assembleRequestErrorData(
            'fetch',
            requestInfo,
            0,
            errorMessage,
            errorMessage
          );
          if (errorData) {
            callback(errorData);
          }
          // 重新抛出异常,不影响业务逻辑的catch处理
          return Promise.reject(error);
        });
    };
  },

  // 组装接口请求异常数据
  private assembleRequestErrorData(
    requestType: 'xhr' | 'fetch',
    requestInfo: any,
    status: number,
    statusText: string,
    responseText: string
  ): any | null {
    const { method, url, requestData, startTime } = requestInfo;
    if (!url) return null;

    // 脱敏处理URL和请求参数
    const requestUrl = HelperUtil.desensitizeSensitiveInfo(url);
    const requestParams = requestData || '无请求参数';

    // 计算请求耗时
    const endTime = CompatibleUtil.getNow();
    const duration = endTime - startTime;

    // 采集异常上下文信息
    const context = HelperUtil.collectErrorContext();

    // 生成异常唯一标识(用于去重:请求类型+URL+方法+状态码)
    const errorId = `${requestType}_${requestUrl}_${method}_${status}`;

    return {
      type: 'exception_request', // 异常类型:接口请求异常
      errorId, // 异常唯一标识(用于去重)
      requestType, // 请求类型(xhr/fetch)
      requestUrl, // 请求URL(脱敏)
      method, // 请求方法(GET/POST等)
      status, // 响应状态码(0表示网络异常)
      statusText, // 状态文本
      requestParams, // 请求参数(脱敏)
      responseText: HelperUtil.desensitizeSensitiveInfo(responseText), // 响应内容(脱敏)
      duration, // 请求耗时(ms)
      timestamp: context.timestamp, // 异常发生时间戳
      context // 异常上下文信息
    };
  }
};
promise-error

Promise异常指未捕获的Promise.reject()异常(如异步请求失败未处理、Promise链缺少catch),核心通过监听window.unhandledrejection事件捕获,同时过滤已处理的Promise异常,确保采集精准。


import { CompatibleUtil } from '../utils/compatible';
import { HelperUtil } from '../utils/helper';

/**
 * Promise异常采集(未捕获的Promise.reject()异常)
 * 技术要点:
 * 1. 核心监听window.unhandledrejection事件,捕获未处理的Promise异常
 * 2. 过滤已处理的Promise异常(通过event.preventDefault()判断)
 * 3. 采集Promise异常的reason(拒绝原因)、堆栈信息,辅助定位问题
 * 4. 适配低版本浏览器,处理事件参数差异
 */
export const PromiseErrorCollector = {
  // 初始化Promise异常采集(监听window.unhandledrejection事件)
  initPromiseErrorCollector(callback: (data: any) => void): void {
    HelperUtil.tryCatchWrapper(() => {
      if (!CompatibleUtil.isSupportCoreErrorApi()) {
        console.warn('浏览器不支持Promise异常采集API,降级不采集');
        return;
      }

      // 监听window.unhandledrejection事件,捕获未处理的Promise异常
      window.onunhandledrejection = (event: PromiseRejectionEvent) => {
        // 过滤已处理的Promise异常(如果业务代码已处理,阻止采集)
        if (event.defaultPrevented) return;

        // 组装Promise异常数据
        const errorData = this.assemblePromiseErrorData(event);
        if (!errorData) return;

        // 异常去重:基于异常标识,3秒内同一异常仅上报一次
        const errorId = errorData.errorId;
        if (!HelperUtil.deduplicateError(errorId)) {
          return;
        }

        // 触发回调,将异常数据传递给处理函数
        callback(errorData);

        // 调用event.preventDefault(),阻止浏览器默认行为(避免控制台打印异常)
        event.preventDefault();
      };
    });
  },

  // 组装Promise异常数据
  private assemblePromiseErrorData(event: PromiseRejectionEvent): any | null {
    // 获取Promise拒绝原因(reason可能是Error对象、字符串等)
    const reason = event.reason;
    if (!reason) return null;

    // 处理拒绝原因,提取异常信息和堆栈
    let errorMessage = '';
    let errorStack = '';
    let errorType = 'UnknownPromiseError';

    if (reason instanceof Error) {
      errorMessage = reason.message || 'Promise rejected';
      errorStack = reason.stack || '';
      errorType = reason.name || 'UnknownPromiseError';
    } else {
      // 如果reason不是Error对象(如Promise.reject('error'))
      errorMessage = typeof reason === 'string' ? reason : JSON.stringify(reason);
      errorStack = '无错误堆栈信息(Promise拒绝原因非Error对象)';
    }

    // 脱敏处理(过滤敏感信息)
    errorMessage = HelperUtil.desensitizeSensitiveInfo(errorMessage);
    errorStack = HelperUtil.desensitizeSensitiveInfo(errorStack);

    // 采集异常上下文信息
    const context = HelperUtil.collectErrorContext();

    // 生成异常唯一标识(用于去重:异常类型+异常信息摘要)
    const errorSummary = errorMessage.slice(0, 50);
    const errorId = `promise_${errorType}_${errorSummary}`;

    return {
      type: 'exception_promise', // 异常类型:Promise异常
      errorId, // 异常唯一标识(用于去重)
      errorMessage, // 异常信息(Promise拒绝原因)
      errorStack, // 错误堆栈(用于定位问题)
      errorType, // 异常类型(如AxiosError、TypeError)
      timestamp: context.timestamp, // 异常发生时间戳
      context // 异常上下文信息
    };
  }
};

异常处理层


/**
 * 前端异常分类与等级判定
 * 核心作用:
 * 1. 细化异常类型(在基础类型上,进一步区分具体异常场景)
 * 2. 判定异常等级(严重、警告、普通),辅助优先级排序
 * 3. 提供异常描述,便于快速理解异常场景
 */
export const ErrorClassifier = {
  // 异常等级枚举(严重>警告>普通)
  ErrorLevel: {
    SEVERE: 'severe', // 严重:影响页面正常运行,用户无法操作(如JS语法错误、核心资源加载失败)
    WARNING: 'warning', // 警告:不影响核心功能,但存在异常(如非核心资源加载失败、接口请求失败)
    NORMAL: 'normal' // 普通:不影响用户使用,仅存在异常日志(如取消请求、轻微Promise异常)
  },

  // 对异常数据进行分类和等级判定
  classifyError(errorData: any): any | null {
    if (!errorData || !errorData.type) return null;

    const { type } = errorData;
    let detailedType = ''; // 细化异常类型
    let level = this.ErrorLevel.NORMAL; // 异常等级
    let description = ''; // 异常描述

    // 根据基础异常类型,细化分类并判定等级
    switch (type) {
      case 'exception_js':
        // JS异常:细化类型,判定等级
        [detailedType, level, description] = this.classifyJsError(errorData);
        break;
      case 'exception_resource':
        // 资源加载异常:细化类型,判定等级
        [detailedType, level, description] = this.classifyResourceError(errorData);
        break;
      case 'exception_request':
        // 接口请求异常:细化类型,判定等级
        [detailedType, level, description] = this.classifyRequestError(errorData);
        break;
      case 'exception_promise':
        // Promise异常:细化类型,判定等级
        [detailedType, level, description] = this.classifyPromiseError(errorData);
        break;
      default:
        detailedType = 'unknown_exception';
        level = this.ErrorLevel.NORMAL;
        description = '未知异常';
    }

    // 补充分类和等级信息,返回处理后的异常数据
    return {
      ...errorData,
      detailedType, // 细化异常类型
      level, // 异常等级
      description // 异常描述
    };
  },

  // 分类JS异常(细化类型,判定等级)
  private classifyJsError(errorData: any): [string, string, string] {
    const { errorType, errorMessage } = errorData;
    // 根据errorType细化JS异常类型
    switch (errorType) {
      case 'ReferenceError':
        return [
          'js_reference_error',
          this.ErrorLevel.SEVERE,
          `引用错误:${errorMessage}(可能是变量未定义、函数未声明)`
        ];
      case 'TypeError':
        return [
          'js_type_error',
          this.ErrorLevel.SEVERE,
          `类型错误:${errorMessage}(可能是变量类型错误、函数调用方式错误)`
        ];
      case 'SyntaxError':
        return [
          'js_syntax_error',
          this.ErrorLevel.SEVERE,
          `语法错误:${errorMessage}(代码编写不符合JS语法规范)`
        ];
      case 'RangeError':
        return [
          'js_range_error',
          this.ErrorLevel.WARNING,
          `范围错误:${errorMessage}(可能是数组越界、参数范围无效)`
        ];
      default:
        return [
          'js_unknown_error',
          this.ErrorLevel.WARNING,
          `未知JS异常:${errorMessage}`
        ];
    }
  },

  // 分类资源加载异常(细化类型,判定等级)
  private classifyResourceError(errorData: any): [string, string, string] {
    const { resourceType, tagName, resourceUrl } = errorData;
    // 根据资源类型,判定等级(核心资源为严重,非核心为警告)
    const isCoreResource = ['js', 'css'].includes(resourceType);
    const level = isCoreResource ? this.ErrorLevel.SEVERE : this.ErrorLevel.WARNING;

    return [
      `resource_${resourceType}_error`,
      level,
      `${tagName}标签资源加载失败,资源类型:${resourceType},URL:${resourceUrl}`
    ];
  },

  // 分类接口请求异常(细化类型,判定等级)
  private classifyRequestError(errorData: any): [string, string, string] {
    const { requestType, status, requestUrl, method } = errorData;
    // 根据状态码,细化类型并判定等级
    if (status === 0) {
      // 状态码为0,通常是网络异常或取消请求
      const isAbort = errorData.statusText.includes('aborted');
      return [
        isAbort ? 'request_aborted' : 'request_network_error',
        isAbort ? this.ErrorLevel.NORMAL : this.ErrorLevel.WARNING,
        `${requestType.toUpperCase()}请求${isAbort ? '被取消' : '网络异常'},URL:${requestUrl},方法:${method}`
      ];
    } else if (status >= 400 && status < 500) {
      // 4xx错误:客户端错误
      return [
        'request_client_error',
        this.ErrorLevel.WARNING,
        `${requestType.toUpperCase()}请求客户端错误,URL:${requestUrl},方法:${method},状态码:${status}`
      ];
    } else if (status >= 500 && status < 600) {
      // 5xx错误:服务器错误
      return [
        'request_server_error',
        this.ErrorLevel.WARNING,
        `${requestType.toUpperCase()}请求服务器错误,URL:${requestUrl},方法:${method},状态码:${status}`
      ];
    } else {
      // 其他状态码异常
      return [
        'request_unknown_error',
        this.ErrorLevel.WARNING,
        `${requestType.toUpperCase()}请求异常,URL:${requestUrl},方法:${method},状态码:${status}`
      ];
    }
  },

  // 分类Promise异常(细化类型,判定等级)
  private classifyPromiseError(errorData: any): [string, string, string] {
    const { errorType, errorMessage } = errorData;
    // 根据errorType细化Promise异常类型
    if (errorType.includes('AxiosError')) {
      return [
        'promise_axios_error',
        this.ErrorLevel.WARNING,
        `Axios请求Promise异常:${errorMessage}`
      ];
    } else if (errorType.includes('FetchError')) {
      return [
        'promise_fetch_error',
        this.ErrorLevel.WARNING,
        `Fetch请求Promise异常:${errorMessage}`
      ];
    } else {
      return [
        'promise_unknown_error',
        this.ErrorLevel.NORMAL,
        `未知Promise异常:${errorMessage}`
      ];
    }
  }
};
export class ErrorMonitor {
  private reportUrl: string;

  constructor(config: { reportUrl: string }) {
    this.reportUrl = config.reportUrl;
  }

  public start() {
    // 采集普通JS错误
    this.collectJsError();
    // 采集Promise错误
    this.collectPromiseError();
    // 采集资源加载错误
    this.collectResourceError();
  }

  // 采集普通JS错误
  private collectJsError() {
    window.onerror = (message, source, lineno, colno, error) => {
      const errorData = {
        type: 'error_js',
        message: String(message), // 错误信息
        source: source || '', // 报错文件
        line: lineno || 0, // 报错行号
        column: colno || 0, // 报错列号
        stack: error?.stack || '', // 错误堆栈(还原报错位置)
        time: new Date().getTime()
      };

      // 数据清洗、上报(逻辑同性能监控子包,此处简化)
      this.reportData(errorData);
      return true; // 阻止浏览器默认报错打印
    };
  }

  // 采集Promise错误(未捕获的reject)
  private collectPromiseError() {
    window.addEventListener('unhandledrejection', (event) => {
      const errorData = {
        type: 'error_promise',
        message: event.reason?.message || 'Promise reject',
        stack: event.reason?.stack || '',
        time: new Date().getTime()
      };
      this.reportData(errorData);
      event.preventDefault(); // 阻止默认行为
    });
  }

  // 采集资源加载错误(JS、CSS、图片等)
  private collectResourceError() {
    window.addEventListener('error', (event) => {
      const target = event.target as HTMLElement;
      if (target.tagName === 'SCRIPT' || target.tagName === 'LINK' || target.tagName === 'IMG') {
        const errorData = {
          type: 'error_resource',
          resourceUrl: target.src || target.href, // 资源地址
          resourceType: target.tagName.toLowerCase(), // 资源类型
          time: new Date().getTime()
        };
        this.reportData(errorData);
      }
    }, true); // 捕获阶段监听,确保能捕获所有资源错误
  }

  private reportData(data: Record<string, any>) {
    // 同性能监控子包的上报逻辑,可复用公共上报方法
  }

  public stop() {
    // 移除所有事件监听,避免内存泄漏
    window.onerror = null;
    window.removeEventListener('unhandledrejection', () => {});
  }
}

behavior-monitor

核心职责:采集用户操作行为(点击、输入、滚动、页面跳转),无需侵入业务代码,轻量化采集,辅助问题归因

export class BehaviorMonitor {
  private reportUrl: string;

  constructor(config: { reportUrl: string }) {
    this.reportUrl = config.reportUrl;
  }

  public start() {
    // 采集用户点击行为(核心)
    this.collectClick();
    // 采集页面跳转行为
    this.collectPageChange();
  }

  // 采集用户点击行为(无侵入,通过事件委托)
  private collectClick() {
    document.addEventListener('click', (event) => {
      const target = event.target as HTMLElement;
      // 组装点击行为数据(仅采集关键信息,避免冗余)
      const behaviorData = {
        type: 'behavior_click',
        target: target.tagName, // 点击元素标签
        targetText: target.textContent?.slice(0, 50) || '', // 点击元素文本(截取前50字)
        page: window.location.href,
        time: new Date().getTime(),
        position: { x: event.clientX, y: event.clientY } // 点击位置
      };

      // 数据清洗、上报(简化)
      this.reportData(behaviorData);
    }, true);
  }

  // 采集页面跳转行为
  private collectPageChange() {
    // 监听hash变化(单页应用)
    window.addEventListener('hashchange', () => {
      this.reportData({
        type: 'behavior_page_change',
        fromPage: window.location.href.split('#')[0] + window.location.hash,
        toPage: window.location.href,
        time: new Date().getTime()
      });
    });
  }

  private reportData(data: Record<string, any>) {
    // 同其他子包上报逻辑
  }

  public stop() {
    // 移除事件监听
    document.removeEventListener('click', () => {}, true);
  }
}

business-monitor

核心职责:提供自定义埋点API,供业务项目采集自定义业务指标(如按钮响应耗时、表单提交成功率),统一格式、统一上报

import { DataClean } from '@monitor/data-clean';

export class BusinessMonitor {
  // 静态方法:自定义业务埋点(业务项目直接调用)
  public static track(
    eventName: string, // 埋点事件名(如:add_cart_click、pay_page_load)
    data: Record<string, any>, // 自定义埋点数据(如:商品ID、耗时)
    config: { reportUrl: string },
    dataClean: DataClean
  ) {
    // 组装业务埋点数据(统一格式,便于后续分析)
    const businessData = {
      type: 'business_track',
      eventName,
      data: data || {},
      page: window.location.href,
      time: new Date().getTime()
    };

    // 数据清洗、上报
    const cleanData = dataClean.clean(businessData);
    this.reportData(cleanData, config.reportUrl);
  }

  private static reportData(data: Record<string, any>, reportUrl: string) {
    // 同其他子包上报逻辑
  }
}

// 业务项目使用示例(集成监控SDK后)
// monitor.trackBusinessEvent('add_cart_click', { goodsId: '123', costTime: 150 });

data-collect

核心职责:统一封装数据采集工具(如兼容性处理、Performance API封装、事件监听工具),供4个业务监控子包调用,避免重复编写采集工具

// 公共数据采集工具类
export class DataCollectUtil {
  // 兼容性判断:是否支持PerformanceObserver
  public static isSupportPerformanceObserver() {
    return !!window.PerformanceObserver;
  }

  // 封装PerformanceObserver监听方法(统一兼容性处理)
  public static observePerformance(type: string, callback: (entries: PerformanceEntryList) => void) {
    if (!this.isSupportPerformanceObserver()) return null;
    const observer = new PerformanceObserver((entries) => {
      callback(entries.getEntries());
    });
    observer.observe({ type, buffered: true });
    return observer;
  }

  // 封装事件监听(统一移除、兼容性处理)
  public static addEventListener(target: Window | Document, event: string, callback: EventListener, useCapture = false) {
    target.addEventListener(event, callback, useCapture);
    // 返回移除监听的方法,避免内存泄漏
    return () => target.removeEventListener(event, callback, useCapture);
  }
}

// 其他子包使用示例:const observer = DataCollectUtil.observePerformance('largest-contentful-paint', (entries) => {});

data-clean

核心职责:统一数据清洗规则(过滤异常值、数据脱敏、格式标准化、去重),所有监控子包的采集数据,均需经过此子包清洗后再上报,确保数据可靠

export class DataClean {
  // 核心方法:清洗数据(所有采集数据均调用此方法)
  public clean(data: Record<string, any>) {
    // 1. 过滤异常值(如耗时为负数、数据为空)
    const cleanData = this.filterAbnormalData({ ...data });
    // 2. 数据脱敏(过滤用户敏感信息,如手机号、密码)
    this.desensitizeData(cleanData);
    // 3. 格式标准化(统一时间格式、数据类型)
    this.standardizeData(cleanData);
    // 4. 去重(简单去重,避免重复上报)
    return this.deduplicateData(cleanData);
  }

  // 过滤异常值
  private filterAbnormalData(data: Record<string, any>) {
    // 示例:过滤耗时异常值(小于0、大于30秒)
    if (data.type.includes('performance') && data.value !== undefined) {
      if (data.value < 0 || data.value > 30000) {
        data.value = null; // 标记为异常,后续上报时过滤
      }
    }
    return data;
  }

  // 数据脱敏(核心:避免采集用户敏感信息)
  private desensitizeData(data: Record<string, any>) {
    // 示例:过滤URL中的用户ID、手机号等敏感信息
    if (data.page) {
      data.page = data.page.replace(/userId=\d+/g, 'userId=***');
    }
    // 示例:过滤自定义数据中的敏感信息
    if (data.data?.phone) {
      data.data.phone = data.data.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
    }
  }

  // 格式标准化
  private standardizeData(data: Record<string, any>) {
    // 统一时间格式(时间戳转为ISO格式)
    data.time = new Date(data.time).toISOString();
    // 统一数据类型(避免字符串、数字混用)
    if (data.value !== undefined) {
      data.value = Number(data.value);
    }
  }

  // 简单去重(基于事件名+时间戳,避免重复上报)
  private deduplicateData(data: Record<string, any>) {
    // 实际落地可结合localStorage/IndexedDB缓存,此处简化
    const key = `${data.type}_${data.time}`;
    if (window.localStorage.getItem(key)) {
      return null; // 重复数据,返回null,不上报
    }
    window.localStorage.setItem(key, '1');
    return data;
  }
}

社区方案

开源方案

核心优势(落地适配性)

核心劣势(落地风险)

适配子包(可集成到哪个子包)

落地建议

web-vitals(Google官方)

轻量级(3KB gzip)、无侵入、仅聚焦性能指标采集,可直接集成到performance-monitor子包,替代手动编写PerformanceObserver逻辑,降低开发成本;兼容性处理完善。

仅支持性能指标采集,无其他功能,需配合其他方案使用;无数据清洗、上报逻辑。

performance-monitor(性能监控子包)

强烈推荐集成,无需重复开发性能指标采集逻辑,直接调用其API采集LCP、INP等核心指标,节省落地时间。

Sentry(开源版)

错误+性能一体化监控,可拆分核心逻辑,其错误采集、堆栈还原功能可集成到error-monitor子包;支持插件化,适配monorepo架构;社区活跃,问题解决快。

体积较大(未压缩≈200KB),需按需拆分核心逻辑,避免引入冗余代码;自主部署维护成本中等。

error-monitor(异常监控子包)、performance-monitor(性能监控子包)

推荐集成其错误采集核心逻辑(无需全量引入),重点复用其堆栈还原、框架错误捕获功能,减少异常监控子包的开发量。

rrweb(用户行为回放)

轻量化、无侵入,行为采集逻辑可直接集成到behavior-monitor子包,无需手动编写大量事件监听代码;还原度高,辅助问题归因。

行为数据体积较大,需配合采样、压缩,避免上报压力;需自己实现数据清洗、上报逻辑。

behavior-monitor(行为监控子包)、visual-panel(可视化面板子包)

推荐集成,重点复用其DOM监听、行为序列化功能,搭配采样和数据清洗,实现轻量化行为监控,辅助落地后的问题定位。

trackjs(开源版)

全类型监控(性能+异常+行为),API简洁,可集成到核心入口子包,统一调度;支持自定义埋点,可复用其business-monitor子包逻辑。

定制化程度较低,难以完全适配本次monorepo子包拆分逻辑;部分高级功能需付费。

monitor-core(核心入口子包)、business-monitor(业务监控子包)

可选集成,仅复用其自定义埋点API逻辑,不推荐全量引入,避免限制子包拆分的灵活性。

核心实现

  1. 核心集成方案:web-vitals(性能采集)+ Sentry核心逻辑(错误采集)+ rrweb(行为采集),分别集成到对应的3个业务监控子包,减少重复开发;

  2. 包管理工具:pnpm workspace(搭建monorepo环境,管理子包依赖);

  3. 打包发布:rollup(子包打包)+ verdaccio(私有仓库发布);

  4. 辅助工具:sourcemap-loader(错误还原)【sourcemap / source-map-js】+ 自研data-clean(数据清洗)+ 自研visual-panel(可视化归因),确保数据可靠、问题可定位

// 使用 webpack 插件上传 sourcemap 到自己的服务器
const fs = require('fs');
const path = require('path');
const axios = require('axios');

class CustomSourceMapUploadPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('UploadSourceMaps', async (stats) => {
      const outputPath = stats.compilation.outputOptions.path;
      const files = fs.readdirSync(outputPath);
      
      for (const file of files) {
        if (file.endsWith('.map')) {
          const sourceMapPath = path.join(outputPath, file);
          const sourceMapContent = fs.readFileSync(sourceMapPath, 'utf8');
          
          await axios.post('https://your-api.com/sourcemaps', {
            filename: file,
            content: JSON.parse(sourceMapContent),
            version: process.env.VERSION,
            buildTime: new Date().toISOString()
          });
          
          // 生产环境删除本地 sourcemap
          if (process.env.NODE_ENV === 'production') {
            fs.unlinkSync(sourceMapPath);
          }
        }
      }
    });
  }
}
const express = require('express');
const { SourceMapConsumer } = require('source-map');
const app = express();

app.post('/api/error/parse', async (req, res) => {
  const { stackTrace, version, file } = req.body;
  
  // 1. 根据 version 和 file 获取对应的 sourcemap
  const sourceMap = await getSourceMapFromStorage(version, file);
  
  // 2. 解析每个堆栈帧
  const parsedStack = [];
  for (const frame of stackTrace) {
    const consumer = await new SourceMapConsumer(sourceMap);
    const original = consumer.originalPositionFor({
      line: frame.line,
      column: frame.column
    });
    
    parsedStack.push({
      source: original.source,
      line: original.line,
      column: original.column,
      name: original.name
    });
    
    consumer.destroy();
  }
  
  res.json({ parsedStack });
});