@zjsix/vue-monitor
Version:
A simple monitoring plugin for Vue.js applications, providing error tracking, performance monitoring and user behavior analysis
335 lines (329 loc) • 12.8 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
/**
* 格式化日期
* @param date Date | string | undefined,不传默认当前时间
* @param format 格式字符串,不传默认 "YYYY-MM-DD HH:mm:ss"
* @returns 格式化后的日期字符串
*/
function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
const d = (new Date());
if (isNaN(d.getTime())) {
throw new Error('Invalid date provided');
}
const pad = (n) => String(n).padStart(2, '0');
const map = {
YYYY: d.getFullYear().toString(),
MM: pad(d.getMonth() + 1),
DD: pad(d.getDate()),
HH: pad(d.getHours()),
mm: pad(d.getMinutes()),
ss: pad(d.getSeconds())
};
return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (key) => map[key]);
}
/**
* 节流函数
* @param func 需要节流的函数
* @param wait 节流时间间隔,单位毫秒
* @returns 节流后的函数
*/
function throttle(func, wait) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= wait) {
lastTime = now;
func.apply(this, args);
}
};
}
class VueMonitor {
constructor(options) {
var _a;
var _b, _c, _d;
this.options = options;
this.breadcrumbs = [];
this.errorCache = new Map();
(_b = this.options).maxBreadcrumbs || (_b.maxBreadcrumbs = 30);
(_c = this.options).errorThrottleTime || (_c.errorThrottleTime = 60 * 1000);
(_a = (_d = this.options).filterInputAndScanData) !== null && _a !== void 0 ? _a : (_d.filterInputAndScanData = true);
}
getErrorHash(e) {
return `${e.message}-${e.stack}-${e.url}`;
}
/** Vue 错误捕获 */
initVue(VueOrApp, isVue3 = false) {
var _a;
const original = (_a = VueOrApp.config) === null || _a === void 0 ? void 0 : _a.errorHandler;
if (isVue3) {
VueOrApp.config.errorHandler = (err, instance, info) => {
this.reportError({
message: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
info,
url: location.href,
timestamp: formatDate()
});
original === null || original === void 0 ? void 0 : original.call(VueOrApp, err, instance, info);
};
}
else {
VueOrApp.config.errorHandler = (err, vm, info) => {
this.reportError({
message: err.message,
stack: err.stack,
info,
url: location.href,
timestamp: formatDate()
});
original === null || original === void 0 ? void 0 : original.call(VueOrApp, err, vm, info);
};
}
}
/** 全局错误捕获 */
initGlobalError() {
function wrap(fn, cb) {
return ((...args) => {
cb(...args);
return fn === null || fn === void 0 ? void 0 : fn(...args);
});
}
window.onerror = wrap(window.onerror, (msg, src, line, col, err) => this.reportError({
message: String(msg) || 'unknown error',
stack: err === null || err === void 0 ? void 0 : err.stack,
url: location.href,
timestamp: formatDate()
}));
window.onunhandledrejection = wrap(window.onunhandledrejection, (e) => {
var _a, _b;
return this.reportError({
message: ((_a = e.reason) === null || _a === void 0 ? void 0 : _a.toString()) || 'unhandled promise rejection',
stack: (_b = e.reason) === null || _b === void 0 ? void 0 : _b.stack,
url: location.href,
timestamp: formatDate()
});
});
window.addEventListener('error', (e) => {
const el = e.target;
const map = {
SCRIPT: 'script load error',
LINK: 'link load error',
IMG: 'image load error'
};
const type = map[el.tagName];
if (type) {
this.reportError({
message: type,
url: el.src || el.href,
timestamp: formatDate()
});
}
}, true);
}
/** 用户行为记录 */
initBehavior() {
const add = (type, target, value) => this.addBreadcrumb({
type,
target: target.tagName +
(target.id ? `#${target.id}` : '') +
(target.className
? typeof target.className === 'string'
? `.${target.className.trim()}`
: `.${Array.from(target.className).map(c => String(c).trim()).join('.')}`
: ''),
value,
timestamp: formatDate()
});
const getTargetValue = (target) => {
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
return target.value;
}
else if (target.isContentEditable) {
return target.textContent || '';
}
return '';
};
// 点击事件
document.addEventListener('click', e => add('click', e.target));
// 输入事件
document.addEventListener('input', e => {
const t = e.target;
const v = getTargetValue(t);
const value = this.options.filterInputAndScanData ? `length:${v.length}` : v;
add('input', t, value);
});
// 中文输入法支持
let composing = false;
document.addEventListener('compositionstart', () => { composing = true; });
document.addEventListener('compositionend', (e) => {
composing = false;
const target = e.target;
const v = getTargetValue(target);
const value = this.options.filterInputAndScanData ? `length:${v.length}` : v;
add('scan', target, value);
});
// 扫码枪
document.addEventListener('keydown', e => {
if (!composing && e.key === 'Enter') {
const target = e.target;
const v = getTargetValue(target);
const value = this.options.filterInputAndScanData ? `length:${v.length}` : v;
add('scan', target, value);
}
});
}
addBreadcrumb(b) {
this.breadcrumbs.push(b);
if (this.breadcrumbs.length > (this.options.maxBreadcrumbs || 20))
this.breadcrumbs.shift();
}
/** 上报错误 */
reportError(err) {
const info = err instanceof Error
? {
message: err.message,
stack: err.stack,
url: location.href,
timestamp: formatDate()
}
: err;
const hash = this.getErrorHash(info);
const now = Date.now();
const limit = this.options.errorThrottleTime;
for (const [h, t] of this.errorCache.entries()) {
if (now - t > limit)
this.errorCache.delete(h);
}
if (this.errorCache.has(hash) && now - this.errorCache.get(hash) < limit) {
console.warn('重复错误被忽略', info.message);
return;
}
this.errorCache.set(hash, now);
let payload = Object.assign({ projectName: this.options.projectName, projectVersion: this.options.projectVersion, error: info, breadcrumbs: this.breadcrumbs }, (this.options.customData || {}));
fetch(this.options.reportUrl, {
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json' }, (this.options.customHeaders || {})),
body: JSON.stringify(payload),
keepalive: true
}).catch(e => console.warn('上报错误失败:', e));
}
/** 性能监控 */
initPerformanceMonitor() {
if (typeof PerformanceObserver !== 'function')
return;
const perfTypes = [
'paint',
'largest-contentful-paint',
'first-input',
'layout-shift'
];
perfTypes.forEach(type => {
try {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.addBreadcrumb({
type: 'performance',
target: type,
value: JSON.stringify({
name: entry.name,
startTime: entry.startTime,
duration: entry.duration
}),
timestamp: formatDate()
});
}
});
observer.observe({ type, buffered: true });
}
catch (e) {
console.warn('监听页面核心性能指标报错', e);
}
});
try {
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.addBreadcrumb({
type: 'performance',
target: 'longtask',
value: JSON.stringify({
name: entry.name,
startTime: entry.startTime,
duration: entry.duration
}),
timestamp: formatDate()
});
}
});
longTaskObserver.observe({ type: 'longtask', buffered: true });
}
catch (e) {
console.warn('监听页面长任务报错', e);
}
/** 掉帧监控 */
function initFrameDropMonitor(addBreadcrumb) {
let lastFrameTime = performance.now();
let pendingFrameDrop = null;
const pushBreadcrumb = throttle(() => {
if (pendingFrameDrop) {
addBreadcrumb({
type: 'performance',
target: 'frame-drop',
value: JSON.stringify(pendingFrameDrop),
timestamp: formatDate()
});
pendingFrameDrop = null;
}
}, 100);
const tick = () => {
const now = performance.now();
const delta = now - lastFrameTime;
lastFrameTime = now;
if (!document.hidden && delta > 50) {
if (pendingFrameDrop) {
pendingFrameDrop.count += 1;
pendingFrameDrop.frameTime = delta;
}
else {
pendingFrameDrop = { frameTime: delta, count: 1 };
}
pushBreadcrumb();
}
if (document.hidden)
lastFrameTime = performance.now();
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}
initFrameDropMonitor((breadcrumb) => this.addBreadcrumb(breadcrumb));
}
}
const VueMonitorPlugin = {
install(VueOrApp, options) {
if (!options || !options.reportUrl) {
console.error('请提供 reportUrl 选项');
return;
}
// 判断是否 Vue 环境
const isVue3 = (VueOrApp === null || VueOrApp === void 0 ? void 0 : VueOrApp.config) && (VueOrApp === null || VueOrApp === void 0 ? void 0 : VueOrApp.config.globalProperties);
const isVue2 = (VueOrApp === null || VueOrApp === void 0 ? void 0 : VueOrApp.prototype) && (VueOrApp === null || VueOrApp === void 0 ? void 0 : VueOrApp.version);
if (!isVue2 && !isVue3) {
console.warn('[VueMonitor] 未检测到 Vue 环境,插件未生效');
return;
}
const monitor = new VueMonitor(options);
monitor.initGlobalError();
monitor.initBehavior();
monitor.initPerformanceMonitor();
if (isVue3) {
monitor.initVue(VueOrApp, true);
VueOrApp.config.globalProperties.$monitor = monitor;
}
else {
monitor.initVue(VueOrApp, false);
VueOrApp.prototype.$monitor = monitor;
}
}
};
exports.VueMonitor = VueMonitor;
exports.default = VueMonitorPlugin;
;