UNPKG

@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
'use strict'; 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;