UNPKG

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
'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