├── static └── .gitkeep ├── config ├── prod.env.js ├── dev.env.js └── index.js ├── src ├── assets │ └── logo.png ├── plugins │ ├── install.js │ └── index.js ├── main.js ├── App.vue └── components │ └── HelloWorld.vue ├── .editorconfig ├── .gitignore ├── .babelrc ├── .postcssrc.js ├── index.html ├── README.md ├── rollup.config.js └── package.json /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qinmudi/vue-error-report/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | vue-error-report 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/plugins/install.js: -------------------------------------------------------------------------------- 1 | import wiierror from './index' 2 | 3 | export default function install(Vue, options) { 4 | if (this.install.installed) return false 5 | 6 | let {isReport,reportUrl,appId} = options 7 | 8 | if (!reportUrl&&!appId) { 9 | return console.error(`reportUrl&appId is required`) 10 | } 11 | 12 | wiierror.reportUrl = reportUrl 13 | wiierror.options = Object.assign({},wiierror.options,{app_id: appId}) 14 | 15 | Object.defineProperty(Vue.prototype, '$wiierror', { 16 | get: () => this 17 | }) 18 | 19 | if(isReport){ 20 | wiierror.init() 21 | } 22 | 23 | this.install.installed = true 24 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import VueErrorReport from './plugins/index' 6 | 7 | Vue.config.productionTip = false 8 | 9 | Vue.use(VueErrorReport,{ 10 | isReport: true, 11 | reportUrl: 'http://dev.sqm.wiiqq.com/api/fe/save', 12 | appId: '86805a7139a2b8000' 13 | }); 14 | 15 | /* eslint-disable no-new */ 16 | new Vue({ 17 | el: '#app', 18 | components: { App }, 19 | template: '', 20 | mounted(){ 21 | var a = []; 22 | console.log(a.b.f) 23 | console.log('init') 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-error-report 2 | 3 | > 平台组前端错误上报插件,会自动收集以下几点: 4 | * 问题静态资源加载 5 | * Ajax请求 6 | * Vue语法错误 7 | * Javascript语法错误 8 | 9 | ## 安装 10 | 11 | ``` 12 | npm i vue-error-report --save 13 | ``` 14 | 15 | ## 引入 16 | 17 | ```javascript 18 | import VueErrorReport from 'vue-error-report' 19 | 20 | Vue.use(VueErrorReport,options) 21 | ``` 22 | 23 | ## 实例 24 | ```javascript 25 | Vue.use(VueErrorReport,{ 26 | isReport: true, 27 | reportUrl: 'https://ping.qq.com', 28 | appId: '' 29 | }) 30 | ``` 31 | 32 | ### options 33 | 34 | | 参数名 | 参数说明 | 是否必填 | 35 | | - | :-: | -:| 36 | | isReport | 是否开启上报 | 必填 | 37 | | reportUrl | 上报地址 | 必填 | 38 | | appId | 项目id | 必填 | 39 | 40 | ## 后置初始化 41 | ```javascript 42 | //当isReport等于 false 时,可以手工调用 this.$wiierror.init() 进行初始化 43 | this.$wiierror.init() 44 | ``` -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global'); 2 | import vue from 'rollup-plugin-vue2'; 3 | import buble from 'rollup-plugin-buble'; 4 | import nodeResolve from 'rollup-plugin-node-resolve'; 5 | import uglify from 'rollup-plugin-uglify'; 6 | import replace from 'rollup-plugin-replace'; 7 | 8 | rm('-rf', 'dist'); 9 | mkdir('-p', 'dist'); 10 | 11 | var entry = 'src/plugins/index.js'; 12 | 13 | export default { 14 | entry: entry, 15 | dest: 'dist/errorreport.js', 16 | format: 'cjs', 17 | sourceMap: true, 18 | plugins: [ 19 | vue(), 20 | buble({ 21 | objectAssign: 'Object.assign', 22 | exclude: 'node_modules/**' // only transpile our source code 23 | }), 24 | nodeResolve({ 25 | browser: true, 26 | jsnext: true, 27 | main: true, 28 | // pass custom options to the resolve plugin 29 | customResolveOptions: { 30 | moduleDirectory: 'node_modules' 31 | } 32 | }), 33 | uglify(), 34 | replace({ 35 | 'process.env.NODE_ENV': JSON.stringify( 'production' ) 36 | }) 37 | ], 38 | external: ['vue'] 39 | } 40 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 66 | 67 | 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-error-report", 3 | "version": "1.0.12", 4 | "description": "vue 错误上报插件", 5 | "author": "秦睦迪 ", 6 | "main": "dist/errorreport.js", 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "build": "rollup -c" 11 | }, 12 | "dependencies": { 13 | "vue": "^2.5.2", 14 | "webpack-zepto": "0.0.1" 15 | }, 16 | "devDependencies": { 17 | "autoprefixer": "^7.1.2", 18 | "babel-core": "^6.22.1", 19 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 20 | "babel-loader": "^7.1.1", 21 | "babel-plugin-syntax-jsx": "^6.18.0", 22 | "babel-plugin-transform-runtime": "^6.22.0", 23 | "babel-plugin-transform-vue-jsx": "^3.5.0", 24 | "babel-preset-env": "^1.3.2", 25 | "babel-preset-stage-2": "^6.22.0", 26 | "chalk": "^2.0.1", 27 | "copy-webpack-plugin": "^4.0.1", 28 | "css-loader": "^0.28.0", 29 | "extract-text-webpack-plugin": "^3.0.0", 30 | "file-loader": "^1.1.4", 31 | "friendly-errors-webpack-plugin": "^1.6.1", 32 | "html-webpack-plugin": "^2.30.1", 33 | "node-notifier": "^5.1.2", 34 | "optimize-css-assets-webpack-plugin": "^3.2.0", 35 | "ora": "^1.2.0", 36 | "portfinder": "^1.0.13", 37 | "postcss-import": "^11.0.0", 38 | "postcss-loader": "^2.0.8", 39 | "postcss-url": "^7.2.1", 40 | "rimraf": "^2.6.0", 41 | "rollup-plugin-babel": "^3.0.1", 42 | "rollup-plugin-buble": "^0.15.0", 43 | "rollup-plugin-commonjs": "^8.1.0", 44 | "rollup-plugin-node-resolve": "^3.0.0", 45 | "rollup-plugin-replace": "^1.1.1", 46 | "rollup-plugin-uglify": "^2.0.1", 47 | "rollup-plugin-vue2": "^0.8.0", 48 | "selenium-server": "^3.0.1", 49 | "semver": "^5.3.0", 50 | "shelljs": "^0.7.6", 51 | "uglifyjs-webpack-plugin": "^1.1.1", 52 | "url-loader": "^0.5.8", 53 | "vue-loader": "^13.3.0", 54 | "vue-style-loader": "^3.0.1", 55 | "vue-template-compiler": "^2.5.2", 56 | "webpack": "^3.6.0", 57 | "webpack-bundle-analyzer": "^2.9.0", 58 | "webpack-dev-server": "^2.9.1", 59 | "webpack-merge": "^4.1.0" 60 | }, 61 | "engines": { 62 | "node": ">= 6.0.0", 63 | "npm": ">= 3.0.0" 64 | }, 65 | "browserslist": [ 66 | "> 1%", 67 | "last 2 versions", 68 | "not ie <= 8" 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: { 14 | '/wmu': { 15 | target: 'http://wii2app.wiiqq.com', 16 | changeOrigin: true 17 | } 18 | }, 19 | 20 | // Various Dev Server settings 21 | host: 'localhost', // can be overwritten by process.env.HOST 22 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 23 | autoOpenBrowser: false, 24 | errorOverlay: true, 25 | notifyOnErrors: true, 26 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 27 | 28 | 29 | /** 30 | * Source Maps 31 | */ 32 | 33 | // https://webpack.js.org/configuration/devtool/#development 34 | devtool: 'cheap-module-eval-source-map', 35 | 36 | // If you have problems debugging vue-files in devtools, 37 | // set this to false - it *may* help 38 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 39 | cacheBusting: true, 40 | 41 | cssSourceMap: true 42 | }, 43 | 44 | build: { 45 | // Template for index.html 46 | index: path.resolve(__dirname, '../dist/index.html'), 47 | 48 | // Paths 49 | assetsRoot: path.resolve(__dirname, '../dist'), 50 | assetsSubDirectory: 'static', 51 | assetsPublicPath: '/', 52 | 53 | /** 54 | * Source Maps 55 | */ 56 | 57 | productionSourceMap: true, 58 | // https://webpack.js.org/configuration/devtool/#production 59 | devtool: '#source-map', 60 | 61 | // Gzip off by default as many popular static hosts such as 62 | // Surge or Netlify already gzip all static assets for you. 63 | // Before setting to `true`, make sure to: 64 | // npm install --save-dev compression-webpack-plugin 65 | productionGzip: false, 66 | productionGzipExtensions: ['js', 'css'], 67 | 68 | // Run the build command with an extra argument to 69 | // View the bundle analyzer report after build finishes: 70 | // `npm run build --report` 71 | // Set to `true` or `false` to always turn it on or off 72 | bundleAnalyzerReport: process.env.npm_config_report 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 96 | 97 | 98 | 114 | -------------------------------------------------------------------------------- /src/plugins/index.js: -------------------------------------------------------------------------------- 1 | import install from './install' 2 | 3 | /* 4 | *格式化参数 5 | */ 6 | const formatParams = function(data) { 7 | var arr = []; 8 | for (var name in data) { 9 | arr.push(encodeURIComponent(name) + "=" + encodeURIComponent(data[name])); 10 | } 11 | return arr.join("&"); 12 | } 13 | 14 | const wiierror = { 15 | reportUrl: '', 16 | options: { 17 | timespan: '', //发送数据时的时间戳 18 | level: 'error', //js日志错误级别,如warning, error, info, debug 19 | msg: '', //错误的具体信息, 20 | user_agent: navigator.userAgent, //userAgent 21 | app_id: '', //项目 id 22 | url: location.href, //上报页面地址 23 | stack: '', //错误堆栈 24 | data: {} //更多错误信息 25 | }, 26 | install, 27 | processStackMsg(error) { 28 | var stack = error.stack 29 | .replace(/\n/gi, '') // 去掉换行,节省传输内容大小 30 | .replace(/\bat\b/gi, '@') // chrome中是at,ff中是@ 31 | .split('@') // 以@分割信息 32 | .slice(0, 9) // 最大堆栈长度(Error.stackTraceLimit = 10),所以只取前10条 33 | .map((v) => v.replace(/^\s*|\s*$/g, '')) // 去除多余空格 34 | .join('~') // 手动添加分隔符,便于后期展示 35 | .replace(/\?[^:]+/gi, ''); // 去除js文件链接的多余参数(?x=1之类) 36 | var msg = error.toString(); 37 | if (stack.indexOf(msg) < 0) { 38 | stack = msg + '@' + stack; 39 | } 40 | return stack; 41 | }, 42 | formatComponentName(vm) { 43 | if (vm.$root === vm) return 'root'; 44 | var name = vm._isVue ? (vm.$options && vm.$options.name) || (vm.$options && vm.$options._componentTag) : vm.name; 45 | return (name ? 'component <' + name + '>' : 'anonymous component') + (vm._isVue && vm.$options && vm.$options.__file ? ' at ' + (vm.$options && vm.$options.__file) : ''); 46 | }, 47 | report_performance() { 48 | /* performance 对象 */ 49 | var performance = window.webkitPerformance || window.msPerformance || window.performance; 50 | /* 所需信息 */ 51 | var points = [ 52 | 'navigationStart', /* 开始浏览的时间 */ 53 | 'unloadEventStart', 'unloadEventEnd', /* 卸载上一个页面的时间 */ 54 | 'redirectStart', 'redirectEnd', /* HTTP重定向所消耗的时间 */ 55 | 'fetchStart', 'domainLookupStart', /* 缓存加载的时间 */ 56 | 'domainLookupStart', 'domainLookupEnd', /* DNS查询的时间 */ 57 | 'connectStart', 'connectEnd', /* 建立TCP连接的时间 */ 58 | 'connectStart', 'requestStart', 'responseStart', 'responseEnd', /* 建立TCP连接的时间 */ 59 | 'domInteractive', /* 可以交互的时间 */ 60 | 'domContentLoadedEventStart', 'domContentLoadedEventEnd', /* DomContentLoaded 页面加载完成的时间*/ 61 | 'domLoading', 'domComplete', /* 页面渲染的时间 */ 62 | 'domLoading', 'navigationStart', /* 加载页面花费的总时间 */ 63 | 'loadEventStart', 'loadEventEnd', /* 加载事件的时间 */ 64 | 'jsHeapSizeLimit', 'totalJSHeapSize', 'usedJSHeapSize', /* 内存的使用情况 */ 65 | 'redirectCount', 'type' /* 页面重定向的次数和类型 */ 66 | ] 67 | /* 性能对象的属性 */ 68 | var timing = performance.timing, 69 | memory = performance.memory, 70 | navigation = performance.navigation 71 | /* 判断性能对象是否可用 */ 72 | if (performance && timing && memory && navigation) { 73 | /* 组装统计的信息 */ 74 | var m = { 75 | timing: timing, 76 | memory: memory, 77 | navigation: navigation, 78 | userAgent: navigator.userAgent, 79 | url: location.href, 80 | data: + new Date /* + 相当于 .valueOf() */ 81 | } 82 | /* 打印出上传的信息 */ 83 | console.log(m); 84 | } 85 | }, 86 | init(){ 87 | //Ajax监控 88 | var s_ajaxListener = new Object(); 89 | s_ajaxListener.tempSend = XMLHttpRequest.prototype.send; //复制原先的send方法 90 | s_ajaxListener.tempOpen = XMLHttpRequest.prototype.open; //复制原先的open方法 91 | //重写open方法,记录请求的url 92 | XMLHttpRequest.prototype.open = function(method, url, boolen) { 93 | s_ajaxListener.tempOpen.apply(this, [method, url, boolen]); 94 | this.ajaxUrl = url; 95 | 96 | }; 97 | XMLHttpRequest.prototype.send = function(_data) { 98 | var oldReq = this.onreadystatechange 99 | this.onreadystatechange = function() { 100 | if (this.readyState == 4) { 101 | if (this.status >= 200 && this.status < 300) { 102 | oldReq?oldReq.apply(this, [_data]):'' 103 | } else { 104 | wiierror.options.msg = 'ajax请求错误'; 105 | wiierror.options.stack = `错误码:${this.status}` 106 | wiierror.options.data = JSON.stringify({ 107 | fileName: this.ajaxUrl, 108 | category: 'ajax', 109 | text: this.statusText, 110 | status: this.status 111 | }) 112 | // 合并上报的数据,包括默认上报的数据和自定义上报的数据 113 | var reportData = Object.assign({}, wiierror.options) 114 | // 把错误信息发送给后台 115 | wiierror.sendReport(reportData) 116 | } 117 | } 118 | } 119 | s_ajaxListener.tempSend.apply(this, [_data]) 120 | }; 121 | 122 | //监控资源加载错误(img,script,css,以及jsonp) 123 | window.addEventListener('error', function(e) { 124 | var target = e.target ? e.target : e.srcElement; 125 | wiierror.options.msg = e.target.localName + ' is load error'; 126 | wiierror.options.stack = 'resouce is not found'; 127 | wiierror.options.data = JSON.stringify({ 128 | tagName: e.target.localName, 129 | html: target.outerHTML, 130 | type: e.type, 131 | fileName: e.target.currentSrc, 132 | category: 'resource' 133 | }); 134 | if (e.target != window) { 135 | //抛去js语法错误 136 | // 合并上报的数据,包括默认上报的数据和自定义上报的数据 137 | var reportData = Object.assign({}, wiierror.options); 138 | wiierror.sendReport(reportData) 139 | } 140 | }, true); 141 | 142 | //监控js错误 143 | window.onerror = function(msg, _url, line, col, error) { 144 | if (msg === 'Script error.' && !_url) { 145 | return false; 146 | } 147 | //采用异步的方式,避免阻塞 148 | setTimeout(function() { 149 | //不一定所有浏览器都支持col参数,如果不支持就用window.event来兼容 150 | col = col || (window.event && window.event.errorCharacter) || 0; 151 | if (error && error.stack) { 152 | //msg信息较少,如果浏览器有追溯栈信息,使用追溯栈信息 153 | wiierror.options.msg = msg; 154 | wiierror.options.stack = error.stack; 155 | } else { 156 | wiierror.options.msg = msg; 157 | wiierror.options.stack = ''; 158 | } 159 | wiierror.options.data = JSON.stringify({ 160 | url: this.ajaxUrl, 161 | fileName: _url, 162 | category: 'javascript', 163 | line: line, 164 | col: col 165 | }) 166 | // 合并上报的数据,包括默认上报的数据和自定义上报的数据 167 | var reportData = Object.assign({}, wiierror.options) 168 | // 把错误信息发送给后台 169 | wiierror.sendReport(reportData) 170 | }, 0); 171 | 172 | return true; //错误不会console浏览器上,如需要,可将这样注释 173 | } 174 | 175 | //监控 promise 异常 176 | window.addEventListener('unhandledrejection', function(event){ 177 | // 进行各种处理 178 | wiierror.options.msg = event.reason; 179 | wiierror.options.data = JSON.stringify({ 180 | url: location.href, 181 | category: 'promise' 182 | }) 183 | wiierror.options.stack = 'promise is error'; 184 | var reportData = Object.assign({}, wiierror.options); 185 | wiierror.sendReport(reportData) 186 | // 如果想要阻止继续抛出,即会在控制台显示 `Uncaught(in promise) Error` 的话,调用以下函数 187 | event.preventDefault() 188 | },true) 189 | 190 | //Vue异常监控 191 | Vue.config.errorHandler = (error, vm, info) => { 192 | var componentName = wiierror.formatComponentName(vm); 193 | // var propsData = vm.$options && vm.$options.propsData; 194 | 195 | wiierror.options.msg = error.message; 196 | wiierror.options.stack = wiierror.processStackMsg(error); 197 | wiierror.options.data = JSON.stringify({ 198 | category: 'vue', 199 | componentName: componentName, 200 | // propsData: propsData, 201 | info: info 202 | }); 203 | 204 | // 合并上报的数据,包括默认上报的数据和自定义上报的数据 205 | var reportData = Object.assign({}, wiierror.options); 206 | wiierror.sendReport(reportData) 207 | } 208 | }, 209 | stop(){ 210 | this.sendReport = function(){} 211 | }, 212 | sendReport(data) { 213 | // this.report_performance() 214 | var img = new Image() 215 | img.onload = img.onerror = function() { 216 | img = null 217 | } 218 | var reqData = Object.assign({}, this.options, data, { 219 | timespan: new Date().getTime(), 220 | url: location.href 221 | }) 222 | img.src = `${this.reportUrl}?${formatParams(reqData)}` 223 | }, 224 | send(error, vm) { 225 | var componentName = this.formatComponentName(vm); 226 | this.options.msg = error.message ? error.message : '' 227 | this.options.stack = error.stack ? error.stack : '' 228 | this.options.data = JSON.stringify({ 229 | category: 'style', 230 | componentName: componentName 231 | }); 232 | this.sendReport(this.options) 233 | } 234 | } 235 | 236 | export default wiierror --------------------------------------------------------------------------------