核心流程
第一步进行获取数据信息: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、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监控(数据为实验室环境,与真实用户场景有差异)

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(性能采集)+ Sentry核心逻辑(错误采集)+ rrweb(行为采集),分别集成到对应的3个业务监控子包,减少重复开发;
包管理工具:pnpm workspace(搭建monorepo环境,管理子包依赖);
打包发布:rollup(子包打包)+ verdaccio(私有仓库发布);
辅助工具: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 });
});