UNPKG

@dmhsq_monitor/web

Version:
636 lines (628 loc) 19 kB
'use strict'; var core = require('@dmhsq_monitor/core'); var processor = require('@dmhsq_monitor/processor'); require('@dmhsq_monitor/utils'); /** * 事件类型常量 */ const EVENT_TYPES = { // 错误类型 JS_ERROR: 'js_error', RESOURCE_ERROR: 'resource_error', UNHANDLED_REJECTION: 'unhandled_rejection', HTTP_ERROR: 'http_error', CONSOLE_ERROR: 'console_error', // 性能类型 PAGE_LOAD: 'page_load', PAGE_VISIBILITY: 'page_visibility', RESOURCE_PERFORMANCE: 'resource_performance', LARGEST_CONTENTFUL_PAINT: 'largest_contentful_paint', FIRST_INPUT_DELAY: 'first_input_delay', CUMULATIVE_LAYOUT_SHIFT: 'cumulative_layout_shift', // 用户行为类型 CLICK: 'click', PAGE_VIEW: 'page_view', CUSTOM: 'custom' }; /** * Web监控版本 */ const WEB_MONITOR_VERSION = '1.0.25'; /** * 默认配置 */ const DEFAULT_CONFIG = { enableAutoErrorCapture: true, enablePerformanceMonitoring: true, enableRequestMonitoring: true, enableBehaviorMonitoring: false, enableRouteMonitoring: true, enableConsoleMonitoring: false, enablePerformance: true, enableError: true, errorSamplingRate: 1.0, behaviorSamplingRate: 0.5, maxCache: 100, reportInterval: 5000 }; /** * 最大内容长度 (用于截断收集的数据) */ const MAX_CONTENT_LENGTH = 2000; /** * 内容采样率 (特定内容的采样) */ const CONTENT_SAMPLING_RATES = { ERROR_STACK: 1.0, CONSOLE_LOGS: 0.1, REQUEST_BODY: 0.1, RESPONSE_BODY: 0.1, INPUT_VALUES: 0.1 }; /** * 检查是否应该采样 * @param rate 采样率 (0-1) * @returns 是否采样 */ function shouldSample(rate) { return Math.random() <= rate; } /** * 判断是否忽略错误 * @param message 错误消息 * @param ignorePatterns 忽略模式 * @returns 是否忽略 */ function shouldIgnoreError(message, ignorePatterns = []) { if (!message) return false; return ignorePatterns.some(pattern => { if (pattern instanceof RegExp) { return pattern.test(message); } return message.includes(pattern); }); } /** * 判断是否忽略URL * @param url URL字符串 * @param ignorePatterns 忽略模式 * @returns 是否忽略 */ function shouldIgnoreUrl(url, ignorePatterns = []) { if (!url) return false; return ignorePatterns.some(pattern => pattern.test(url)); } /** * 错误监控 */ class ErrorMonitor { constructor(monitor, config) { this.isInstalled = false; this.originalOnError = null; this.originalOnUnhandledRejection = null; this.monitor = monitor; this.config = config; this.originalOnError = null; this.originalOnUnhandledRejection = null; this.originalConsoleError = console.error; } /** * 初始化错误监控 */ install() { if (this.isInstalled) return; // 注册JS错误捕获 this.setupJsErrorHandler(); // 注册Promise错误捕获 this.setupPromiseErrorHandler(); // 注册资源加载错误捕获 this.setupResourceErrorHandler(); // 注册控制台错误捕获 if (this.config.enableConsoleMonitoring) { this.setupConsoleErrorHandler(); } this.isInstalled = true; } /** * 卸载错误监控 */ uninstall() { if (!this.isInstalled) return; // 恢复原始error处理函数 if (this.originalOnError !== null) { window.onerror = this.originalOnError; this.originalOnError = null; } // 恢复原始unhandledrejection处理函数 if (this.originalOnUnhandledRejection !== null) { window.removeEventListener('unhandledrejection', this.handleUnhandledRejection); this.originalOnUnhandledRejection = null; } // 移除资源错误监听 window.removeEventListener('error', this.handleResourceError, true); // 恢复原始console.error函数 if (this.config.enableConsoleMonitoring) { console.error = this.originalConsoleError; } this.isInstalled = false; } /** * 设置JS错误处理函数 */ setupJsErrorHandler() { this.originalOnError = window.onerror; window.onerror = (message, source, lineno, colno, error) => { // 首先调用原始的处理函数 if (this.originalOnError) { this.originalOnError.call(window, message, source, lineno, colno, error); } this.handleJsError(message, source, lineno, colno, error); }; } /** * 处理JS错误 */ handleJsError(message, source, lineno, colno, error) { // 采样控制 if (!shouldSample(this.config.errorSamplingRate || 1.0)) return; const errorMessage = (error === null || error === void 0 ? void 0 : error.message) || (typeof message === 'string' ? message : 'Unknown error'); // 检查是否应该忽略该错误 if (shouldIgnoreError(errorMessage, this.config.ignoreErrors)) return; if (source && shouldIgnoreUrl(source, this.config.ignoreUrls)) return; const errorInfo = { message: errorMessage, name: (error === null || error === void 0 ? void 0 : error.name) || 'Error', stack: error === null || error === void 0 ? void 0 : error.stack, url: source || window.location.href, timestamp: Date.now(), userAgent: navigator.userAgent }; // 上报错误 this.monitor.report({ type: core.EventType.ERROR, name: EVENT_TYPES.JS_ERROR, data: errorInfo }); } /** * 设置Promise错误处理函数 */ setupPromiseErrorHandler() { this.handleUnhandledRejection = this.handleUnhandledRejection.bind(this); window.addEventListener('unhandledrejection', this.handleUnhandledRejection); } /** * 处理未捕获的Promise错误 */ handleUnhandledRejection(event) { // 采样控制 if (!shouldSample(this.config.errorSamplingRate || 1.0)) return; event.preventDefault(); const reason = event.reason; let message = 'Promise rejected'; let stack = ''; let name = 'UnhandledRejection'; if (reason instanceof Error) { message = reason.message; stack = reason.stack || ''; name = reason.name; } else if (typeof reason === 'string') { message = reason; } else if (reason !== null && typeof reason === 'object') { try { message = JSON.stringify(reason); } catch (e) { message = 'Unserializable promise rejection reason'; } } // 检查是否应该忽略该错误 if (shouldIgnoreError(message, this.config.ignoreErrors)) return; const errorInfo = { message, stack, name, type: 'unhandledrejection', url: window.location.href, timestamp: Date.now() }; // 上报错误 this.monitor.report({ type: core.EventType.ERROR, name: EVENT_TYPES.UNHANDLED_REJECTION, data: errorInfo }); } /** * 设置资源加载错误处理函数 */ setupResourceErrorHandler() { this.handleResourceError = this.handleResourceError.bind(this); window.addEventListener('error', this.handleResourceError, true); } /** * 处理资源加载错误 */ handleResourceError(event) { // 采样控制 if (!shouldSample(this.config.errorSamplingRate || 1.0)) return; // 跳过JS错误(由onerror处理) if (!event.target || !event.target.tagName) { return; } const target = event.target; const tagName = target.tagName.toLowerCase(); // 只处理加载资源的标签 if (!['img', 'link', 'script', 'audio', 'video'].includes(tagName)) { return; } const src = target.src || target.href || ''; if (this.shouldIgnoreUrl(src)) { return; } const errorInfo = { tagName, src, href: target.href, outerHTML: target.outerHTML.slice(0, 200), // 限制长度 url: window.location.href, timestamp: Date.now() }; // 上报错误 this.monitor.report({ type: core.EventType.ERROR, name: EVENT_TYPES.RESOURCE_ERROR, data: errorInfo }); } /** * 设置控制台错误处理函数 */ setupConsoleErrorHandler() { this.handleConsoleError = this.handleConsoleError.bind(this); // 保存原始console.error函数 this.originalConsoleError = console.error; // 重写console.error函数 console.error = (...args) => { // 调用原始控制台错误函数 this.originalConsoleError.apply(console, args); // 处理错误 this.handleConsoleError(...args); }; } /** * 处理控制台错误 */ handleConsoleError(...args) { // 采样控制 if (!shouldSample(this.config.errorSamplingRate || 1.0)) return; const message = args.map(arg => { if (arg instanceof Error) { return arg.message; } else if (typeof arg === 'object') { try { return JSON.stringify(arg); } catch (e) { return String(arg); } } else { return String(arg); } }).join(' '); // 检查是否应该忽略该错误 if (shouldIgnoreError(message, this.config.ignoreErrors)) return; // 上报错误 this.monitor.report({ type: core.EventType.ERROR, name: EVENT_TYPES.CONSOLE_ERROR, data: { message, name: 'ConsoleError', timestamp: Date.now(), args: args.map(String).join(', ') } }); } /** * 检查是否应该忽略该URL */ shouldIgnoreUrl(url) { if (!url) return false; if (!this.config.ignoreUrls || !this.config.ignoreUrls.length) { return false; } return this.config.ignoreUrls.some(pattern => pattern.test(url)); } } /** * 性能监控 */ class PerformanceMonitor { constructor(monitor, config) { this.isInstalled = false; this.originalOnLoad = null; this.visibilityChangeHandler = null; this.monitor = monitor; this.config = config; this.originalOnLoad = null; } /** * 初始化性能监控 */ install() { if (this.isInstalled) return; // 检查是否启用了性能监控 if (this.config.enablePerformance === false && this.config.enablePerformanceMonitoring === false) { return; } // 注册页面加载性能收集 this.setupLoadPerformanceMonitor(); // 注册页面可见性变化监听 this.setupVisibilityChangeMonitor(); this.isInstalled = true; } /** * 卸载性能监控 */ uninstall() { if (!this.isInstalled) return; // 移除页面可见性监听 if (this.visibilityChangeHandler) { document.removeEventListener('visibilitychange', this.visibilityChangeHandler); this.visibilityChangeHandler = null; } this.isInstalled = false; } /** * 设置页面加载性能监控 */ setupLoadPerformanceMonitor() { // 再次检查性能监控是否启用 if (this.config.enablePerformance === false && this.config.enablePerformanceMonitoring === false) { return; } // 如果页面已加载,则立即收集 if (document.readyState === 'complete') { this.collectPageLoadPerformance(); } else { // 否则等待页面加载完成 window.addEventListener('load', () => { // 延迟一点收集,确保所有性能数据都可用 setTimeout(() => this.collectPageLoadPerformance(), 0); }); } } /** * 收集页面加载性能数据 */ collectPageLoadPerformance() { // 再次检查性能监控是否启用 if (this.config.enablePerformance === false && this.config.enablePerformanceMonitoring === false) { return; } // 采样控制 const samplingRate = this.config.errorSamplingRate || 1.0; if (!shouldSample(samplingRate)) return; // 使用Performance API获取性能数据 if (!window.performance || !window.performance.timing) { return; } const timing = performance.getEntriesByType('navigation')[0]; // 获取来源链接 let sourceLink = ''; try { if (document.referrer) { const referrerUrl = new URL(document.referrer); // 如果是同域名,只记录路径 if (referrerUrl.hostname === window.location.hostname) { sourceLink = referrerUrl.pathname; } else { // 如果是不同域名,记录完整URL sourceLink = document.referrer; } } } catch (e) { // URL解析失败时,使用完整的referrer sourceLink = document.referrer || ''; } const performanceData = { loadTime: Number((timing.loadEventEnd - timing.startTime).toFixed(3)), domReadyTime: Number((timing.domComplete - timing.domInteractive).toFixed(3)), redirectTime: Number((timing.redirectEnd - timing.redirectStart).toFixed(3)), dnsTime: Number((timing.domainLookupEnd - timing.domainLookupStart).toFixed(3)), tcpTime: Number((timing.connectEnd - timing.connectStart).toFixed(3)), ttfb: Number((timing.responseStart - timing.requestStart).toFixed(3)), responseTime: Number((timing.responseEnd - timing.responseStart).toFixed(3)), domContentLoadedTime: Number((timing.domContentLoadedEventEnd - timing.startTime).toFixed(3)), url: window.location.href, referrer: document.referrer || 'direct', entryType: document.referrer ? 'navigation' : 'direct', sourceLink }; // 收集首次绘制和首次内容绘制时间 (如果可用) this.collectPaintTimings(performanceData); // 上报性能数据 this.monitor.report({ type: core.EventType.PERFORMANCE, name: EVENT_TYPES.PAGE_LOAD, data: performanceData }); } /** * 收集绘制时间指标 * @param performanceData 性能数据对象 */ collectPaintTimings(performanceData) { if (window.performance && 'getEntriesByType' in window.performance) { const paintMetrics = window.performance.getEntriesByType('paint'); paintMetrics.forEach(entry => { const { name, startTime } = entry; if (name === 'first-paint') { performanceData.firstPaint = Math.round(startTime); } else if (name === 'first-contentful-paint') { performanceData.firstContentfulPaint = Math.round(startTime); } }); } } /** * 设置页面可见性变化监控 */ setupVisibilityChangeMonitor() { // 检查是否启用了页面可见性统计 if (!this.config.enablePageVisibility) { return; } this.visibilityChangeHandler = () => { const isVisible = document.visibilityState === 'visible'; this.monitor.report({ type: core.EventType.PERFORMANCE, name: EVENT_TYPES.PAGE_VISIBILITY, data: { isVisible, timestamp: Date.now(), url: window.location.href } }); }; document.addEventListener('visibilitychange', this.visibilityChangeHandler); } } /** * Web监控实例 */ class WebMonitor { /** * 创建Web监控实例 * @param config 配置 */ constructor(config) { this.errorMonitor = null; this.performanceMonitor = null; this.isStarted = false; // 处理显式禁用功能的情况 const processedConfig = { ...DEFAULT_CONFIG, ...config }; // 确保配置的一致性 if (config.enablePerformance === false) { processedConfig.enablePerformance = false; processedConfig.enablePerformanceMonitoring = false; } if (config.enableError === false) { processedConfig.enableError = false; processedConfig.enableAutoErrorCapture = false; } this.config = processedConfig; this.monitor = new core.Monitor(); // 立即初始化处理器,确保在其他操作前配置好 this.processor = new processor.Processor(); this.processor.updateConfig({ enableDeduplicate: this.config.enableDeduplicate !== false, collectGeoInfo: this.config.collectGeoInfo === true, mergeSimilarErrors: this.config.mergeSimilarErrors !== false, customProcessors: [] }); if (processedConfig.debug) { console.log('[WebMonitor] Processor initialized with config:', this.processor.getConfig()); } } /** * 初始化并启动监控 */ start() { if (this.isStarted) return; // 初始化核心监控 this.monitor.init(this.config); // 注册事件处理钩子 - 确保在上报前进行数据处理 const originalOnReport = this.config.onReport; this.config.onReport = async (events, context) => { try { // 使用处理器处理事件 const processedEvents = await this.processor.batchProcess(events, context); // 如果有自定义上报回调,则调用 if (typeof originalOnReport === 'function') { return originalOnReport(processedEvents, context); } return processedEvents; } catch (error) { console.error('[WebMonitor] Error processing events:', error); // 出错时仍然返回原始事件,确保数据不丢失 return typeof originalOnReport === 'function' ? originalOnReport(events, context) : events; } }; this.monitor.start(); // 初始化错误监控 if (this.config.enableError !== false && this.config.enableAutoErrorCapture !== false) { this.errorMonitor = new ErrorMonitor(this.monitor, this.config); this.errorMonitor.install(); } // 初始化性能监控,只有当明确启用时才初始化 if (this.config.enablePerformance === true && this.config.enablePerformanceMonitoring !== false) { this.performanceMonitor = new PerformanceMonitor(this.monitor, this.config); this.performanceMonitor.install(); } this.isStarted = true; } /** * 停止监控 */ stop() { if (!this.isStarted) return; // 停止错误监控 if (this.errorMonitor) { this.errorMonitor.uninstall(); this.errorMonitor = null; } // 停止性能监控 if (this.performanceMonitor) { this.performanceMonitor.uninstall(); this.performanceMonitor = null; } // 停止核心监控 this.monitor.stop(); this.isStarted = false; } /** * 手动上报事件 * @param event 事件对象 */ report(event) { this.monitor.report(event); } /** * 获取核心监控实例 */ getMonitor() { return this.monitor; } /** * 获取处理器实例 */ getProcessor() { return this.processor; } /** * 手动清理处理器的重复事件缓存 */ clearDeduplicationCache() { // 通过公开的API调用处理器的清理方法 const cleanupFn = this.processor._cleanupEventHashes; if (typeof cleanupFn === 'function') { cleanupFn(); } } } exports.CONTENT_SAMPLING_RATES = CONTENT_SAMPLING_RATES; exports.DEFAULT_CONFIG = DEFAULT_CONFIG; exports.EVENT_TYPES = EVENT_TYPES; exports.ErrorMonitor = ErrorMonitor; exports.MAX_CONTENT_LENGTH = MAX_CONTENT_LENGTH; exports.PerformanceMonitor = PerformanceMonitor; exports.WEB_MONITOR_VERSION = WEB_MONITOR_VERSION; exports.WebMonitor = WebMonitor; //# sourceMappingURL=index.cjs.js.map