@dmhsq_monitor/web
Version:
Web monitoring package for monitor system
636 lines (628 loc) • 19 kB
JavaScript
'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