web-performance-monitor-sdk
Version:
A modern, lightweight performance monitoring SDK for web applications. Monitor Core Web Vitals (LCP, FCP, FID, CLS, TTFB) with sendBeacon support.
809 lines (802 loc) • 25 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
/**
* 性能指标工具函数
*/
/**
* 根据性能指标值计算评分
*/
function getPerformanceScore(metric, value) {
const thresholds = {
fcp: { good: 1800, poor: 3000 },
lcp: { good: 2500, poor: 4000 },
fid: { good: 100, poor: 300 },
cls: { good: 0.1, poor: 0.25 },
ttfb: { good: 800, poor: 1800 },
};
const threshold = thresholds[metric];
if (!threshold)
return 'good';
if (value <= threshold.good)
return 'good';
if (value <= threshold.poor)
return 'needs-improvement';
return 'poor';
}
/**
* 创建性能指标详情
*/
function createMetricDetail(value, metric) {
return {
value: Math.round(value * 100) / 100, // 保留两位小数
score: getPerformanceScore(metric, value),
timestamp: Date.now(),
};
}
/**
* 检查浏览器是否支持Performance Observer
*/
function isPerformanceObserverSupported() {
return typeof window !== 'undefined' &&
'PerformanceObserver' in window &&
'PerformanceEntry' in window;
}
/**
* 生成随机采样ID
*/
/**
* 根据采样率决定是否采集数据。
*
* @param sampleRate 介于0和1之间的数,表示采样概率
* @returns 是否采样(true为采样,false为跳过)
*/
function shouldSample(sampleRate = 1) {
return Math.random() < sampleRate;
}
/**
* 性能指标收集器
*/
class MetricsCollector {
constructor(config) {
this.metrics = {};
this.observers = [];
this.config = config;
this.init();
}
/**
* 初始化性能监控
*/
init() {
if (!this.shouldCollect())
return;
// 收集FCP (First Contentful Paint)
this.observePaint();
// 收集LCP (Largest Contentful Paint)
this.observeLCP();
// 收集FID (First Input Delay)
this.observeFID();
// 收集CLS (Cumulative Layout Shift)
this.observeCLS();
// 收集TTFB (Time to First Byte)
this.observeTTFB();
}
/**
* 判断是否应该收集数据
*/
shouldCollect() {
return typeof window !== 'undefined' &&
'PerformanceObserver' in window &&
(this.config.sampleRate === undefined || Math.random() < this.config.sampleRate);
}
/**
* 监控Paint指标 (FCP)
*/
observePaint() {
try {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
if (entry.name === 'first-contentful-paint') {
this.metrics.fcp = createMetricDetail(entry.startTime, 'fcp');
this.reportMetric('fcp', this.metrics.fcp);
}
});
});
observer.observe({ entryTypes: ['paint'] });
this.observers.push(observer);
}
catch (error) {
this.log('Failed to observe paint metrics:', error);
}
}
/**
* 监控LCP指标
*/
observeLCP() {
try {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.metrics.lcp = createMetricDetail(lastEntry.startTime, 'lcp');
this.reportMetric('lcp', this.metrics.lcp);
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
this.observers.push(observer);
}
catch (error) {
this.log('Failed to observe LCP metrics:', error);
}
}
/**
* 监控FID指标
*/
observeFID() {
try {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
if (entry.processingStart && entry.startTime) {
const fid = entry.processingStart - entry.startTime;
this.metrics.fid = createMetricDetail(fid, 'fid');
this.reportMetric('fid', this.metrics.fid);
}
});
});
observer.observe({ entryTypes: ['first-input'] });
this.observers.push(observer);
}
catch (error) {
this.log('Failed to observe FID metrics:', error);
}
}
/**
* 监控CLS指标
*/
observeCLS() {
let clsValue = 0;
try {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
});
this.metrics.cls = createMetricDetail(clsValue, 'cls');
this.reportMetric('cls', this.metrics.cls);
});
observer.observe({ entryTypes: ['layout-shift'] });
this.observers.push(observer);
}
catch (error) {
this.log('Failed to observe CLS metrics:', error);
}
}
/**
* 监控TTFB指标
*/
observeTTFB() {
try {
const navigation = performance.getEntriesByType('navigation')[0];
if (navigation) {
const ttfb = navigation.responseStart - navigation.requestStart;
this.metrics.ttfb = createMetricDetail(ttfb, 'ttfb');
this.reportMetric('ttfb', this.metrics.ttfb);
}
}
catch (error) {
this.log('Failed to observe TTFB metrics:', error);
}
}
/**
* 报告单个指标
*/
reportMetric(name, detail) {
if (this.config.debug) {
console.log(`[Performance Monitor] ${name}:`, detail);
}
// 根据配置选择输出方式
switch (this.config.output) {
case 'console':
console.log(`Performance Metric - ${name}:`, detail);
break;
case 'sendBeacon':
this.sendViaBeacon({ [name]: detail });
break;
case 'fetch':
this.sendViaFetch({ [name]: detail });
break;
case 'custom':
if (this.config.customReporter) {
this.config.customReporter({ [name]: detail.value });
}
break;
}
}
/**
* 通过sendBeacon发送数据
*/
sendViaBeacon(data) {
if (!this.config.endpoint || !navigator.sendBeacon)
return;
try {
navigator.sendBeacon(this.config.endpoint, JSON.stringify(data));
}
catch (error) {
this.log('Failed to send via beacon:', error);
}
}
/**
* 通过fetch发送数据
*/
sendViaFetch(data) {
if (!this.config.endpoint)
return;
try {
fetch(this.config.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
keepalive: true,
}).catch((error) => {
this.log('Failed to send via fetch:', error);
});
}
catch (error) {
this.log('Failed to send via fetch:', error);
}
}
/**
* 获取所有收集的指标
*/
getMetrics() {
const result = {};
Object.entries(this.metrics).forEach(([key, detail]) => {
result[key] = detail.value;
});
return result;
}
/**
* 获取详细的指标信息
*/
getDetailedMetrics() {
return { ...this.metrics };
}
/**
* 停止监控
*/
disconnect() {
this.observers.forEach(observer => observer.disconnect());
this.observers = [];
}
/**
* 调试日志
*/
log(message, ...args) {
if (this.config.debug) {
console.log(`[Performance Monitor] ${message}`, ...args);
}
}
}
/**
* 错误监控器
*/
class ErrorTracker {
constructor(config) {
this.errorCount = 0;
this.config = config;
this.init();
}
/**
* 初始化错误监控
*/
init() {
if (!this.config.enableErrorTracking)
return;
// 监控JavaScript错误
window.addEventListener('error', this.handleError.bind(this));
// 监控Promise rejection
window.addEventListener('unhandledrejection', this.handlePromiseRejection.bind(this));
// 监控资源加载错误
window.addEventListener('error', this.handleResourceError.bind(this), true);
}
/**
* 处理JavaScript错误
*/
handleError(event) {
this.errorCount++;
const errorData = {
type: 'javascript-error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href,
};
this.reportError(errorData);
}
/**
* 处理Promise rejection
*/
handlePromiseRejection(event) {
this.errorCount++;
const errorData = {
type: 'promise-rejection',
reason: event.reason?.toString(),
stack: event.reason?.stack,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href,
};
this.reportError(errorData);
}
/**
* 处理资源加载错误
*/
handleResourceError(event) {
const target = event.target;
if (target && (target.tagName === 'IMG' || target.tagName === 'SCRIPT' || target.tagName === 'LINK')) {
this.errorCount++;
const errorData = {
type: 'resource-error',
tagName: target.tagName,
src: target.src || target.href,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href,
};
this.reportError(errorData);
}
}
/**
* 报告错误
*/
reportError(errorData) {
if (this.config.debug) {
console.error('[Performance Monitor] Error:', errorData);
}
// 根据配置选择输出方式
switch (this.config.output) {
case 'console':
console.error('Performance Error:', errorData);
break;
case 'sendBeacon':
this.sendViaBeacon(errorData);
break;
case 'fetch':
this.sendViaFetch(errorData);
break;
case 'custom':
if (this.config.customReporter) {
this.config.customReporter(errorData);
}
break;
}
}
/**
* 通过sendBeacon发送错误数据
*/
sendViaBeacon(data) {
if (!this.config.endpoint || !navigator.sendBeacon)
return;
try {
navigator.sendBeacon(this.config.endpoint, JSON.stringify(data));
}
catch (error) {
console.error('Failed to send error via beacon:', error);
}
}
/**
* 通过fetch发送错误数据
*/
sendViaFetch(data) {
if (!this.config.endpoint)
return;
try {
fetch(this.config.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
keepalive: true,
}).catch((error) => {
console.error('Failed to send error via fetch:', error);
});
}
catch (error) {
console.error('Failed to send error via fetch:', error);
}
}
/**
* 获取错误统计
*/
getErrorCount() {
return this.errorCount;
}
}
/**
* 性能监控主类
*/
class PerformanceMonitor {
constructor(config = {}) {
this.isInitialized = false;
// 验证配置参数
this.validateConfig(config);
this.config = {
output: 'console',
enableErrorTracking: false,
enableResourceTiming: false,
sampleRate: 1,
debug: false,
reportOnUnload: true,
sdkVersion: '1.0.0',
...config,
};
// 生成会话ID
this.sessionId = this.generateSessionId();
this.init();
}
/**
* 验证配置参数
*/
validateConfig(config) {
if (config.sampleRate !== undefined) {
if (typeof config.sampleRate !== 'number' || config.sampleRate < 0 || config.sampleRate > 1) {
throw new Error('sampleRate must be a number between 0 and 1');
}
}
if (config.endpoint !== undefined && typeof config.endpoint !== 'string') {
throw new Error('endpoint must be a string');
}
if (config.customReporter !== undefined && typeof config.customReporter !== 'function') {
throw new Error('customReporter must be a function');
}
}
/**
* 初始化性能监控
*/
init() {
// 检查浏览器支持
if (!isPerformanceObserverSupported()) {
this.log('Performance Observer not supported, skipping initialization');
return;
}
// 跳过某些检查采样率
if (!shouldSample(this.config.sampleRate)) {
this.log('Sampled out, skipping initialization');
return;
}
try {
// 初始化性能指标收集器
this.metricsCollector = new MetricsCollector(this.config);
// 初始化错误追踪器(如果启用)
if (this.config.enableErrorTracking) {
this.errorTracker = new ErrorTracker(this.config);
}
// 设置自动上报
if (this.config.autoReportInterval && this.config.autoReportInterval > 0) {
this.startAutoReport();
}
// 设置页面卸载时上报
if (this.config.reportOnUnload) {
this.setupUnloadReporter();
}
this.isInitialized = true;
this.log('Performance monitoring initialized successfully');
}
catch (error) {
this.log('Failed to initialize performance monitoring:', error);
}
}
/**
* 生成会话ID
*/
generateSessionId() {
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
}
/**
* 获取设备信息
* 只检测主流现代浏览器:Chrome、Safari、Firefox、Edge
*/
getDeviceInfo() {
const ua = navigator.userAgent;
// 设备类型检测
let deviceType = 'desktop';
if (/mobile/i.test(ua)) {
deviceType = 'mobile';
}
else if (/tablet|ipad/i.test(ua)) {
deviceType = 'tablet';
}
// 浏览器检测(优先检测 Edge 和 Chrome,因为 Edge 也包含 Chrome 字符串)
let browser = 'Unknown';
let browserVersion = '';
if (ua.indexOf('Edg') > -1) {
// 新版 Edge(基于 Chromium)
browser = 'Edge';
browserVersion = ua.match(/Edg\/(\d+)/)?.[1] || '';
}
else if (ua.indexOf('Chrome') > -1 && ua.indexOf('Safari') > -1) {
// Chrome(包含 Safari 字符串但不是真正的 Safari)
browser = 'Chrome';
browserVersion = ua.match(/Chrome\/(\d+)/)?.[1] || '';
}
else if (ua.indexOf('Safari') > -1 && ua.indexOf('Chrome') === -1) {
// 真正的 Safari
browser = 'Safari';
browserVersion = ua.match(/Version\/(\d+)/)?.[1] || '';
}
else if (ua.indexOf('Firefox') > -1) {
browser = 'Firefox';
browserVersion = ua.match(/Firefox\/(\d+)/)?.[1] || '';
}
// 操作系统检测
let os = 'Unknown';
if (ua.indexOf('Win') > -1) {
os = 'Windows';
}
else if (ua.indexOf('Mac') > -1) {
os = 'MacOS';
}
else if (ua.indexOf('Linux') > -1 && ua.indexOf('Android') === -1) {
os = 'Linux';
}
else if (ua.indexOf('Android') > -1) {
os = 'Android';
}
else if (/iPad|iPhone|iPod/.test(ua)) {
os = 'iOS';
}
return {
deviceType,
browser,
browserVersion,
os,
userAgent: ua,
};
}
/**
* 获取网络信息
* 只支持现代浏览器的 Network Information API
*/
getNetworkInfo() {
const connection = navigator.connection;
if (!connection) {
return {};
}
return {
connectionType: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt,
};
}
/**
* 获取资源加载时间
*/
getResourceTiming() {
if (!this.config.enableResourceTiming || !window.performance || !window.performance.timing) {
return {};
}
const timing = window.performance.timing;
return {
dnsTime: timing.domainLookupEnd - timing.domainLookupStart,
tcpTime: timing.connectEnd - timing.connectStart,
sslTime: timing.secureConnectionStart ? timing.connectEnd - timing.secureConnectionStart : 0,
requestTime: timing.responseStart - timing.requestStart,
downloadTime: timing.responseEnd - timing.responseStart,
domParseTime: timing.domInteractive - timing.domLoading,
domContentLoadedTime: timing.domContentLoadedEventEnd - timing.navigationStart,
loadTime: timing.loadEventEnd - timing.navigationStart,
};
}
/**
* 启动自动上报
*/
startAutoReport() {
if (this.autoReportTimer) {
clearInterval(this.autoReportTimer);
}
this.autoReportTimer = window.setInterval(() => {
this.reportMetrics();
}, this.config.autoReportInterval);
this.log('Auto report started with interval:', this.config.autoReportInterval);
}
/**
* 设置页面卸载时上报
*/
setupUnloadReporter() {
// 使用 visibilitychange 和 pagehide 事件
const reportOnHidden = () => {
if (document.visibilityState === 'hidden') {
this.reportMetrics();
}
};
document.addEventListener('visibilitychange', reportOnHidden);
window.addEventListener('pagehide', () => this.reportMetrics());
this.log('Unload reporter setup complete');
}
/**
* 获取当前性能指标
*/
getMetrics() {
if (!this.metricsCollector) {
this.log('Metrics collector not initialized');
return {};
}
return this.metricsCollector.getMetrics();
}
/**
* 获取详细的性能指标信息
*/
getDetailedMetrics() {
if (!this.metricsCollector) {
this.log('Metrics collector not initialized');
return {};
}
return this.metricsCollector.getDetailedMetrics();
}
/**
* 获取错误统计
*/
getErrorCount() {
if (!this.errorTracker) {
return 0;
}
return this.errorTracker.getErrorCount();
}
/**
* 手动报告性能指标
*/
reportMetrics() {
const metrics = this.getDetailedMetrics();
if (Object.keys(metrics).length === 0) {
this.log('No metrics to report');
return;
}
// 增强数据:添加页面信息、设备信息、网络信息等
const enrichedMetrics = {
...metrics,
// 页面信息
url: window.location.href,
pageTitle: document.title,
// 设备和浏览器信息
...this.getDeviceInfo(),
// 网络信息
...this.getNetworkInfo(),
// 资源加载时间
...this.getResourceTiming(),
// 用户信息
userId: this.config.userId,
// 错误信息
errorCount: this.errorTracker?.getErrorCount() || 0,
// 元数据
sessionId: this.sessionId,
sdkVersion: this.config.sdkVersion,
timestamp: Date.now(),
metadata: this.config.customMetadata ? JSON.stringify(this.config.customMetadata) : undefined,
};
switch (this.config.output) {
case 'console':
console.log('Performance Metrics Report:', enrichedMetrics);
break;
case 'sendBeacon':
this.sendViaBeacon(enrichedMetrics);
break;
case 'fetch':
this.sendViaFetch(enrichedMetrics);
break;
case 'custom':
if (this.config.customReporter) {
this.config.customReporter(enrichedMetrics);
}
break;
}
}
/**
* 停止监控
*/
disconnect() {
if (this.metricsCollector) {
this.metricsCollector.disconnect();
}
// 清理自动上报定时器
if (this.autoReportTimer) {
clearInterval(this.autoReportTimer);
this.autoReportTimer = undefined;
}
this.isInitialized = false;
this.log('Performance monitoring disconnected');
}
/**
* 检查是否已初始化
*/
isReady() {
return this.isInitialized;
}
/**
* 更新配置
*/
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
this.log('Configuration updated:', newConfig);
}
/**
* 通过sendBeacon发送数据
* sendBeacon 是最可靠的方式,即使在页面卸载时也能发送数据
*/
sendViaBeacon(data) {
if (!this.config.endpoint) {
this.log('Endpoint not configured');
return;
}
if (!navigator.sendBeacon) {
this.log('sendBeacon not available, falling back to fetch');
this.sendViaFetch(data);
return;
}
try {
// sendBeacon 发送 JSON 数据
// 创建 Blob 对象以确保正确的 Content-Type
const blob = new Blob([JSON.stringify(data)], {
type: 'application/json',
});
const success = navigator.sendBeacon(this.config.endpoint, blob);
if (success) {
this.log('Data sent via sendBeacon successfully');
}
else {
this.log('sendBeacon failed, data might be too large or endpoint unavailable');
// 如果 sendBeacon 失败,尝试使用 fetch 作为备用方案
this.sendViaFetch(data);
}
}
catch (error) {
this.log('Failed to send via beacon:', error);
// 如果出现异常,尝试使用 fetch 作为备用方案
this.sendViaFetch(data);
}
}
/**
* 通过fetch发送数据
*/
sendViaFetch(data) {
if (!this.config.endpoint) {
this.log('Endpoint not configured');
return;
}
try {
fetch(this.config.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
keepalive: true,
}).then(() => {
this.log('Data sent via fetch');
}).catch((error) => {
this.log('Failed to send via fetch:', error);
});
}
catch (error) {
this.log('Failed to send via fetch:', error);
}
}
/**
* 调试日志
*/
log(message, ...args) {
if (this.config.debug) {
console.log(`[Performance Monitor] ${message}`, ...args);
}
}
}
exports.PerformanceMonitor = PerformanceMonitor;
exports.default = PerformanceMonitor;
//# sourceMappingURL=index.js.map