├── LICENSE ├── README.en.md ├── README.md ├── backend ├── .autod.conf.js ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── README.md ├── app │ ├── controller │ │ ├── home.js │ │ └── mointor.js │ ├── middleware │ │ └── error_handler.js │ ├── router.js │ └── utils │ │ ├── sendMail.js │ │ └── stackparser.js ├── appveyor.yml ├── config │ ├── config.default.js │ └── plugin.js ├── jsconfig.json └── package.json └── vue-app ├── .env.development ├── .env.production ├── .env.uat ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── components │ └── Count.vue ├── main.js ├── plugins │ └── uploadSourceMapWebPackPlugin.js └── utils │ └── monitor.js ├── vue.config.js └── yarn.lock /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ZhenyuWang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # Exception monitoring 2 | 3 | [简体中文](./README.md) | English 4 | 5 | #### Description 6 | 7 | Front-end exception monitoring Demo for Vue2 8 | 9 | 10 | #### Corresponding article 11 | 12 | [Vue2异常监控](https://juejin.cn/post/7019857325800292388) 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 异常监控 2 | 3 | 简体中文 | [English](./README.en.md) 4 | 5 | #### 介绍 6 | 7 | Vue2 前端异常监控系统 8 | 9 | #### 掘金对应文章 10 | 11 | [Vue2异常监控](https://juejin.cn/post/7019857325800292388) 12 | -------------------------------------------------------------------------------- /backend/.autod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | write: true, 5 | prefix: '^', 6 | plugin: 'autod-egg', 7 | test: [ 8 | 'test', 9 | 'benchmark', 10 | ], 11 | dep: [ 12 | 'egg', 13 | 'egg-scripts', 14 | ], 15 | devdep: [ 16 | 'egg-ci', 17 | 'egg-bin', 18 | 'egg-mock', 19 | 'autod', 20 | 'autod-egg', 21 | 'eslint', 22 | 'eslint-config-egg', 23 | ], 24 | exclude: [ 25 | './test/fixtures', 26 | './dist', 27 | ], 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /backend/.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /backend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg" 3 | } 4 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | yarn-error.log 4 | node_modules/ 5 | package-lock.json 6 | yarn.lock 7 | coverage/ 8 | .idea/ 9 | run/ 10 | .DS_Store 11 | *.sw* 12 | *.un~ 13 | typings/ 14 | .nyc_output/ 15 | -------------------------------------------------------------------------------- /backend/.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: node_js 3 | node_js: 4 | - '10' 5 | before_install: 6 | - npm i npminstall -g 7 | install: 8 | - npminstall 9 | script: 10 | - npm run ci 11 | after_script: 12 | - npminstall codecov && codecov 13 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # mointor-backend 2 | 3 | 4 | 5 | ## QuickStart 6 | 7 | 8 | 9 | see [egg docs][egg] for more detail. 10 | 11 | ### Development 12 | 13 | ```bash 14 | $ npm i 15 | $ npm run dev 16 | $ open http://localhost:7001/ 17 | ``` 18 | 19 | ### Deploy 20 | 21 | ```bash 22 | $ npm start 23 | $ npm stop 24 | ``` 25 | 26 | ### npm scripts 27 | 28 | - Use `npm run lint` to check code style. 29 | - Use `npm test` to run unit test. 30 | - Use `npm run autod` to auto detect dependencies upgrade, see [autod](https://www.npmjs.com/package/autod) for more detail. 31 | 32 | 33 | [egg]: https://eggjs.org -------------------------------------------------------------------------------- /backend/app/controller/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class HomeController extends Controller { 6 | async index() { 7 | const { ctx } = this; 8 | ctx.body = 'wzy test home'; 9 | } 10 | } 11 | 12 | module.exports = HomeController; 13 | -------------------------------------------------------------------------------- /backend/app/controller/mointor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Controller = require('egg').Controller; 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const StackParser = require('../utils/stackparser'); 6 | const sendMail = require('../utils/sendMail'); 7 | 8 | class MointorController extends Controller { 9 | // 准备要上传的sourcemap的目标文件夹(没有则创建,如果有历史文件则清除) 10 | async emptyFolder() { 11 | const { ctx } = this; 12 | const env = ctx.query.env; 13 | const dir = path.join(this.config.baseDir, `upload/${env}`); 14 | // 判断upload/env文件夹是否存在 15 | if (!fs.existsSync(dir)) { 16 | // 没有创建 17 | fs.mkdirSync(dir); 18 | } else { 19 | // 有清空 20 | const files = fs.readdirSync(dir); 21 | files.forEach(file => { 22 | // 每一个文件路径 23 | const currentPath = dir + '/' + file; 24 | // 因为这里存放的都是.map文件,所以不需要判断是文件还是文件夹 25 | fs.unlinkSync(currentPath); 26 | }); 27 | } 28 | } 29 | // 前端打包时,上送sourcemap文件 30 | async uploadSourceMap() { 31 | const { ctx } = this; 32 | const stream = ctx.req, 33 | filename = ctx.query.name, 34 | env = ctx.query.env; 35 | // 要上传的目标路径 36 | const dir = path.join(this.config.baseDir, `upload/${env}`); 37 | // 目标文件 38 | const target = path.join(dir, filename); 39 | // 写入文件内容 40 | const writeStream = fs.createWriteStream(target); 41 | stream.pipe(writeStream); 42 | } 43 | // 前端报错,上报error 44 | async reportError() { 45 | const { ctx } = this; 46 | const { environment, location, message, stack, component, browserInfo, userId, userName, routerHistory, clickHistory } = ctx.request.body; 47 | let env = ''; 48 | if (environment === '测试环境') { 49 | env = 'uat'; 50 | } else if (environment === '生产环境') { 51 | env = 'prod'; 52 | } 53 | // 组合sourcemap文件路径 54 | const sourceMapDir = path.join(this.config.baseDir, `upload/${env}`); 55 | // 解析报错信息 56 | const stackParser = new StackParser(sourceMapDir); 57 | let routerHistoryStr = '
name:${item.name} | fullPath:${item.fullPath}
`; 62 | routerHistoryStr += `params:${JSON.stringify(item.params)} | query:${JSON.stringify(item.query)}
--------------------
`; 63 | }); 64 | // 组合点击历史信息 65 | clickHistory && clickHistory.length && clickHistory.forEach(item => { 66 | clickHistoryStr += `pageX:${item.pageX} | pageY:${item.pageY}
`; 67 | clickHistoryStr += `nodeName:${item.nodeName} | className:${item.className} | id:${item.id}
`; 68 | clickHistoryStr += `innerText:${item.innerText}
--------------------
`; 69 | }); 70 | // 通过上送的sourcemap文件,配合error信息,解析报错信息 71 | const errInfo = await stackParser.parseStackTrack(stack, message); 72 | // 获取当前时间 73 | const now = new Date(); 74 | const time = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()} ${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`; 75 | // 组织邮件正文 76 | const mailMsg = ` 77 |component:${component}
80 |source:${errInfo.source}
81 |line::${errInfo.lineNumber}
82 |column:${errInfo.columnNumber}
83 |fileName:${errInfo.fileName}
84 |functionName:${errInfo.functionName}
85 |time::${time}
86 |browserInfo::${browserInfo}
87 |userId::${userId}
88 |userName::${userName}
89 | ${routerHistoryStr} 90 | ${clickHistoryStr} 91 | `; 92 | // sendMail('发件箱地址', '发件箱授权码', '收件箱地址', 主题 environment, 正文 mailMsg); 93 | sendMail('发件箱地址', '发件箱授权码', '收件箱地址', environment, mailMsg); 94 | ctx.body = { 95 | header: { 96 | code: 0, 97 | message: 'OK', 98 | }, 99 | }; 100 | ctx.status = 200; 101 | } 102 | } 103 | 104 | module.exports = MointorController; 105 | -------------------------------------------------------------------------------- /backend/app/middleware/error_handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = () => { 3 | return async function errorHandler(ctx, next) { 4 | try { 5 | await next(); 6 | } catch (err) { 7 | // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 8 | ctx.app.emit('error', err, ctx); 9 | 10 | const status = err.status || 500; 11 | // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息 12 | const error = status === 500 && ctx.app.config.env === 'prod' 13 | ? 'Internal Server Error' 14 | : err.message; 15 | 16 | // 从 error 对象上读出各个属性,设置到响应中 17 | ctx.body = { error }; 18 | if (status === 422) { 19 | ctx.body.detail = err.errors; 20 | } 21 | ctx.status = status; 22 | } 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /backend/app/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @param {Egg.Application} app - egg application 5 | */ 6 | module.exports = app => { 7 | const { router, controller } = app; 8 | router.get('/', controller.home.index); 9 | router.post('/mointor/reportError', controller.mointor.reportError); 10 | router.get('/mointor/emptyFolder', controller.mointor.emptyFolder); 11 | router.post('/mointor/uploadSourceMap', controller.mointor.uploadSourceMap); 12 | }; 13 | -------------------------------------------------------------------------------- /backend/app/utils/sendMail.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const nodemailer = require('nodemailer'); 3 | // 发送邮件方法 4 | function sendMail(from, fromPass, receivers, subject, msg) { 5 | const smtpTransport = nodemailer.createTransport({ 6 | host: 'smtp.qq.email', 7 | service: 'qq', 8 | secureConnection: true, // use SSL 9 | secure: true, 10 | port: 465, 11 | auth: { 12 | user: from, 13 | pass: fromPass, 14 | }, 15 | }); 16 | 17 | smtpTransport.sendMail({ 18 | from, 19 | // 收件人邮箱,多个邮箱地址间用英文逗号隔开 20 | to: receivers, 21 | // 邮件主题 22 | subject, 23 | // 邮件正文 24 | html: msg, 25 | }, err => { 26 | if (err) { 27 | console.log('send mail error: ', err); 28 | } 29 | }); 30 | } 31 | module.exports = sendMail; 32 | -------------------------------------------------------------------------------- /backend/app/utils/stackparser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // 解析Error方法 3 | const ErrorStachParser = require('error-stack-parser'); 4 | const { SourceMapConsumer } = require('source-map'); 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | module.exports = class StackParser { 8 | constructor(sourceMapDir) { 9 | this.sourceMapDir = sourceMapDir; 10 | this.consumers = {}; 11 | } 12 | async parseStackTrack(stack, message) { 13 | const error = new Error(message); 14 | error.stack = stack; 15 | // 获取error堆栈信息 16 | const stackFrame = ErrorStachParser.parse(error); 17 | return await this.getOriginalErrorStack(stackFrame); 18 | } 19 | // 获取原始错误堆栈信息 20 | async getOriginalErrorStack(stackFrame) { 21 | const origin = await this.getOriginPosition(stackFrame[0]); 22 | return origin; 23 | } 24 | async getOriginPosition(stackFrame) { 25 | let { columnNumber, lineNumber, fileName } = stackFrame; 26 | fileName = path.basename(fileName); 27 | // 判断consumer是否存在 28 | let consumer = this.consumers[fileName]; 29 | if (consumer === undefined) { 30 | // 读取sourcemap 31 | const sourceMapPath = path.resolve(this.sourceMapDir, fileName + '.map'); 32 | // 判断文件是否存在 33 | if (!fs.existsSync(sourceMapPath)) { 34 | return stackFrame; 35 | } 36 | // 获取sourcemap内容 37 | const content = fs.readFileSync(sourceMapPath, 'utf-8'); 38 | consumer = await new SourceMapConsumer(content, null); 39 | // 将本次获取的sourcemap对象存放在缓存,方便下次使用 40 | this.consumers[fileName] = consumer; 41 | } 42 | const parseData = consumer.originalPositionFor({ line: lineNumber, column: columnNumber }); 43 | return parseData; 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /backend/appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '10' 4 | 5 | install: 6 | - ps: Install-Product node $env:nodejs_version 7 | - npm i npminstall && node_modules\.bin\npminstall 8 | 9 | test_script: 10 | - node --version 11 | - npm --version 12 | - npm run test 13 | 14 | build: off 15 | -------------------------------------------------------------------------------- /backend/config/config.default.js: -------------------------------------------------------------------------------- 1 | /* eslint valid-jsdoc: "off" */ 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * @param {Egg.EggAppInfo} appInfo app info 7 | */ 8 | module.exports = appInfo => { 9 | /** 10 | * built-in config 11 | * @type {Egg.EggAppConfig} 12 | **/ 13 | const config = exports = {}; 14 | 15 | // use for cookie sign key, should change to your own and keep security 16 | config.keys = appInfo.name + '_1625466426614_2490'; 17 | 18 | // 跨域配置 19 | config.security = { 20 | csrf: false, 21 | debug: 'csrf-disable', 22 | domainWhiteList: [ 'http://localhost:8080', 'http://127.0.0.1:8080', 'http://127.0.0.1:5000' ], 23 | }; 24 | 25 | // add your middleware config here 26 | config.middleware = [ 'errorHandler' ]; 27 | config.errorHandler = { 28 | match: '/', 29 | }; 30 | 31 | // add your user config here 32 | const userConfig = { 33 | // myAppName: 'egg', 34 | }; 35 | 36 | return { 37 | ...config, 38 | ...userConfig, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /backend/config/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type Egg.EggPlugin */ 4 | module.exports = { 5 | // had enabled by egg 6 | // static: { 7 | // enable: true, 8 | // } 9 | cors: { 10 | enable: true, 11 | package: 'egg-cors', 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /backend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*" 4 | ] 5 | } -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mointor-backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "egg": { 7 | "declarations": true 8 | }, 9 | "dependencies": { 10 | "egg": "^2.15.1", 11 | "egg-cors": "^2.2.3", 12 | "egg-scripts": "^2.11.0", 13 | "error-stack-parser": "^2.0.6", 14 | "nodemailer": "^6.6.2", 15 | "source-map": "^0.7.3" 16 | }, 17 | "devDependencies": { 18 | "autod": "^3.0.1", 19 | "autod-egg": "^1.1.0", 20 | "egg-bin": "^4.11.0", 21 | "egg-ci": "^1.11.0", 22 | "egg-mock": "^3.21.0", 23 | "eslint": "^5.13.0", 24 | "eslint-config-egg": "^7.1.0" 25 | }, 26 | "engines": { 27 | "node": ">=10.0.0" 28 | }, 29 | "scripts": { 30 | "start": "egg-scripts start --port=7001 --daemon --title=egg-server-mointor-backend", 31 | "stop": "egg-scripts stop --title=egg-server-mointor-backend", 32 | "dev": "egg-bin dev --port=7001", 33 | "debug": "egg-bin debug", 34 | "test": "npm run lint -- --fix && npm run test-local", 35 | "test-local": "egg-bin test", 36 | "cov": "egg-bin cov", 37 | "lint": "eslint .", 38 | "ci": "npm run lint && npm run cov", 39 | "autod": "autod" 40 | }, 41 | "ci": { 42 | "version": "10" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "" 47 | }, 48 | "author": "", 49 | "license": "MIT" 50 | } 51 | -------------------------------------------------------------------------------- /vue-app/.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | NODE_ENV='development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '测试环境地址' 6 | VUE_APP_MONITOR_BASE_API='http://127.0.0.1:7001' 7 | -------------------------------------------------------------------------------- /vue-app/.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | NODE_ENV='production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '生产环境地址' 6 | VUE_APP_MONITOR_BASE_API='http://127.0.0.1:7001' 7 | -------------------------------------------------------------------------------- /vue-app/.env.uat: -------------------------------------------------------------------------------- 1 | # just a flag 2 | NODE_ENV='production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '测试环境地址' 6 | VUE_APP_MONITOR_BASE_API='http://127.0.0.1:7001' 7 | -------------------------------------------------------------------------------- /vue-app/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /vue-app/README.md: -------------------------------------------------------------------------------- 1 | # vue-app 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | yarn lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /vue-app/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /vue-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build:uat": "vue-cli-service build --mode uat", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.21.1", 13 | "core-js": "^3.6.5", 14 | "vue": "^2.6.11" 15 | }, 16 | "devDependencies": { 17 | "@vue/cli-plugin-babel": "~4.5.0", 18 | "@vue/cli-plugin-eslint": "~4.5.0", 19 | "@vue/cli-service": "~4.5.0", 20 | "babel-eslint": "^10.1.0", 21 | "eslint": "^6.7.2", 22 | "eslint-plugin-vue": "^6.2.2", 23 | "vue-template-compiler": "^2.6.11" 24 | }, 25 | "eslintConfig": { 26 | "root": true, 27 | "env": { 28 | "node": true 29 | }, 30 | "extends": [ 31 | "plugin:vue/essential", 32 | "eslint:recommended" 33 | ], 34 | "parserOptions": { 35 | "parser": "babel-eslint" 36 | }, 37 | "rules": {} 38 | }, 39 | "browserslist": [ 40 | "> 1%", 41 | "last 2 versions", 42 | "not dead" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /vue-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenyuWang/Vue2-front-end--exception-monitoring/cb8a8d26eecc13aaba73aa016ccb9b2bb18b0cbf/vue-app/public/favicon.ico -------------------------------------------------------------------------------- /vue-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |