web-see-monitor
Version:
前端监控 SDK,包含性能监控、错误监控和用户行为监控
287 lines (279 loc) • 9.37 kB
JavaScript
'use strict';
class Monitor {
constructor(config) {
var _a, _b, _c, _d;
this.plugins = new Map();
this.cache = [];
this.timer = null;
this.MAX_CACHE_LENGTH = 10;
this.REPORT_INTERVAL = 5000; // 5秒
this.config = {
...config,
enable: (_a = config.enable) !== null && _a !== void 0 ? _a : true,
sampling: (_b = config.sampling) !== null && _b !== void 0 ? _b : 1,
maxCache: (_c = config.maxCache) !== null && _c !== void 0 ? _c : this.MAX_CACHE_LENGTH,
reportInterval: (_d = config.reportInterval) !== null && _d !== void 0 ? _d : this.REPORT_INTERVAL,
};
this.init();
}
init() {
if (!this.config.enable)
return;
// 初始化插件
if (this.config.plugins) {
this.config.plugins.forEach(Plugin => {
const instance = new Plugin();
instance.setReport(this.addCache.bind(this));
instance.init();
this.plugins.set(instance.name, instance);
});
}
// 页面卸载前发送剩余数据
window.addEventListener('unload', () => {
this.report(true);
});
}
destroy() {
this.plugins.forEach(plugin => plugin.destroy());
this.plugins.clear();
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
addCache(data) {
// 采样
if (Math.random() > this.config.sampling)
return;
this.cache.push({
...data,
timestamp: Date.now(),
});
if (this.cache.length >= this.config.maxCache) {
this.report();
}
else if (!this.timer) {
this.timer = setTimeout(() => this.report(), this.config.reportInterval);
}
}
report(isUnload = false) {
if (this.cache.length === 0)
return;
const data = this.cache.slice();
this.cache = [];
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
const reportData = {
appId: this.config.appId,
userId: this.config.userId,
userAgent: navigator.userAgent,
data
};
// 页面卸载前使用 sendBeacon
if (isUnload && navigator.sendBeacon) {
navigator.sendBeacon(this.config.reportUrl, JSON.stringify(reportData));
return;
}
// 普通上报使用 fetch
fetch(this.config.reportUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(reportData),
keepalive: true
}).catch(error => {
console.error('Report error:', error);
// 上报失败,重新加入缓存
this.cache.push(...data);
});
}
}
class BasePlugin {
constructor() {
this.config = {};
}
setConfig(config) {
this.config = { ...this.config, ...config };
}
setReport(report) {
this.report = report;
}
}
class PerformancePlugin extends BasePlugin {
constructor() {
super(...arguments);
this.name = 'performance';
}
init() {
this.monitorPageLoad();
this.monitorResourceLoad();
}
destroy() {
var _a;
(_a = this.performanceObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
}
monitorPageLoad() {
window.addEventListener('load', () => {
setTimeout(() => {
var _a;
const performance = window.performance;
if (!performance)
return;
const timing = performance.timing;
const data = {
type: 'performance',
subType: 'page-load',
dnsTime: timing.domainLookupEnd - timing.domainLookupStart,
tcpTime: timing.connectEnd - timing.connectStart,
whiteScreenTime: timing.domLoading - timing.navigationStart,
domReadyTime: timing.domContentLoadedEventEnd - timing.navigationStart,
loadTime: timing.loadEventEnd - timing.navigationStart,
};
(_a = this.report) === null || _a === void 0 ? void 0 : _a.call(this, data);
}, 0);
});
}
monitorResourceLoad() {
this.performanceObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
var _a;
const data = {
type: 'performance',
subType: 'resource',
name: entry.name,
initiatorType: entry.initiatorType,
duration: entry.duration,
transferSize: entry.transferSize,
};
(_a = this.report) === null || _a === void 0 ? void 0 : _a.call(this, data);
});
});
this.performanceObserver.observe({ entryTypes: ['resource'] });
}
}
class ErrorPlugin extends BasePlugin {
constructor() {
super();
this.name = 'error';
this.errorHandler = this.handleError.bind(this);
this.rejectionHandler = this.handleRejection.bind(this);
}
init() {
window.addEventListener('error', this.errorHandler, true);
window.addEventListener('unhandledrejection', this.rejectionHandler);
}
destroy() {
if (this.errorHandler) {
window.removeEventListener('error', this.errorHandler, true);
}
if (this.rejectionHandler) {
window.removeEventListener('unhandledrejection', this.rejectionHandler);
}
}
shouldIgnoreError(error) {
const ignoreErrors = (this.config.ignoreErrors || []);
const errorMessage = error instanceof Error ? error.message : error;
return ignoreErrors.some(pattern => pattern.test(errorMessage));
}
handleError(event) {
var _a, _b;
if (this.shouldIgnoreError(event.error || event.message))
return;
const data = {
type: 'error',
subType: 'javascript',
message: event.message,
filename: event.filename,
position: `${event.lineno}:${event.colno}`,
stack: (_a = event.error) === null || _a === void 0 ? void 0 : _a.stack,
};
(_b = this.report) === null || _b === void 0 ? void 0 : _b.call(this, data);
}
handleRejection(event) {
var _a;
const error = event.reason;
if (this.shouldIgnoreError(error))
return;
const data = {
type: 'error',
subType: 'promise',
message: (error === null || error === void 0 ? void 0 : error.message) || error,
stack: error === null || error === void 0 ? void 0 : error.stack,
};
(_a = this.report) === null || _a === void 0 ? void 0 : _a.call(this, data);
}
}
function debounce(fn, delay) {
let timer = null;
return function (...args) {
if (timer)
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
class BehaviorPlugin extends BasePlugin {
constructor() {
super();
this.name = 'behavior';
this.clickHandler = debounce(this.handleClick.bind(this), 300);
}
init() {
this.monitorPV();
document.addEventListener('click', this.clickHandler, true);
}
destroy() {
if (this.clickHandler) {
document.removeEventListener('click', this.clickHandler, true);
}
}
monitorPV() {
var _a;
const data = {
type: 'behavior',
subType: 'pv',
url: window.location.href,
referer: document.referrer,
};
(_a = this.report) === null || _a === void 0 ? void 0 : _a.call(this, data);
}
handleClick(event) {
var _a;
const target = event.target;
const data = {
type: 'behavior',
subType: 'click',
path: this.getElementPath(target),
};
(_a = this.report) === null || _a === void 0 ? void 0 : _a.call(this, data);
}
getElementPath(element) {
if (!element || !element.tagName)
return '';
const path = [];
let currentElement = element;
while (currentElement) {
let selector = currentElement.tagName.toLowerCase();
if (currentElement.id) {
selector += `#${currentElement.id}`;
}
else if (currentElement.className) {
selector += `.${currentElement.className.split(' ').join('.')}`;
}
path.unshift(selector);
currentElement = currentElement.parentElement;
}
return path.join(' > ');
}
}
exports.BehaviorPlugin = BehaviorPlugin;
exports.ErrorPlugin = ErrorPlugin;
exports.Monitor = Monitor;
exports.PerformancePlugin = PerformancePlugin;
//# sourceMappingURL=index.cjs.map