UNPKG

error-reports

Version:
410 lines (376 loc) 11.8 kB
import Vue from "vue"; import axios from "axios"; /* * 格式化参数 */ function formatParams(data = {}) { const arr = []; for (const name in data) { arr.push(encodeURIComponent(name) + "=" + encodeURIComponent(data[name])); } return arr.join("&"); } /** * 获取浏览器类型 */ function getBrowser() { // 取得浏览器的userAgent字符串 const userAgent = navigator.userAgent; let isOpera = false; // 判断是否Opera浏览器 if (userAgent.indexOf("Opera") > -1) { isOpera = true; return "Opera"; } // 判断是否Firefox浏览器 if (userAgent.indexOf("Firefox") > -1) { return "Firefox"; } // 判断是否Chrome浏览器 if (userAgent.indexOf("Chrome") > -1) { return "Chrome"; } // 判断是否Safari浏览器 if (userAgent.indexOf("Safari") > -1) { return "Safari"; } // 判断是否IE浏览器 if ( userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1 && !isOpera ) { return "IE"; } // 判断是否QQ浏览器 if (userAgent.match(/MQQBrowser\/([\d.]+)/i)) { return "QQBrower"; } return "Other"; } /** * 获取设备是安卓、 IOS 还是PC端 */ function getDevices() { const ua = navigator.userAgent; let isIPad = false; let isIPod = false; if (ua.match(/(Android)\s+([\d.]+)/i)) { return "Android"; } if (ua.match(/(iPad).*OS\s([\d_]+)/i)) { isIPad = true; return "iPad"; } if (ua.match(/(iPod).*OS\s([\d_]+)/i)) { isIPod = true; return "iPod"; } if (!isIPad && !isIPod && ua.match(/(iPhone\sOS)\s([\d_]+)/i)) { return "iPhone"; } return "PC"; } class ErrerReport { constructor(ops = {}) { // 上报Error地址 this.reportUrl = ops.reportUrl || `${window.location.origin}/errorReport`; // 延时上报Error时间 this.delayTime = ops.delayTime || 3000; // 断网标记, 默认不断网 this.offLineFlg = false; this.options = { appId: "", // 项目ID appName: "", // 项目名称 browser: getBrowser(), device: getDevices(), userId: "", // userId token: "", // token timeSpan: "", // 发送数据时的时间戳 infoType: "error", // 信息类别 msg: "", // 错误的具体信息, userAgent: navigator.userAgent, // userAgent pageUrl: window.location.href, // 上报页面地址 stack: "", // 错误堆栈信息 localStorageKey: "error_report_data", // localStorageKey category: "", // 类别 env: "dev", // 环境:dev、test、uat、pro data: {} // 更多错误信息 }; // 任务列表,存放所有任务 this.reqDataList = []; Object.assign(this.options, ops); this.init(); this.localStorageRestoreToList(); this.asyncSendReport(); } init() { // Ajax监控 const ajaxListener = {}; // 复制send方法 ajaxListener.tempSend = XMLHttpRequest.prototype.send; // 复制open方法 ajaxListener.tempOpen = XMLHttpRequest.prototype.open; // 重写open方法,记录请求的url XMLHttpRequest.prototype.open = function(method, url, boolen) { ajaxListener.tempOpen.apply(this, [method, url, boolen]); this.ajaxUrl = url; }; const self = this; // 发送 XMLHttpRequest.prototype.send = function(data) { const tempReadystate = this.onreadystatechange; this.onreadystatechange = function() { if (this.readyState === 4) { if (this.status >= 200 && this.status < 300) { tempReadystate && tempReadystate.apply(this, [data]); return; } self.options.msg = "AJAX 请求错误"; self.options.stack = `错误码:${this.status}`; self.options.category = "XMLHttpRequest"; self.options.data = JSON.stringify({ requestUrl: this.ajaxUrl, text: this.statusText, status: this.status }); // 合并上报的数据,包括默认上报的数据和自定义上报的数据 const reportData = Object.assign({}, self.options); // 把错误信息发送给后台 self.saveReport(reportData); } }; ajaxListener.tempSend.apply(this, [data]); }; /** * 监控资源加载错误(img,script,css,以及jsonp) * * 其中包括行列号,Error对象中存在错误的堆栈信息等。 */ window.addEventListener( "error", e => { const target = e.target ? e.target : e.srcElement; this.options.msg = e.target.localName + " is load error"; this.options.stack = "resouce is not found"; this.options.category = "Resource"; this.options.data = JSON.stringify({ tagName: e.target.localName, html: target.outerHTML, type: e.type, fileName: e.target.currentSrc }); if (e.target !== window) { // 抛去js语法错误 // 合并上报的数据,包括默认上报的数据和自定义上报的数据 const reportData = Object.assign({}, this.options); this.saveReport(reportData); } }, true ); /** * 监控 JS 错误,加载第三方JS出现 * * 其中包括行列号,Error对象中存在错误的堆栈信息等。 */ window.onerror = (msg, url, line, col, error) => { if (msg === "Script error." && !url) { return false; } // 采用异步的方式,避免阻塞 setTimeout(() => { // 不一定所有浏览器都支持col参数,如果不支持就用window.event来兼容 const newCol = col || (window.event && window.event.errorCharacter) || 0; if (error && error.stack) { // msg信息较少,如果浏览器有追溯栈信息,使用追溯栈信息 this.options.msg = msg; this.options.stack = error.stack; } else { this.options.msg = msg; this.options.stack = ""; } this.options.category = "JavaScript"; this.options.data = JSON.stringify({ pageUrl: this.ajaxUrl, fileName: url, line: line, col: newCol }); // 合并上报的数据,包括默认上报的数据和自定义上报的数据 const reportData = Object.assign({}, this.options); // 把错误信息发送给后台 this.saveReport(reportData); }, 0); // 错误不会console浏览器上,如需要,可将这样注释 return true; }; // 监控 Promise 异常 window.addEventListener( "unhandledrejection", event => { // 错误信息 this.options.msg = event.reason || ""; this.options.data = JSON.stringify({ pageUrl: window.location.href }); this.options.category = "Promise"; this.options.stack = "Promise is Error"; const reportData = Object.assign({}, this.options); this.saveReport(reportData); // 如果想要阻止继续抛出,即会在控制台显示 `Uncaught(in promise) Error` 的话,调用以下函数 event.preventDefault(); }, true ); // Vue 异常监控 Vue.config.errorHandler = (error, vm, info) => { const componentName = this.formatComponentName(vm); const propsData = vm.$options && vm.$options.propsData; this.options.msg = error.message; this.options.stack = this.processStackMsg(error); this.options.category = "Vue"; this.options.data = JSON.stringify({ componentName, propsData, info }); // 合并上报的数据,包括默认上报的数据和自定义上报的数据 const reportData = Object.assign({}, this.options); this.saveReport(reportData); }; // Axios 异常监控 axios.interceptors.response.use(null, error => { this.options.msg = error.message; this.options.stack = this.processStackMsg(error); this.options.category = "Axios"; this.options.data = JSON.stringify({}); // 合并上报的数据,包括默认上报的数据和自定义上报的数据 const reportData = Object.assign({}, this.options); this.saveReport(reportData); return Promise.reject(error); }); } /* eslint-disable class-methods-use-this */ processStackMsg(error) { let stack = error.stack .replace(/\n/gi, "") // 去掉换行,节省传输内容大小 .replace(/\bat\b/gi, "@") // chrome中是at,ff中是@ .split("@") // 以@分割信息 .slice(0, 9) // 最大堆栈长度(Error.stackTraceLimit = 10),所以只取前10条 .map(v => v.replace(/^\s*|\s*$/g, "")) // 去除多余空格 .join("~") // 手动添加分隔符,便于后期展示 .replace(/\?[^:]+/gi, ""); // 去除js文件链接的多余参数(?x=1之类) const msg = error.toString(); if (stack.indexOf(msg) < 0) { stack = msg + "@" + stack; } return stack; } /* eslint-disable class-methods-use-this */ formatComponentName(vm) { if (vm.$root === vm) { return "root"; } const name = vm._isVue ? (vm.$options && vm.$options.name) || (vm.$options && vm.$options._componentTag) : vm.name; return ( (name ? "component <" + name + ">" : "anonymous component") + (vm._isVue && vm.$options && vm.$options.__file ? " at " + (vm.$options && vm.$options.__file) : "") ); } /** * 保存异常信息 * @param {*} data 异常信息 */ saveReport(data) { const reqData = Object.assign({}, data, { timeSpan: Date.now(), pageUrl: window.location.href }); this.reqDataList.push(reqData); // 断网 if (navigator.onLine === false) { this.listSaveToLocalStorage(); this.reqDataList = []; } } /** * 异步上报异常信息 */ asyncSendReport() { // 在线 if (navigator.onLine) { // 之前离线 if (this.offLineFlg) { this.localStorageRestoreToList(); this.offLineFlg = false; } if (this.reqDataList.length > 0) { while (this.reqDataList.length > 0) { const img = new Image(); const reqData = this.reqDataList.shift(); // 延时处理 setTimeout(() => { img.src = `${this.reportUrl}?${formatParams(reqData)}`; }); } } } else { // 断网 this.offLineFlg = true; } this.executeDelayFunction(); } /** * list 保存到 localStorage */ listSaveToLocalStorage() { const key = this.options.localStorageKey; const errorReportDataList = this.getLocalStorageErrorData(key); if (errorReportDataList.length > 0) { this.reqDataList.push(...errorReportDataList); } localStorage.setItem(key, JSON.stringify(this.reqDataList)); } /** * localStorage 恢复到 List */ localStorageRestoreToList() { const key = this.options.localStorageKey; const errorReportDataList = this.getLocalStorageErrorData(key); if (errorReportDataList.length > 0) { this.reqDataList.push(...errorReportDataList); } localStorage.removeItem(key); } /** * 获取LocalStorage 存入的错误信息 */ getLocalStorageErrorData(key) { const errorReportStr = localStorage.getItem(key); if (errorReportStr === null || errorReportStr === "") { return []; } return JSON.parse(errorReportStr); } /** * 执行延时方法 */ executeDelayFunction() { setTimeout(() => { this.asyncSendReport(); }, this.delayTime); } } export default { install(Vue, options) { /* eslint-disable no-new */ new ErrerReport(options); } };