├── .gitignore ├── .travis.yml ├── Demo ├── demo1.png ├── demo2.png ├── timing.png ├── weixin.png └── zhifubao.png ├── README.md ├── backend_server ├── Dockerfile ├── app.js ├── bin │ └── init.js ├── business │ ├── apiInfo.js │ ├── consoleInfo.js │ ├── focusClickInfo.js │ ├── jsError.js │ ├── perfInfo.js │ ├── pvInfo.js │ ├── resourceInfo.js │ ├── scheduleTask.js │ ├── site.js │ └── userInfo.js ├── data │ └── ip2region.db ├── docker-build.sh ├── models │ ├── apiModel.js │ ├── consoleModel.js │ ├── focusClickModel.js │ ├── jsModel.js │ ├── perfModel.js │ ├── pvModel.js │ ├── resourceModel.js │ ├── siteModel.js │ └── userModel.js ├── package.json ├── public │ └── stylesheets │ │ └── style.css ├── routes │ ├── index.js │ ├── monitor.js │ ├── upData.js │ └── user.js ├── setting.json ├── utils │ └── util.js └── views │ ├── error.jade │ ├── index.jade │ └── layout.jade ├── docker-compose.yml ├── docker-start.sh └── web ├── .editorconfig ├── .gitignore ├── Dockerfile ├── README.md ├── angular.json ├── docker-build.sh ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── extraJs └── china.js ├── karma.conf.js ├── nginx.conf ├── package.json ├── protractor.conf.js ├── src ├── app │ ├── animations.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── dashboard │ │ ├── dashboard.module.ts │ │ └── sys-list │ │ │ ├── sys-list.component.html │ │ │ ├── sys-list.component.scss │ │ │ └── sys-list.component.ts │ ├── homepage │ │ ├── home-page │ │ │ ├── homepage.component.html │ │ │ ├── homepage.component.scss │ │ │ └── homepage.component.ts │ │ └── homepage.module.ts │ ├── model │ │ └── entity.ts │ ├── monitor.common.service.ts │ ├── public │ │ ├── advise-bar │ │ │ ├── advise-bar.component.html │ │ │ ├── advise-bar.component.scss │ │ │ └── advise-bar.component.ts │ │ ├── custom-highchart │ │ │ ├── custom-highchart.component.html │ │ │ ├── custom-highchart.component.scss │ │ │ └── custom-highchart.component.ts │ │ ├── line-progress │ │ │ ├── line-progress.component.html │ │ │ ├── line-progress.component.scss │ │ │ └── line-progress.component.ts │ │ ├── monitor-a-blank.directive.ts │ │ ├── public.module.ts │ │ └── time-choice-panel │ │ │ ├── time-choice-panel.component.html │ │ │ ├── time-choice-panel.component.scss │ │ │ └── time-choice-panel.component.ts │ └── web │ │ ├── web-sys │ │ ├── api-request │ │ │ ├── api-request.component.html │ │ │ ├── api-request.component.scss │ │ │ └── api-request.component.ts │ │ ├── backend-log │ │ │ ├── backend-log.component.html │ │ │ ├── backend-log.component.scss │ │ │ └── backend-log.component.ts │ │ ├── js-error-track │ │ │ ├── js-error-track.component.html │ │ │ ├── js-error-track.component.scss │ │ │ └── js-error-track.component.ts │ │ ├── js-error │ │ │ ├── js-error.component.html │ │ │ ├── js-error.component.scss │ │ │ └── js-error.component.ts │ │ ├── resource-load-details │ │ │ ├── resource-load-details.component.html │ │ │ ├── resource-load-details.component.scss │ │ │ └── resource-load-details.component.ts │ │ ├── sys-index │ │ │ ├── sys-index.component.html │ │ │ ├── sys-index.component.scss │ │ │ └── sys-index.component.ts │ │ ├── sys-setting │ │ │ ├── sys-setting.component.html │ │ │ ├── sys-setting.component.scss │ │ │ └── sys-setting.component.ts │ │ ├── user-path │ │ │ ├── user-path.component.html │ │ │ ├── user-path.component.scss │ │ │ └── user-path.component.ts │ │ ├── visit-details │ │ │ ├── visit-details.component.html │ │ │ ├── visit-details.component.scss │ │ │ └── visit-details.component.ts │ │ ├── visit-geo │ │ │ ├── visit-geo.component.html │ │ │ ├── visit-geo.component.scss │ │ │ └── visit-geo.component.ts │ │ ├── visit-os │ │ │ ├── visit-os.component.html │ │ │ ├── visit-os.component.scss │ │ │ └── visit-os.component.ts │ │ ├── visit-page │ │ │ ├── visit-page.component.html │ │ │ ├── visit-page.component.scss │ │ │ └── visit-page.component.ts │ │ ├── visit-speed │ │ │ ├── visit-speed.component.html │ │ │ ├── visit-speed.component.scss │ │ │ └── visit-speed.component.ts │ │ ├── web-sys.component.html │ │ ├── web-sys.component.scss │ │ └── web-sys.component.ts │ │ └── web.module.ts ├── assets │ ├── .gitkeep │ └── images │ │ ├── error404.png │ │ ├── github.png │ │ ├── home.jpg │ │ ├── icon │ │ ├── SDK.png │ │ ├── alarm.png │ │ ├── app.png │ │ ├── data.png │ │ ├── m.png │ │ ├── monitor.png │ │ ├── multi.png │ │ ├── q.png │ │ └── safe.png │ │ ├── weixin-gongzhonghao.jpg │ │ ├── weixin.png │ │ └── zhifubao.png ├── bundle.js ├── environments │ ├── environment.docker.ts │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # IDEs and editors 4 | .vscode/ 5 | package-lock.json 6 | node_modules 7 | /browser_probe 8 | .history 9 | /backend_server/access.log 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | install: 5 | | 6 | npm install -g npm@latest 7 | npm --version 8 | script: 9 | - cd web&&npm install&&npm run test 10 | -------------------------------------------------------------------------------- /Demo/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/Demo/demo1.png -------------------------------------------------------------------------------- /Demo/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/Demo/demo2.png -------------------------------------------------------------------------------- /Demo/timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/Demo/timing.png -------------------------------------------------------------------------------- /Demo/weixin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/Demo/weixin.png -------------------------------------------------------------------------------- /Demo/zhifubao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/Demo/zhifubao.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web-monitoring 2 | 3 | ### [功能列表] 4 | 5 | - [x] 允许用户创建多个监测站点 6 | - [x] 从不同维度统计用户访问情况 7 | - [x] 自定义查询时间 8 | - [x] 多种图表展示 9 | - [x] 支持自定义上报(js错误,api请求,性能信息) 10 | - [x] 用户访问路径追踪 11 | - [x] 自定义阈值(注册用户) 12 | - [x] 自动报警功能,发送报警邮件提醒(注册用户) 13 | 14 | ------ 15 | ### [技术支持] 16 | 17 | - [x] 前端:Angular5+,ant-design 18 | - [x] 后端:Nodejs+Express 19 | - [x] 数据库:Mongoose+MongoDB 20 | - [x] Docker:一键部署Docker环境 21 | 22 | 23 | ------ 24 | 前端监控平台专注于 Web 端体验数据监控。监测 Web 页面的健康度的三个方面: 25 | > * 页面打开速度(测速) 26 | > * 页面稳定性(JS Error) 27 | > * 外部服务调用成功率(API) 28 | 29 | 然后从不同纬度去分析用户体验。 30 | 31 | > - 访问页面 32 | > - 访问速度 33 | > - API请求 34 | > - 地理 35 | > - 终端 36 | 37 | 38 | 39 | 参考: 40 | > http://fex.baidu.com/blog/2014/05/build-performance-monitor-in-7-days 41 | 42 | > 阿里云前端监控 43 | 44 | 45 | ### 页面打开速度 46 | 47 | 网络耗时数据可以借助前面提到 Navigation Timing 接口获取,与之类似的还有Resource Timing,可以获取页面所有静态资源的加载耗时。通过此接口可以轻松获取 DNS、TCP、首字节、html 传输等耗时,Navigation Timing 的接口示意图如下所示: 48 | 49 | ![file-list](https://github.com/kisslove/web-front-end-monitoring/blob/master/Demo/timing.png) 50 | 51 | 52 | ### API请求 53 | 54 | 默认情况下,使用XMLHTTP拦截用户请求,在请求成功/失败后,统计时间,上报请求。 55 | 用户可使用**__ml.api(api,success, time, code, msg)**手动上报。 56 | ```javascript 57 | api:请求接口 58 | success:上传是否成功(true/false ) 59 | time:耗时(ms) 60 | code:返回码 61 | msg:消息(string/object) 62 | ``` 63 | ### JS错误 64 | 65 | 默认情况下,使用window.onError去监听用户错误脚本,自动上报。 66 | 用户使用的有些前端框架会捕获js错误,错误信息不会抛至window.onError,这种情况需用户手动调用。 67 | 例如在Angular2+,在你的框架全局捕获错误的地方调用**__ml.error(errorObj)** 68 | ```javascript 69 | export class MyErrorHandler implements ErrorHandler { 70 | handleError(error) { 71 | console.error(error); 72 | window.__ml && window.__ml.error && window.__ml.error(error.stack || error); 73 | } 74 | } 75 | @NgModule({ 76 | declarations: [], 77 | imports: [], 78 | providers: [{ provide: ErrorHandler, useClass: MyErrorHandler }], 79 | bootstrap: [] 80 | }) 81 | export class AppModule { } 82 | ``` 83 | 在Vue: 84 | ```javascript 85 | import Vue from 'vue' 86 | const errorHandler = (error, vm)=>{ 87 | console.error(error); 88 | window.__ml && window.__ml.error && window.__ml.error(error); 89 | } 90 | Vue.config.errorHandler = errorHandler; 91 | Vue.prototype.$throw = (error)=> errorHandler(error,this); 92 | ``` 93 | ### 如何使用(easy!!!) 94 | 95 | 网页地址:WEB-MONITOR 96 | 97 | 第一步:在监控站点中创建一个站点。 98 | 99 | ![file-list](https://github.com/kisslove/web-front-end-monitoring/blob/master/Demo/demo1.png) 100 | 101 | 第二步:复制应用配置中的探针到你需要监控的站点(index.html)页面。大功告成! 102 | 103 | ![file-list](https://github.com/kisslove/web-front-end-monitoring/blob/master/Demo/demo2.png) 104 | 105 | ### 贡献者支持 106 | 107 | 您的支持,是我们最大的前进动力。 108 | 109 | 支付宝: 110 | 111 | ![file-list](https://github.com/kisslove/web-front-end-monitoring/blob/master/Demo/zhifubao.png) 112 | 113 | 微信: 114 | 115 | ![file-list](https://github.com/kisslove/web-front-end-monitoring/blob/master/Demo/weixin.png) 116 | 117 | 118 | -------------------------------------------------------------------------------- /backend_server/Dockerfile: -------------------------------------------------------------------------------- 1 | # 基于 node 镜像 2 | 3 | FROM node 4 | RUN rm -rf /www 5 | RUN mkdir /www 6 | WORKDIR /www 7 | 8 | COPY . /www 9 | RUN npm install 10 | RUN npm i -g pm2 11 | EXPOSE 9000 12 | CMD pm2 start bin/init.js --name web-monitoring/backend_server_docker --no-daemon -------------------------------------------------------------------------------- /backend_server/app.js: -------------------------------------------------------------------------------- 1 | var createError = require("http-errors"); 2 | var express = require("express"); 3 | var path = require("path"); 4 | var cookieParser = require("cookie-parser"); 5 | var logger = require("morgan"); 6 | var indexRouter = require("./routes/index"); 7 | var monitorRouter = require("./routes/monitor"); 8 | var upDataRouter = require("./routes/upData"); 9 | var userRouter = require("./routes/user"); 10 | var userInfo = require("./business/userInfo"); 11 | var ScheduleTask = require("./business/scheduleTask"); 12 | 13 | var util = require("./utils/util"); 14 | var setting = require("./setting.json"); 15 | var mongoose = require("mongoose"); 16 | var fs = require("fs"); 17 | 18 | // 判断是否是docker部署方式 19 | var isDocker = process.env.name === "web-monitoring/backend_server_docker"; 20 | console.log(process.env.name,"process.env.name"); 21 | mongoose 22 | .connect( 23 | `mongodb://${ 24 | isDocker ? setting.mongoDB.dockerAddress : setting.mongoDB.address 25 | }/monitor`, 26 | { 27 | socketTimeoutMS: 0, 28 | keepAlive: true, 29 | useNewUrlParser: true, 30 | reconnectTries: Number.MAX_VALUE, 31 | reconnectInterval: 1000, 32 | } 33 | ) 34 | .then(() => { 35 | //默认系统管理员 36 | userInfo.createAdmin("sa@admins.com"); 37 | }); 38 | 39 | //开启任务 40 | if (setting.nodemailer.enable) { 41 | ScheduleTask.startTask(); 42 | } 43 | 44 | var app = express(); 45 | 46 | // view engine setup 47 | app.set("views", path.join(__dirname, "views")); 48 | app.set("view engine", "jade"); 49 | 50 | //日志 51 | var accessLogStream = fs.createWriteStream(path.join(__dirname, "access.log"), { 52 | flags: "a", 53 | }); 54 | app.use(logger("short", { stream: accessLogStream })); 55 | app.use(logger("dev")); 56 | 57 | app.use(express.json()); 58 | app.use(express.urlencoded({ extended: false })); 59 | app.use(cookieParser()); 60 | app.use(express.static(path.join(__dirname, "public"))); 61 | 62 | //cors 63 | app.all("*", function (req, res, next) { 64 | res.header("Access-Control-Allow-Origin", "*"); 65 | res.header( 66 | "Access-Control-Allow-Headers", 67 | "Content-Type,Content-Length, Authorization, Accept,X-Requested-With" 68 | ); 69 | res.header("Access-Control-Allow-Methods", "POST,GET,OPTIONS"); 70 | res.header("X-Powered-By", " 3.2.1"); 71 | if (req.method == "OPTIONS") res.sendStatus(200); 72 | /*让options请求快速返回*/ else next(); 73 | }); 74 | 75 | app.use("/", indexRouter); 76 | app.use("/Monitor/", util.resolveToken, monitorRouter); 77 | app.use("/Up", upDataRouter); 78 | app.use("/User", userRouter); 79 | 80 | // catch 404 and forward to error handler 81 | app.use(function (req, res, next) { 82 | next(createError(404)); 83 | }); 84 | 85 | // error handler 86 | app.use(function (err, req, res, next) { 87 | // set locals, only providing error in development 88 | res.locals.message = err.message; 89 | res.locals.error = req.app.get("env") === "development" ? err : {}; 90 | 91 | // render the error page 92 | res.sendStatus(err.status || 500); 93 | // res.render('error'); 94 | }); 95 | 96 | module.exports = app; 97 | -------------------------------------------------------------------------------- /backend_server/bin/init.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('backend-server:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort('9000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' ? 62 | 'Pipe ' + port : 63 | 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' ? 87 | 'pipe ' + addr : 88 | 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } -------------------------------------------------------------------------------- /backend_server/business/consoleInfo.js: -------------------------------------------------------------------------------- 1 | var Mongoose = require('mongoose'); 2 | var ConsoleModel = require('../models/consoleModel'); 3 | 4 | exports.create = (data) => { 5 | var temp = new ConsoleModel(data); 6 | temp.save(function(err, r) { 7 | if (err) { 8 | console.error(err); 9 | } 10 | }); 11 | }; -------------------------------------------------------------------------------- /backend_server/business/focusClickInfo.js: -------------------------------------------------------------------------------- 1 | var Mongoose = require('mongoose'); 2 | var FocusClickModel = require('../models/focusClickModel'); 3 | var _ = require('lodash'); 4 | var util = require('../utils/util'); 5 | 6 | 7 | exports.create = (data) => { 8 | var temp = new FocusClickModel(data); 9 | temp.save(function(err, r) { 10 | if (err) { 11 | console.error(err); 12 | } 13 | }); 14 | }; -------------------------------------------------------------------------------- /backend_server/business/resourceInfo.js: -------------------------------------------------------------------------------- 1 | var Mongoose = require('mongoose'); 2 | var ResourceModel = require('../models/resourceModel'); 3 | var _ = require('lodash'); 4 | var util = require('../utils/util'); 5 | 6 | /** 7 | * 资源加载-列表 8 | * @param {*} req 9 | */ 10 | exports.list = async(req) => { 11 | let appKey = new Mongoose.Types.ObjectId(req.body.appKey); 12 | let body = util.computeSTimeAndEtime(req.body); 13 | let resJson = { 14 | List: [], 15 | TotalCount: 0 16 | }; 17 | let tempCon = ''; 18 | tempCon = body.type ? { 19 | "createTime": { 20 | '$gte': body.sTime, 21 | '$lt': body.eTime 22 | }, 23 | "appKey": appKey, 24 | "rUrl": { 25 | '$regex': new RegExp(`${body.keywords}.*`, "gi") 26 | }, 27 | "rInitiatorType": body.type 28 | } : { 29 | "createTime": { 30 | '$gte': body.sTime, 31 | '$lt': body.eTime 32 | }, 33 | "appKey": appKey, 34 | "rUrl": { 35 | '$regex': new RegExp(`${body.keywords}.*`, "gi") 36 | } 37 | }; 38 | resJson.TotalCount = await ResourceModel.find(tempCon).countDocuments(); 39 | if (resJson.TotalCount) { 40 | resJson.List = await ResourceModel.find(tempCon).sort({ "createTime": -1 }).skip((req.body.pageIndex - 1) * req.body.pageSize).limit(req.body.pageSize); 41 | } 42 | return resJson; 43 | }; 44 | 45 | exports.create = (data) => { 46 | var temp = new ResourceModel(data); 47 | temp.save(function(err, r) { 48 | if (err) { 49 | console.error(err); 50 | } 51 | }); 52 | }; -------------------------------------------------------------------------------- /backend_server/business/scheduleTask.js: -------------------------------------------------------------------------------- 1 | var Mongoose = require("mongoose"); 2 | var Agenda = require("agenda"); 3 | var nodemailer = require("nodemailer"); 4 | var JsModel = require("../models/jsModel"); 5 | var PvModel = require("../models/pvModel"); 6 | var ApiModel = require("../models/apiModel"); 7 | var PerfModel = require("../models/perfModel"); 8 | var SiteModel = require("../models/siteModel"); 9 | var _ = require("lodash"); 10 | var setting = require("../setting.json"); 11 | // 判断是否是docker部署方式 12 | var isDocker = process.env.name === "web-monitoring/backend_server_docker"; 13 | const mongoConnectionString = `mongodb://${ 14 | isDocker ? setting.mongoDB.dockerAddress : setting.mongoDB.address 15 | }/agendatask`; 16 | const agenda = new Agenda({ db: { address: mongoConnectionString } }); 17 | agenda.processEvery("30 seconds"); 18 | 19 | const transporter = nodemailer.createTransport({ 20 | ...setting.nodemailer, 21 | // host: "smtpdm.aliyun.com", 22 | // port: 25, 23 | // secureConnection: true, // use SSL, the port is 465 24 | // auth: { 25 | // user: "admin@hubing.online", 26 | // pass: "*********", 27 | // }, 28 | }); 29 | 30 | async function sendEmail(options) { 31 | let appName = options.appKey; //站点名称 32 | let appKey = new Mongoose.Types.ObjectId(options.appKey); 33 | let appInfo = await SiteModel.findOne({ appKey: appKey }); 34 | if (appInfo) { 35 | appName = appInfo.appName; 36 | } 37 | let body = ""; 38 | let subject = ""; 39 | let result; 40 | switch (options.alarmType) { 41 | case "jsError": 42 | result = await checkjsErrorResult(options); 43 | body = `

${ 44 | options.email 45 | }

你好,前端监控平台(hubing.online:8083)发现您在平台创建的【${appName}】系统触发JavaScript错误临界值,错误率为:${ 46 | result * 100 47 | }%,请前往系统及时处理。

前端监控平台管理员

`; 48 | subject = "JavaScript错误报警提醒"; 49 | break; 50 | case "apiError": 51 | result = await checkApiErrorResult(options); 52 | body = `

${ 53 | options.email 54 | }

你好,前端监控平台(hubing.online:8083)发现您在平台创建的【${ 55 | options.appName 56 | }】系统触发API请求错误临界值,错误率为:${ 57 | result * 100 58 | }%,请前往系统及时处理。

前端监控平台管理员

`; 59 | subject = "API错误报警提醒"; 60 | break; 61 | case "perfSpeed": 62 | result = await checkPerfSpeedResult(options); 63 | body = `

${options.email}

你好,前端监控平台(hubing.online:8083)发现您在平台创建的【${options.appName}】系统触发访问速度临界值,目前访问速度平均值为:${result},请前往系统及时处理。

前端监控平台管理员

`; 64 | subject = "访问速度报警提醒"; 65 | break; 66 | default: 67 | break; 68 | } 69 | var mailOptions = { 70 | from: "前端监控平台管理员", // sender address mailfrom must be same with the user 71 | to: options.email, // list of receivers 72 | subject: subject, // Subject line 73 | replyTo: "676022504@qq.com", //custom reply address 74 | html: body, // html body 75 | }; 76 | 77 | if (result * 100 < options.limitValue) return; 78 | 79 | transporter.sendMail(mailOptions, function (error, info) { 80 | if (error) { 81 | console.log("Message sent: " + error); 82 | } 83 | console.log("Message sent: " + info.response); 84 | }); 85 | } 86 | 87 | //任务1:统计用户站点js错误达到临界值 88 | async function checkjsErrorResult(options) { 89 | let appKey = new Mongoose.Types.ObjectId(options.appKey); 90 | let sTime = new Date( 91 | new Date().setMinutes(new Date().getMinutes() - options.times) 92 | ); 93 | let eTime = new Date(); 94 | let matchCond = { 95 | createTime: { 96 | $gte: sTime, 97 | $lte: eTime, 98 | }, 99 | appKey: appKey, 100 | }; 101 | 102 | // 查询当前阶段错误率 103 | let r1 = await JsModel.find(matchCond).countDocuments(); 104 | let r2 = await PvModel.find(matchCond).countDocuments(); 105 | let rate1 = isNaN(r1 / r2) ? 0 : r1 / r2; 106 | return rate1; 107 | } 108 | 109 | //任务2:统计用户站点API错误率达到临界值 110 | async function checkApiErrorResult(options) { 111 | let appKey = new Mongoose.Types.ObjectId(options.appKey); 112 | let sTime = new Date( 113 | new Date().setMinutes(new Date().getMinutes() - options.times) 114 | ); 115 | let eTime = new Date(); 116 | let matchCond = { 117 | createTime: { 118 | $gte: sTime, 119 | $lte: eTime, 120 | }, 121 | appKey: appKey, 122 | }; 123 | // 查询当前阶段错误率 124 | let r1 = await ApiModel.find( 125 | _.assign({ success: false }, matchCond) 126 | ).countDocuments(); 127 | let r2 = await ApiModel.find(matchCond).countDocuments(); 128 | let rate1 = isNaN(r1 / r2) ? 0 : r1 / r2; 129 | return rate1; 130 | } 131 | 132 | //任务3:统计用户站点平均访问速度达到临界值 133 | async function checkPerfSpeedResult(options) { 134 | let appKey = new Mongoose.Types.ObjectId(options.appKey); 135 | let sTime = new Date( 136 | new Date().setMinutes(new Date().getMinutes() - options.times) 137 | ); 138 | let eTime = new Date(); 139 | let matchCond = { 140 | createTime: { 141 | $gte: sTime, 142 | $lte: eTime, 143 | }, 144 | appKey: appKey, 145 | }; 146 | // 查询当前阶段加载速度平均值 147 | let r1 = await PerfModel.find(matchCond); 148 | let r1Avg = r1.reduce((acc, val) => acc + val.load, 0) / r1.length; 149 | return r1Avg; 150 | } 151 | 152 | //创建/取消发送邮件任务 153 | /* 154 | options.email 邮件 155 | options.alarmType 邮件类型(jsError,apiError,perfSpeed) 156 | options.times 间隔时间(s) 157 | options.state 任务状态(true/false) 158 | options.limitValue 159 | options.appKey 160 | */ 161 | function createTask(options) { 162 | //取消任务 163 | agenda.cancel({ 164 | name: `send alarm email`, 165 | "data.appKey": options.appKey, 166 | "data.alarmType": options.alarmType, 167 | }); 168 | if (!options.state) { 169 | return; 170 | } 171 | (async function () { 172 | await agenda.start(); 173 | let job = agenda.create(`send alarm email`, options); 174 | job.repeatEvery(`${options.times} minutes`, { 175 | skipImmediate: true, 176 | }); 177 | await job.save(); 178 | })(); 179 | } 180 | 181 | exports.startTask = function () { 182 | //定义任务 183 | agenda.define(`send alarm email`, (job, done) => { 184 | sendEmail(job.attrs.data); 185 | done(); 186 | }); 187 | //开启任务 188 | (async function () { 189 | await agenda.start(); 190 | })(); 191 | }; 192 | 193 | exports.createTask = createTask; 194 | -------------------------------------------------------------------------------- /backend_server/business/site.js: -------------------------------------------------------------------------------- 1 | var Mongoose = require("mongoose"); 2 | var SiteModel = require("../models/siteModel"); 3 | var ScheduleTask = require("../business/scheduleTask"); 4 | var util = require("../utils/util"); 5 | var mongoose = require("mongoose"); 6 | /* system listing. */ 7 | exports.list = function (req, res, next) { 8 | if (req.userId === util.adminUserId) { 9 | SiteModel.find(function (err, r) { 10 | if (!err) { 11 | res.json( 12 | util.resJson({ 13 | IsSuccess: true, 14 | Data: r, 15 | }) 16 | ); 17 | } else { 18 | res.json( 19 | util.resJson({ 20 | IsSuccess: false, 21 | Data: null, 22 | }) 23 | ); 24 | } 25 | }); 26 | } else { 27 | SiteModel.find({ userId: req.userId }, function (err, r) { 28 | if (!err) { 29 | res.json( 30 | util.resJson({ 31 | IsSuccess: true, 32 | Data: r, 33 | }) 34 | ); 35 | } else { 36 | res.json( 37 | util.resJson({ 38 | IsSuccess: false, 39 | Data: null, 40 | }) 41 | ); 42 | } 43 | }); 44 | } 45 | }; 46 | 47 | exports.create = function (req, res, next) { 48 | var ObjectId = mongoose.Types.ObjectId; 49 | var id1 = new ObjectId(); 50 | var temp = new SiteModel({ 51 | appName: req.body.appName, 52 | disableHook: false, 53 | disableJS: false, 54 | appKey: id1, 55 | id: id1, 56 | userId: req.userId, 57 | }); 58 | temp.save(function (err, r) { 59 | console.log(err); 60 | 61 | if (!err) { 62 | res.json( 63 | util.resJson({ 64 | IsSuccess: true, 65 | Data: r, 66 | }) 67 | ); 68 | } else { 69 | res.json( 70 | util.resJson({ 71 | IsSuccess: false, 72 | Data: null, 73 | }) 74 | ); 75 | } 76 | }); 77 | }; 78 | 79 | exports.update = function (req, res, next) { 80 | let id = new Mongoose.Types.ObjectId(req.body.id); 81 | SiteModel.findOneAndUpdate( 82 | { 83 | id: id, 84 | }, 85 | { 86 | disableHook: req.body.disableHook, 87 | disableJS: req.body.disableJS, 88 | disableResource: req.body.disableResource, 89 | }, 90 | function (err, r) { 91 | if (!err) { 92 | res.json( 93 | util.resJson({ 94 | IsSuccess: true, 95 | Data: null, 96 | }) 97 | ); 98 | } else { 99 | res.json( 100 | util.resJson({ 101 | IsSuccess: false, 102 | Data: null, 103 | }) 104 | ); 105 | } 106 | } 107 | ); 108 | }; 109 | 110 | //js错误报警 111 | exports.alarmJsErrorUpdate = function (req, res, next) { 112 | let id = new Mongoose.Types.ObjectId(req.body.id); 113 | ScheduleTask.createTask({ 114 | appKey: req.body.id, 115 | email: req.body.email, 116 | alarmType: "jsError", 117 | times: req.body.alarmTimes, 118 | state: req.body.alarmState, 119 | limitValue: req.body.alarmLimit, 120 | }); 121 | 122 | SiteModel.findOneAndUpdate( 123 | { 124 | id: id, 125 | }, 126 | { 127 | alarmJsState: req.body.alarmState, 128 | alarmJsLimit: req.body.alarmLimit, 129 | alarmJsTimes: req.body.alarmTimes, 130 | alarmJsEmail: req.body.email, 131 | }, 132 | function (err, r) { 133 | if (!err) { 134 | res.json( 135 | util.resJson({ 136 | IsSuccess: true, 137 | Data: null, 138 | }) 139 | ); 140 | } else { 141 | res.json( 142 | util.resJson({ 143 | IsSuccess: false, 144 | Data: null, 145 | }) 146 | ); 147 | } 148 | } 149 | ); 150 | }; 151 | 152 | //api错误报警 153 | exports.alarmApiErrorUpdate = function (req, res, next) { 154 | let id = new Mongoose.Types.ObjectId(req.body.id); 155 | ScheduleTask.createTask({ 156 | appKey: req.body.id, 157 | email: req.body.email, 158 | alarmType: "apiError", 159 | times: req.body.alarmTimes, 160 | state: req.body.alarmState, 161 | limitValue: req.body.alarmLimit, 162 | }); 163 | 164 | SiteModel.findOneAndUpdate( 165 | { 166 | id: id, 167 | }, 168 | { 169 | alarmApiState: req.body.alarmState, 170 | alarmApiLimit: req.body.alarmLimit, 171 | alarmApiTimes: req.body.alarmTimes, 172 | alarmApiEmail: req.body.email, 173 | }, 174 | function (err, r) { 175 | if (!err) { 176 | res.json( 177 | util.resJson({ 178 | IsSuccess: true, 179 | Data: null, 180 | }) 181 | ); 182 | } else { 183 | res.json( 184 | util.resJson({ 185 | IsSuccess: false, 186 | Data: null, 187 | }) 188 | ); 189 | } 190 | } 191 | ); 192 | }; 193 | 194 | //perf报警 195 | exports.alarmPerfSpeedUpdate = function (req, res, next) { 196 | let id = new Mongoose.Types.ObjectId(req.body.id); 197 | ScheduleTask.createTask({ 198 | appKey: req.body.id, 199 | email: req.body.email, 200 | alarmType: "perfSpeed", 201 | times: req.body.alarmTimes, 202 | state: req.body.alarmState, 203 | limitValue: req.body.alarmLimit, 204 | }); 205 | 206 | SiteModel.findOneAndUpdate( 207 | { 208 | id: id, 209 | }, 210 | { 211 | alarmPerfState: req.body.alarmState, 212 | alarmPerfLimit: req.body.alarmLimit, 213 | alarmPerfTimes: req.body.alarmTimes, 214 | alarmPerfEmail: req.body.email, 215 | }, 216 | function (err, r) { 217 | if (!err) { 218 | res.json( 219 | util.resJson({ 220 | IsSuccess: true, 221 | Data: null, 222 | }) 223 | ); 224 | } else { 225 | res.json( 226 | util.resJson({ 227 | IsSuccess: false, 228 | Data: null, 229 | }) 230 | ); 231 | } 232 | } 233 | ); 234 | }; 235 | -------------------------------------------------------------------------------- /backend_server/business/userInfo.js: -------------------------------------------------------------------------------- 1 | var Mongoose = require("mongoose"); 2 | var UserModel = require("../models/userModel"); 3 | var util = require("../utils/util"); 4 | var mongoose = require("mongoose"); 5 | var AES = require("crypto-js/aes"); 6 | 7 | exports.createAdmin = async (email) => { 8 | let r = await UserModel.find({ 9 | id: util.adminUserId, 10 | }); 11 | if (r && r.length > 0) { 12 | return; 13 | } 14 | var ObjectId = mongoose.Types.ObjectId; 15 | var id1 = new ObjectId(util.adminUserId); 16 | let token = util.encrypt(id1 + "@" + new Date()); 17 | var temp = new UserModel({ 18 | email: email, 19 | password:`9f2c1604c8ceec9943fd69f96220a8d1`, 20 | id: id1, 21 | token: token, 22 | }); 23 | 24 | temp.save(function (err, r) { 25 | console.log(err); 26 | if (!err) { 27 | console.log("管理员初始化成功"); 28 | } else { 29 | console.log("管理员初始化失败"); 30 | } 31 | }); 32 | }; 33 | 34 | exports.create = async (req, res, next) => { 35 | let body = req.body; 36 | let r = await UserModel.find({ 37 | email: body.email, 38 | }); 39 | if (r && r.length > 0) { 40 | res.json( 41 | util.resJson({ 42 | IsSuccess: false, 43 | Data: "邮箱已存在", 44 | }) 45 | ); 46 | return; 47 | } 48 | var ObjectId = mongoose.Types.ObjectId; 49 | var id1 = new ObjectId(); 50 | let token = util.encrypt(id1 + "@" + new Date()); 51 | var temp = new UserModel({ 52 | email: body.email, 53 | password: body.password, 54 | id: id1, 55 | token: token, 56 | }); 57 | 58 | temp.save(function (err, r) { 59 | console.log(err); 60 | if (!err) { 61 | res.json( 62 | util.resJson({ 63 | IsSuccess: true, 64 | Data: { 65 | userName: body.email, 66 | userId: id1, 67 | token: token, 68 | }, 69 | }) 70 | ); 71 | } else { 72 | res.json( 73 | util.resJson({ 74 | IsSuccess: false, 75 | Data: null, 76 | }) 77 | ); 78 | } 79 | }); 80 | }; 81 | 82 | exports.login = async (req, res, next) => { 83 | let body = req.body; 84 | let r = await UserModel.find({ 85 | email: body.email, 86 | }); 87 | if (r && r.length == 0) { 88 | res.json( 89 | util.resJson({ 90 | IsSuccess: false, 91 | Data: "用户名不存在", 92 | }) 93 | ); 94 | return; 95 | } 96 | if (r[0].password != body.password) { 97 | res.json( 98 | util.resJson({ 99 | IsSuccess: false, 100 | Data: "用户名或密码错误", 101 | }) 102 | ); 103 | return; 104 | } 105 | 106 | if (r[0].password == body.password) { 107 | let token = util.encrypt(r[0].id + "@" + new Date()); 108 | UserModel.findOneAndUpdate( 109 | { 110 | id: new mongoose.Types.ObjectId(r[0].id), 111 | }, 112 | { 113 | token: token, 114 | }, 115 | (err) => { 116 | if (!err) { 117 | res.json( 118 | util.resJson({ 119 | IsSuccess: true, 120 | Data: { 121 | userName: r[0].email, 122 | userId: r[0].id, 123 | token: token, 124 | }, 125 | }) 126 | ); 127 | } else { 128 | res.json( 129 | util.resJson({ 130 | IsSuccess: false, 131 | Data: "更新token失败", 132 | }) 133 | ); 134 | } 135 | } 136 | ); 137 | } 138 | }; 139 | 140 | exports.validToken = async (userId) => { 141 | let r = await UserModel.find({ 142 | id: new mongoose.Types.ObjectId(userId), 143 | }); 144 | if (r && r.length == 0) { 145 | return false; 146 | } 147 | return r[0]; 148 | }; 149 | 150 | exports.logout = async (req, res, next) => { 151 | let query = req.query; 152 | let r = await UserModel.findOneAndUpdate( 153 | { 154 | id: new mongoose.Types.ObjectId(query.id), 155 | }, 156 | { 157 | token: null, 158 | } 159 | ); 160 | if (r) { 161 | res.json( 162 | util.resJson({ 163 | IsSuccess: true, 164 | Data: null, 165 | }) 166 | ); 167 | } else { 168 | res.json( 169 | util.resJson({ 170 | IsSuccess: false, 171 | Data: "更新出错", 172 | }) 173 | ); 174 | } 175 | }; 176 | -------------------------------------------------------------------------------- /backend_server/data/ip2region.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/backend_server/data/ip2region.db -------------------------------------------------------------------------------- /backend_server/docker-build.sh: -------------------------------------------------------------------------------- 1 | docker build -t hubing/monitor-backend . -------------------------------------------------------------------------------- /backend_server/models/apiModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | var schema = new Schema({ 4 | createTime: { type: Date, default: Date.now }, 5 | updateTime: { type: Date, default: Date.now }, 6 | type: String, 7 | page: String, 8 | appKey: Schema.Types.ObjectId, 9 | os: String, 10 | bs: String, 11 | pageWh: String, 12 | ua: String, 13 | city_nameCN: String, 14 | country_nameCN: String, 15 | latitude: Number, 16 | longitude: Number, 17 | mostSpecificSubdivision_nameCN: String, 18 | onlineip: String, 19 | isp: String, 20 | organizationCN: String, 21 | api: String, 22 | success: Boolean, 23 | time: Number, 24 | code: Number, 25 | msg: String, 26 | visitedUserId: String, 27 | }); 28 | 29 | module.exports = mongoose.model('ApiDataInfo', schema); -------------------------------------------------------------------------------- /backend_server/models/consoleModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | var schema = new Schema({ 4 | createTime: { type: Date, default: Date.now }, 5 | updateTime: { type: Date, default: Date.now }, 6 | type: String, 7 | page: String, 8 | appKey: Schema.Types.ObjectId, 9 | os: String, 10 | bs: String, 11 | pageWh: String, 12 | ua: String, 13 | city_nameCN: String, 14 | country_nameCN: String, 15 | latitude: Number, 16 | longitude: Number, 17 | mostSpecificSubdivision_nameCN: String, 18 | onlineip: String, 19 | isp: String, 20 | organizationCN: String, 21 | cType:String, 22 | cMsg: String, 23 | visitedUserId: String, 24 | }); 25 | 26 | module.exports = mongoose.model('ConsoleDataInfo', schema); -------------------------------------------------------------------------------- /backend_server/models/focusClickModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | var schema = new Schema({ 4 | createTime: { type: Date, default: Date.now }, 5 | updateTime: { type: Date, default: Date.now }, 6 | type: String, 7 | page: String, 8 | appKey: Schema.Types.ObjectId, 9 | os: String, 10 | bs: String, 11 | pageWh: String, 12 | ua: String, 13 | city_nameCN: String, 14 | country_nameCN: String, 15 | latitude: Number, 16 | longitude: Number, 17 | mostSpecificSubdivision_nameCN: String, 18 | onlineip: String, 19 | isp: String, 20 | organizationCN: String, 21 | title: String, 22 | href: String, 23 | text: String, 24 | visitedUserId: String, 25 | }); 26 | 27 | module.exports = mongoose.model('FocusClickModel', schema); -------------------------------------------------------------------------------- /backend_server/models/jsModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | var schema = new Schema({ 4 | createTime: { type: Date, default: Date.now }, 5 | updateTime: { type: Date, default: Date.now }, 6 | page: String, 7 | type: String, 8 | appKey: Schema.Types.ObjectId, 9 | os: String, 10 | bs: String, 11 | pageWh: String, 12 | ua: String, 13 | city_nameCN: String, 14 | country_nameCN: String, 15 | latitude: Number, 16 | longitude: Number, 17 | mostSpecificSubdivision_nameCN: String, 18 | onlineip: String, 19 | isp: String, 20 | organizationCN: String, 21 | error: String, 22 | visitedUserId: String, 23 | }); 24 | 25 | module.exports = mongoose.model('JsDataInfo', schema); -------------------------------------------------------------------------------- /backend_server/models/perfModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | var schema = new Schema({ 4 | createTime: { type: Date, default: Date.now }, 5 | updateTime: { type: Date, default: Date.now }, 6 | type: String, 7 | page: String, 8 | appKey: Schema.Types.ObjectId, 9 | os: String, 10 | bs: String, 11 | pageWh: String, 12 | ua: String, 13 | city_nameCN: String, 14 | country_nameCN: String, 15 | latitude: Number, 16 | longitude: Number, 17 | mostSpecificSubdivision_nameCN: String, 18 | onlineip: String, 19 | isp: String, 20 | organizationCN: String, 21 | dns: Number, 22 | tcp: Number, 23 | ssl: Number, 24 | ttfb: Number, 25 | trans: Number, 26 | dom: Number, 27 | res: Number, 28 | firstbyte: Number, 29 | fpt: Number, 30 | tti: Number, 31 | ready: Number, 32 | load: Number, 33 | navt: String, 34 | visitedUserId: String, 35 | }); 36 | 37 | module.exports = mongoose.model('PerfDataInfo', schema); -------------------------------------------------------------------------------- /backend_server/models/pvModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | var schema = new Schema({ 4 | createTime: { type: Date, default: Date.now }, 5 | updateTime: { type: Date, default: Date.now }, 6 | type: String, 7 | page: String, 8 | prePage: String, 9 | appKey: Schema.Types.ObjectId, 10 | os: String, 11 | bs: String, 12 | pageWh: String, 13 | ua: String, 14 | city_nameCN: String, 15 | country_nameCN: String, 16 | latitude: Number, 17 | longitude: Number, 18 | mostSpecificSubdivision_nameCN: String, 19 | onlineip: String, 20 | isp: String, 21 | organizationCN: String, 22 | visitedUserId: String, 23 | }); 24 | 25 | module.exports = mongoose.model('PvDataInfo', schema); -------------------------------------------------------------------------------- /backend_server/models/resourceModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | var schema = new Schema({ 4 | createTime: { type: Date, default: Date.now }, 5 | updateTime: { type: Date, default: Date.now }, 6 | type: String, 7 | page: String, 8 | appKey: Schema.Types.ObjectId, 9 | os: String, 10 | bs: String, 11 | pageWh: String, 12 | ua: String, 13 | city_nameCN: String, 14 | country_nameCN: String, 15 | latitude: Number, 16 | longitude: Number, 17 | mostSpecificSubdivision_nameCN: String, 18 | onlineip: String, 19 | isp: String, 20 | organizationCN: String, 21 | rUrl: String, 22 | rEntryType: String, 23 | rInitiatorType: String, 24 | rDuration: Number, //单位ms 25 | rSize: Number, 26 | visitedUserId: String, 27 | }); 28 | 29 | module.exports = mongoose.model('ResourceDataInfo', schema); -------------------------------------------------------------------------------- /backend_server/models/siteModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | var schema = new Schema({ 4 | appName: String, 5 | disableHook: Boolean, 6 | disableJS: Boolean, 7 | disableResource: Boolean, 8 | createTime: { type: Date, default: Date.now }, 9 | updateTime: { type: Date, default: Date.now }, 10 | appKey: Schema.Types.ObjectId, 11 | id: Schema.Types.ObjectId, 12 | state: String, 13 | userId: String, 14 | 15 | alarmJsState:{ type: Boolean, default: false }, 16 | alarmJsLimit:Number, 17 | alarmJsTimes:Number, 18 | alarmJsEmail:String, 19 | 20 | alarmApiState:{ type: Boolean, default: false }, 21 | alarmApiLimit:Number, 22 | alarmApiTimes:Number, 23 | alarmApiEmail:String, 24 | 25 | alarmPerfState:{ type: Boolean, default: false }, 26 | alarmPerfLimit:Number, 27 | alarmPerfTimes:Number, 28 | alarmPerfEmail:String, 29 | }); 30 | 31 | module.exports = mongoose.model('SiteDataInfo', schema); -------------------------------------------------------------------------------- /backend_server/models/userModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | var schema = new Schema({ 4 | email: String, 5 | password:String, 6 | phone: String, 7 | isAcitve: { type: Boolean, default: false }, 8 | createTime: { type: Date, default: Date.now }, 9 | id: Schema.Types.ObjectId, 10 | token:String 11 | }); 12 | 13 | module.exports = mongoose.model('UserDataInfo', schema); -------------------------------------------------------------------------------- /backend_server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend-server", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "supervisor ./bin/www", 7 | "start": "pm2 start bin/init.js --name web-monitoring/backend_server", 8 | "stop": "pm2 stop web-monitoring/backend_server", 9 | "restart": "npm run stop && npm run start" 10 | }, 11 | "dependencies": { 12 | "agenda": "^2.0.2", 13 | "cookie-parser": "~1.4.3", 14 | "crypto-js": "^3.1.9-1", 15 | "debug": "~2.6.9", 16 | "evenboy-ip2region": "0.0.2", 17 | "express": "~4.16.0", 18 | "http-errors": "~1.6.2", 19 | "jade": "~1.11.0", 20 | "lodash": "^4.17.10", 21 | "mongoose": "^5.2.9", 22 | "morgan": "~1.9.0", 23 | "nodemailer": "^6.3.0", 24 | "supervisor": "^0.12.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend_server/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /backend_server/routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET home page. */ 5 | router.get('/', function(req, res, next) { 6 | res.render('index', { title: '哈喽' }); 7 | }); 8 | 9 | module.exports = router; -------------------------------------------------------------------------------- /backend_server/routes/upData.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var jsError = require('../business/jsError'); 4 | var pvInfo = require('../business/pvInfo'); 5 | var apiInfo = require('../business/apiInfo'); 6 | var perfInfo = require('../business/perfInfo'); 7 | var resourceInfo = require('../business/resourceInfo'); 8 | var focusClickInfo = require('../business/focusClickInfo'); 9 | var consoleInfo = require('../business/consoleInfo'); 10 | var util = require('../utils/util'); 11 | var _ = require('lodash'); 12 | 13 | let uploadDataTaskList = []; 14 | 15 | const solveData = () => { 16 | const tasks = uploadDataTaskList.splice(0, 5); 17 | tasks.forEach(r => { 18 | switch (r.type) { 19 | case 'pv': 20 | pvInfo.create(r); 21 | break; 22 | case 'js': 23 | jsError.create(r); 24 | break; 25 | case 'api': 26 | apiInfo.create(r); 27 | break; 28 | case 'perf': 29 | perfInfo.create(r); 30 | break; 31 | case 'resource': 32 | resourceInfo.create(r); 33 | break; 34 | case 'focusClick': 35 | focusClickInfo.create(r); 36 | break; 37 | case 'console': 38 | consoleInfo.create(r); 39 | break; 40 | default: 41 | break; 42 | } 43 | }) 44 | } 45 | 46 | // 每10秒执行一次检查 47 | setInterval(() => { 48 | solveData() 49 | }, 10000); 50 | 51 | /*上传数据 */ 52 | router.get('', util.getIp, function (req, res, next) { 53 | let temp = JSON.parse(req.query.paramsJson); 54 | temp = _.extend(temp, req.netInfo); 55 | temp.type = req.query.type; 56 | if (temp.type) { 57 | uploadDataTaskList.push(temp) 58 | } 59 | res.status(200).end(); 60 | }); 61 | 62 | module.exports = router; -------------------------------------------------------------------------------- /backend_server/routes/user.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var router = express.Router(); 3 | var util = require("../utils/util"); 4 | var user = require("../business/userInfo"); 5 | router.post("/login", user.login); 6 | router.post("/register", util.resolveToken, util.needAdminToken, user.create); 7 | router.post("/logout", util.resolveToken, util.needToken, user.logout); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /backend_server/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "mongoDB": { 3 | "address": "127.0.0.1:27017", 4 | "dockerAddress":"database:27017" 5 | }, 6 | "nodemailer": { 7 | "enable":false, 8 | "host": "smtpdm.aliyun.com", 9 | "port": 25, 10 | "secureConnection": true, 11 | "auth": { 12 | "user": "admin@hubing.online", 13 | "pass": "*********" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend_server/views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /backend_server/views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= '你好像走错了' 5 | p 这里什么也没有,o(╥﹏╥)o。 6 | -------------------------------------------------------------------------------- /backend_server/views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | database: 4 | image: mongo 5 | restart: always 6 | volumes: 7 | - ~/data/db:/data/db 8 | networks: 9 | - webapp-network 10 | backend: 11 | image: hubing/monitor-backend 12 | depends_on: 13 | - database 14 | ports: 15 | - 9000:9000 16 | networks: 17 | - webapp-network 18 | web: 19 | image: hubing/monitor-web 20 | ports: 21 | - 9010:80 22 | networks: 23 | - webapp-network 24 | networks: 25 | webapp-network: 26 | driver: bridge 27 | -------------------------------------------------------------------------------- /docker-start.sh: -------------------------------------------------------------------------------- 1 | docker-compose up -d -------------------------------------------------------------------------------- /web/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /dist-server 6 | /tmp 7 | /out-tsc 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | testem.log 35 | /typings 36 | 37 | # e2e 38 | /e2e/*.js 39 | /e2e/*.map 40 | 41 | # System Files 42 | .DS_Store 43 | Thumbs.db 44 | -------------------------------------------------------------------------------- /web/Dockerfile: -------------------------------------------------------------------------------- 1 | # 基于 nginx 镜像(本地打包后,上传dist文件) 2 | 3 | FROM nginx:latest 4 | COPY ./nginx.conf /etc/nginx 5 | COPY ./dist /usr/share/nginx/html -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # MonitorWeb 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.5.3. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /web/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "monitor-web": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets", 21 | "src/favicon.ico", 22 | "src/bundle.js", 23 | { 24 | "glob": "**/*", 25 | "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/", 26 | "output": "/assets/" 27 | } 28 | ], 29 | "styles": [ 30 | "src/styles.scss", 31 | "node_modules/ng-zorro-antd/src/ng-zorro-antd.less" 32 | ], 33 | "scripts": [] 34 | }, 35 | "configurations": { 36 | "production": { 37 | "optimization": true, 38 | "outputHashing": "all", 39 | "sourceMap": false, 40 | "extractCss": true, 41 | "namedChunks": false, 42 | "aot": true, 43 | "extractLicenses": true, 44 | "vendorChunk": false, 45 | "buildOptimizer": true, 46 | "fileReplacements": [ 47 | { 48 | "replace": "src/environments/environment.ts", 49 | "with": "src/environments/environment.prod.ts" 50 | } 51 | ] 52 | }, 53 | "docker": { 54 | "optimization": true, 55 | "outputHashing": "all", 56 | "sourceMap": false, 57 | "extractCss": true, 58 | "namedChunks": false, 59 | "aot": true, 60 | "extractLicenses": true, 61 | "vendorChunk": false, 62 | "buildOptimizer": true, 63 | "fileReplacements": [ 64 | { 65 | "replace": "src/environments/environment.ts", 66 | "with": "src/environments/environment.docker.ts" 67 | } 68 | ] 69 | } 70 | } 71 | }, 72 | "serve": { 73 | "builder": "@angular-devkit/build-angular:dev-server", 74 | "options": { 75 | "browserTarget": "monitor-web:build" 76 | }, 77 | "configurations": { 78 | "production": { 79 | "browserTarget": "monitor-web:build:production" 80 | } 81 | } 82 | }, 83 | "extract-i18n": { 84 | "builder": "@angular-devkit/build-angular:extract-i18n", 85 | "options": { 86 | "browserTarget": "monitor-web:build" 87 | } 88 | }, 89 | "test": { 90 | "builder": "@angular-devkit/build-angular:karma", 91 | "options": { 92 | "main": "src/test.ts", 93 | "karmaConfig": "./karma.conf.js", 94 | "polyfills": "src/polyfills.ts", 95 | "tsConfig": "src/tsconfig.spec.json", 96 | "scripts": [], 97 | "styles": [ 98 | "src/styles.scss", 99 | "node_modules/ng-zorro-antd/src/ng-zorro-antd.less" 100 | ], 101 | "assets": [ 102 | "src/assets", 103 | "src/favicon.ico" 104 | ] 105 | } 106 | }, 107 | "lint": { 108 | "builder": "@angular-devkit/build-angular:tslint", 109 | "options": { 110 | "tsConfig": [ 111 | "src/tsconfig.app.json", 112 | "src/tsconfig.spec.json" 113 | ], 114 | "exclude": [ 115 | "**/node_modules/**" 116 | ] 117 | } 118 | } 119 | } 120 | }, 121 | "monitor-web-e2e": { 122 | "root": "e2e", 123 | "sourceRoot": "e2e", 124 | "projectType": "application", 125 | "architect": { 126 | "e2e": { 127 | "builder": "@angular-devkit/build-angular:protractor", 128 | "options": { 129 | "protractorConfig": "./protractor.conf.js", 130 | "devServerTarget": "monitor-web:serve" 131 | } 132 | }, 133 | "lint": { 134 | "builder": "@angular-devkit/build-angular:tslint", 135 | "options": { 136 | "tsConfig": [ 137 | "e2e/tsconfig.e2e.json" 138 | ], 139 | "exclude": [ 140 | "**/node_modules/**" 141 | ] 142 | } 143 | } 144 | } 145 | } 146 | }, 147 | "defaultProject": "monitor-web", 148 | "schematics": { 149 | "@schematics/angular:component": { 150 | "prefix": "app", 151 | "styleext": "scss" 152 | }, 153 | "@schematics/angular:directive": { 154 | "prefix": "app" 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /web/docker-build.sh: -------------------------------------------------------------------------------- 1 | npm install&&npm run build-docker&&docker build -t hubing/monitor-web . -------------------------------------------------------------------------------- /web/e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('monitor-web App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /web/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /web/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /web/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], 20 | fixWebpackSourcePaths: true 21 | }, 22 | 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /web/nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | #user nobody; 3 | worker_processes 1; 4 | 5 | #error_log logs/error.log; 6 | #error_log logs/error.log notice; 7 | #error_log logs/error.log info; 8 | #pid logs/nginx.pid; 9 | 10 | 11 | events { 12 | worker_connections 1024; 13 | } 14 | 15 | 16 | http { 17 | include mime.types; 18 | default_type application/octet-stream; 19 | 20 | #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 21 | # '$status $body_bytes_sent "$http_referer" ' 22 | # '"$http_user_agent" "$http_x_forwarded_for"'; 23 | 24 | #access_log logs/access.log main; 25 | 26 | sendfile on; 27 | #tcp_nopush on; 28 | 29 | #keepalive_timeout 0; 30 | keepalive_timeout 65; 31 | 32 | #gzip on; 33 | 34 | 35 | 36 | # another virtual host using mix of IP-, name-, and port-based configuration 37 | # 38 | server { 39 | listen 80; 40 | server_name localhost; 41 | 42 | location / { 43 | root /usr/share/nginx/html; 44 | index index.html; 45 | } 46 | } 47 | 48 | 49 | # HTTPS server 50 | # 51 | #server { 52 | # listen 443 ssl; 53 | # server_name localhost; 54 | 55 | # ssl_certificate cert.pem; 56 | # ssl_certificate_key cert.key; 57 | 58 | # ssl_session_cache shared:SSL:1m; 59 | # ssl_session_timeout 5m; 60 | 61 | # ssl_ciphers HIGH:!aNULL:!MD5; 62 | # ssl_prefer_server_ciphers on; 63 | 64 | # location / { 65 | # root html; 66 | # index index.html index.htm; 67 | # } 68 | #} 69 | 70 | } 71 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monitor-web", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve --port 9001", 8 | "build": "ng build --prod", 9 | "build-docker": "ng build -c=docker", 10 | "test": "ng test", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular-devkit/core": "^0.6.8", 17 | "@angular/animations": "^6.1.4", 18 | "@angular/cdk": "^6.4.6", 19 | "@angular/cli": "^6.1.5", 20 | "@angular/common": "^6.1.4", 21 | "@angular/compiler": "^6.1.4", 22 | "@angular/core": "^6.1.4", 23 | "@angular/forms": "^6.1.4", 24 | "@angular/http": "^6.1.4", 25 | "@angular/platform-browser": "^6.1.4", 26 | "@angular/platform-browser-dynamic": "^6.1.4", 27 | "@angular/router": "^6.1.4", 28 | "@types/crypto-js": "^3.1.43", 29 | "classlist.js": "^1.1.20150312", 30 | "codemirror": "^5.48.0", 31 | "core-js": "^2.4.1", 32 | "crypto-js": "^3.1.9-1", 33 | "highcharts": "^6.1.0", 34 | "lodash": "^4.17.10", 35 | "ng-zorro-antd": "^1.4.0", 36 | "ngx-cookie-service": "^1.0.10", 37 | "node-sass": "^6.0.1", 38 | "rxjs": "^6.2.2", 39 | "rxjs-compat": "^6.0.0-rc.0", 40 | "web-animations-js": "^2.3.1", 41 | "zone.js": "^0.8.19" 42 | }, 43 | "devDependencies": { 44 | "@angular-devkit/build-angular": "~0.7.0", 45 | "@angular/compiler-cli": "^6.1.4", 46 | "@angular/language-service": "^6.1.4", 47 | "@types/jasmine": "~2.8.3", 48 | "@types/jasminewd2": "~2.0.2", 49 | "@types/node": "~6.0.60", 50 | "codelyzer": "^4.0.1", 51 | "jasmine-core": "~2.8.0", 52 | "jasmine-spec-reporter": "~4.2.1", 53 | "karma": "~2.0.0", 54 | "karma-chrome-launcher": "~2.2.0", 55 | "karma-coverage-istanbul-reporter": "^1.2.1", 56 | "karma-jasmine": "~1.1.0", 57 | "karma-jasmine-html-reporter": "^0.2.2", 58 | "protractor": "~5.1.2", 59 | "ts-node": "~4.1.0", 60 | "tslint": "~5.9.1", 61 | "typescript": "~2.9.2" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /web/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /web/src/app/animations.ts: -------------------------------------------------------------------------------- 1 | import { animate, state, style, transition, trigger, keyframes } from '@angular/animations'; 2 | 3 | // Component transition animations 4 | export const slideInDownAnimation = 5 | trigger('routeAnimation', [ 6 | state('in', style({ transform: 'rotateX(0)' })), 7 | transition('void => *', [ 8 | animate(300, keyframes([ // 回弹的效果 9 | style({ opacity: 0, transform: 'rotateX(90deg)' }), 10 | style({ opacity: 1, transform: 'rotateX(45deg)' }), 11 | style({ opacity: 1, transform: 'rotateX(0)' }) 12 | ])) 13 | ]) 14 | ]); 15 | 16 | export const slideInDownAnimation1 = 17 | trigger('routeAnimation1', [ 18 | state('*', 19 | style({ 20 | opacity: 1, 21 | // transform: 'translateY(0%)' 22 | }) 23 | ), 24 | transition(':enter', [ 25 | style({ 26 | opacity: 0.5, 27 | // transform: 'translateY(50%)' 28 | }), 29 | animate('.6s ease-in') 30 | ]) 31 | ]); -------------------------------------------------------------------------------- /web/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    9 |
  • 10 | 15 | 前端监控平台 20 | 21 | 22 | {{ userName 24 | }}注销 34 | 35 | 43 | 44 |
  • 45 |
46 |
47 |
48 | 60 | 61 | 73 | 74 |
75 | 76 |
77 | -------------------------------------------------------------------------------- /web/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .ant-layout-header { 2 | padding: 0 !important; 3 | } 4 | 5 | .ant-menu-inline, 6 | .ant-menu-vertical, 7 | .ant-menu-vertical-left { 8 | border-right: none !important; 9 | } 10 | 11 | .left-nav-container { 12 | position: fixed; 13 | left: 0; 14 | overflow-y: auto; 15 | overflow-x: hidden; 16 | // border-right: solid 1px #eee; 17 | background: #EAEDF1; 18 | } 19 | 20 | .ant-menu-dark.ant-menu-horizontal { 21 | border-bottom-color: #373d41; 22 | } 23 | 24 | .ant-menu-dark, 25 | .ant-menu-dark .ant-menu-sub { 26 | background: #373d41; 27 | } 28 | 29 | .ant-menu.ant-menu-root.ant-menu-light.ant-menu-inline { 30 | background: #EAEDF1; 31 | color: #333; 32 | li.ant-menu-item { 33 | margin: 0; 34 | font-size: 12px; 35 | &:hover { 36 | color: #333; 37 | background: #F4F6F8; 38 | } 39 | &:after { 40 | border-right: 3px solid #fff; 41 | } 42 | } 43 | .ant-menu-item-selected { 44 | background-color: #fff; 45 | color: #333; 46 | &:after { 47 | border-right: 1px solid #fff; 48 | margin-right: -1px; 49 | } 50 | } 51 | } 52 | 53 | .ant-layout-header { 54 | height: 50px; 55 | line-height: 50px; 56 | } 57 | 58 | .thanks-for-users { 59 | position: fixed; 60 | left: 4px; 61 | bottom: 4px; 62 | // width: 190px; 63 | right: 0px; 64 | z-index: 1000; 65 | padding: 4px; 66 | background: rgba(191, 191, 191, 0.6588235294117647); 67 | .close-tips { 68 | position: absolute; 69 | right: 4px; 70 | top: 4px; 71 | color: #fff; 72 | cursor: pointer; 73 | } 74 | } -------------------------------------------------------------------------------- /web/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { CookieService } from "ngx-cookie-service"; 2 | import { HttpClient } from "@angular/common/http"; 3 | import { NzMessageService } from "ng-zorro-antd"; 4 | import { Router } from "@angular/router"; 5 | import { UserService, Broadcaster } from "./monitor.common.service"; 6 | import { Component, ElementRef, ViewChild, Renderer2 } from "@angular/core"; 7 | 8 | @Component({ 9 | selector: "app-root", 10 | templateUrl: "./app.component.html", 11 | styleUrls: ["./app.component.scss"], 12 | }) 13 | export class AppComponent { 14 | userName = null; 15 | unsubscribe = { 16 | sub0: null, 17 | }; 18 | showTips: boolean = true; 19 | dataItems = [ 20 | { 21 | url: "https://s.imooc.com/Wxv7KHc", 22 | src: "1.png", 23 | title: "两小时学会 Node.js stream", 24 | }, 25 | { 26 | url: "https://s.imooc.com/Wlba9hm", 27 | src: "2.png", 28 | title: "一条龙的 Node·Vue·React 服务器部署", 29 | }, 30 | { 31 | url: "https://s.imooc.com/SNTMyFV", 32 | src: "3.png", 33 | title: "纯正商业级应用 Node.js Koa2开发微信小程序服务端", 34 | }, 35 | { 36 | url: "https://s.imooc.com/SoXuPiZ", 37 | src: "4.png", 38 | title: "前端下一代开发语言TypeScript 从基础到axios实战", 39 | }, 40 | { 41 | url: "https://s.imooc.com/SxjKSih", 42 | src: "5.png", 43 | title: "从基础到实战 手把手带你掌握新版Webpack4.0", 44 | }, 45 | { 46 | url: "https://s.imooc.com/SYa6RSU", 47 | src: "6.png", 48 | title: "JavaScript版 数据结构与算法", 49 | }, 50 | ]; 51 | dataItems1 = [ 52 | { 53 | src: "weixin-gongzhonghao.jpg", 54 | title: "微信公众号", 55 | }, 56 | ]; 57 | constructor( 58 | private user: UserService, 59 | private router: Router, 60 | private msg: NzMessageService, 61 | private http: HttpClient, 62 | private cookie: CookieService, 63 | private broadcaster: Broadcaster 64 | ) {} 65 | ngOnInit(): void { 66 | this.userName = this.user.getUserName(); 67 | this.unsubscribe.sub0 = this.broadcaster.on("refreshUser").subscribe(() => { 68 | this.userName = this.user.getUserName(); 69 | }); 70 | } 71 | 72 | ngOnDestroy(): void { 73 | this.unsubscribe.sub0.unsubscribe(); 74 | } 75 | 76 | logout() { 77 | this.http 78 | .post( 79 | "user/logout", 80 | {}, 81 | { 82 | params: { 83 | id: this.user.getUserId(), 84 | }, 85 | } 86 | ) 87 | .subscribe((data: any) => { 88 | if (data.IsSuccess) { 89 | this.cookie.delete("user"); 90 | this.userName = null; 91 | this.router.navigate(["home"], { replaceUrl: true }); 92 | } else { 93 | this.msg.error(data.Data); 94 | } 95 | }); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /web/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule,ErrorHandler } from '@angular/core'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { LocationStrategy, HashLocationStrategy } from '@angular/common'; 6 | import { NgZorroAntdModule,NZ_I18N, zh_CN } from 'ng-zorro-antd'; 7 | import { registerLocaleData } from '@angular/common'; 8 | import zh from '@angular/common/locales/zh'; 9 | registerLocaleData(zh); 10 | 11 | import { CookieService } from 'ngx-cookie-service'; 12 | import { JwtInterceptorService, Broadcaster, ConfigService, UserService } from './monitor.common.service'; 13 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 14 | import { AppComponent } from './app.component'; 15 | import { PublicModule } from './public/public.module'; 16 | declare var window:any 17 | const routes: Routes = [ 18 | { path: '', pathMatch:'full',redirectTo:'home' }, 19 | { path: 'home', loadChildren:'app/homepage/homepage.module#HomepageModule' }, 20 | { path: 'dashboard', loadChildren:'app/dashboard/dashboard.module#DashboardModule' }, 21 | { path: 'sys/:appKey', loadChildren:'app/web/web.module#WebModule' } 22 | ]; 23 | export class MyErrorHandler implements ErrorHandler { 24 | handleError(error) { 25 | console.error(error); 26 | window.__ml && window.__ml.error && window.__ml.error(error.stack || error); 27 | } 28 | } 29 | @NgModule({ 30 | declarations: [ 31 | AppComponent 32 | ], 33 | imports: [ 34 | BrowserModule, 35 | BrowserAnimationsModule, 36 | HttpClientModule, 37 | PublicModule, 38 | RouterModule.forRoot(routes), 39 | NgZorroAntdModule.forRoot() 40 | ], 41 | providers: [UserService,CookieService,{ provide: NZ_I18N, useValue: zh_CN },{ provide: ErrorHandler, useClass: MyErrorHandler },ConfigService, { provide: LocationStrategy, useClass: HashLocationStrategy }, { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptorService, multi: true }, Broadcaster], 42 | bootstrap: [AppComponent] 43 | }) 44 | export class AppModule { } 45 | -------------------------------------------------------------------------------- /web/src/app/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { PublicModule } from './../public/public.module'; 2 | 3 | import { NgModule } from '@angular/core'; 4 | import { CommonModule } from '@angular/common'; 5 | import { RouterModule, Routes } from '@angular/router'; 6 | import { FormsModule } from '@angular/forms'; 7 | import { NgZorroAntdModule } from 'ng-zorro-antd'; 8 | import { SysListComponent } from './sys-list/sys-list.component'; 9 | const routes: Routes = [ 10 | {path: '', component:SysListComponent} 11 | ]; 12 | @NgModule({ 13 | imports: [ 14 | CommonModule, 15 | RouterModule, 16 | FormsModule, 17 | NgZorroAntdModule, 18 | PublicModule, 19 | RouterModule.forChild(routes) 20 | ], 21 | declarations: [SysListComponent] 22 | }) 23 | export class DashboardModule { } -------------------------------------------------------------------------------- /web/src/app/dashboard/sys-list/sys-list.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 8 |
9 |

10 | 13 |

14 |
15 |
23 | 24 | 30 |
31 |

32 | PV: 33 | {{ item.total_pv_uv?.totalPv || "--" }} 34 |

35 |

36 | UV: 37 | {{ item.total_pv_uv?.totalUv || "--" }} 38 |

39 |
40 | 41 | 44 |
45 |
46 | 47 | {{ item.appName }} 48 | 49 | 50 | 55 | 56 |
57 |
58 |
59 |
60 | 61 | 71 | 72 | 站点名称 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /web/src/app/dashboard/sys-list/sys-list.component.scss: -------------------------------------------------------------------------------- 1 | .total_pv_uv{ 2 | position: absolute; 3 | right: 32px; 4 | color: gray; 5 | z-index: 1; 6 | top: 44px; 7 | p{ 8 | margin-bottom: 4px; 9 | font-weight: bold; 10 | font-size: 12px; 11 | span{ 12 | color: #52c41a; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /web/src/app/dashboard/sys-list/sys-list.component.ts: -------------------------------------------------------------------------------- 1 | import { UserService } from "./../../monitor.common.service"; 2 | import { HighchartConfig } from "./../../model/entity"; 3 | import { Router } from "@angular/router"; 4 | import { Component, OnInit } from "@angular/core"; 5 | import { HttpClient } from "@angular/common/http"; 6 | import { NzMessageService } from "ng-zorro-antd"; 7 | import * as _ from "lodash"; 8 | @Component({ 9 | selector: "app-sys-list", 10 | templateUrl: "./sys-list.component.html", 11 | styleUrls: ["./sys-list.component.scss"], 12 | }) 13 | export class SysListComponent implements OnInit { 14 | sysItems: Array = []; 15 | isVisible_add: boolean = false; 16 | appName = ""; 17 | systemId = ""; 18 | isSpinning: boolean = true; 19 | validUser = true; 20 | isSaving: boolean = false; 21 | pv_uv_config: HighchartConfig; 22 | total_pv_uv: any = {}; 23 | constructor( 24 | private route: Router, 25 | private http: HttpClient, 26 | private msg: NzMessageService, 27 | private user: UserService 28 | ) {} 29 | 30 | ngOnInit() { 31 | this.list(); 32 | } 33 | 34 | gotoSys(item) { 35 | this.isSpinning = true; 36 | this.route.navigate(["sys/" + item.appKey]); 37 | } 38 | 39 | gotoSysSetting(item) { 40 | this.isSpinning = true; 41 | this.route.navigate(["sys/" + item.appKey + "/setting"]); 42 | } 43 | 44 | addSys() { 45 | this.isVisible_add = true; 46 | } 47 | 48 | list() { 49 | this.isSpinning = true; 50 | this.http.post("Monitor/SiteList", {}).subscribe((data: any) => { 51 | if (data.IsSuccess) { 52 | this.sysItems = data.Data; 53 | _.each(this.sysItems, (r) => { 54 | r.isSpinning = true; 55 | this.loadPvUvData(r); 56 | }); 57 | } 58 | this.isSpinning = false; 59 | }); 60 | } 61 | 62 | save() { 63 | if (!this.appName) { 64 | this.msg.info("站点名称必填"); 65 | return; 66 | } 67 | 68 | if (this.sysItems.findIndex((r) => r.appName === this.appName) !== -1) { 69 | this.msg.info("站点名称已存在"); 70 | return; 71 | } 72 | this.isSaving = true; 73 | this.http 74 | .post("Monitor/RegisterSite", { 75 | appName: this.appName, 76 | systemId: this.systemId, 77 | }) 78 | .subscribe( 79 | (data: any) => { 80 | this.isSaving = false; 81 | if (data.IsSuccess) { 82 | this.msg.success("创建成功"); 83 | this.list(); 84 | setTimeout(() => { 85 | scrollTo({ 86 | top: 10000, 87 | left: 0, 88 | behavior: "smooth", 89 | }); 90 | }, 1000); 91 | this.isVisible_add = false; 92 | } else { 93 | this.msg.error("创建失败"); 94 | } 95 | }, 96 | () => { 97 | this.isSaving = false; 98 | this.msg.error("创建失败"); 99 | } 100 | ); 101 | } 102 | 103 | cancel() { 104 | this.isVisible_add = false; 105 | } 106 | //加载PV/UV数据 107 | loadPvUvData(data) { 108 | this.http 109 | .post("Monitor/PvAndUvStatis", { 110 | TimeQuantum: 4, 111 | sTime: "", 112 | eTime: "", 113 | appKey: data.appKey, 114 | }) 115 | .subscribe((d: any) => { 116 | if (d.IsSuccess) { 117 | this.renderPvUvChart(d.Data, data); 118 | } 119 | }); 120 | } 121 | //渲染PV/UV对比图 122 | renderPvUvChart(data, item) { 123 | let tempData = { 124 | pv: [], 125 | uv: [], 126 | }; 127 | 128 | item.total_pv_uv = { 129 | totalPv: data.totalPv, 130 | totalUv: data.totalUv, 131 | }; 132 | _.each(data.pvAndUvVmList, (val) => { 133 | tempData.pv.push([new Date(val.createTime).getTime(), val.pv]); 134 | tempData.uv.push([new Date(val.createTime).getTime(), val.uv]); 135 | }); 136 | item.pv_uv_config = { 137 | type: 10, 138 | ext: { 139 | series: [ 140 | { 141 | name: "PV", 142 | data: tempData.pv, 143 | }, 144 | { 145 | name: "UV", 146 | data: tempData.uv, 147 | }, 148 | ], 149 | legend: false, 150 | chart: { 151 | type: "column", 152 | height: 124, 153 | options3d: { 154 | enabled: true, 155 | depth: 50, 156 | }, 157 | }, 158 | }, 159 | }; 160 | item.isSpinning = false; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /web/src/app/homepage/home-page/homepage.component.scss: -------------------------------------------------------------------------------- 1 | #home-section { 2 | margin-top: 50px; 3 | height: 400px; 4 | position: relative; 5 | background-position: center; 6 | background-size: cover; 7 | border-bottom-left-radius: 4px; 8 | border-bottom-right-radius: 4px; 9 | 10 | #deccription { 11 | position: absolute; 12 | top: 30%; 13 | text-align: center; 14 | left: 0; 15 | right: 0; 16 | height: 500px; 17 | margin: -250px 10% auto; 18 | border-radius: 20px; 19 | padding: 8px; 20 | padding-top: 160px; 21 | 22 | .title { 23 | color: #fff; 24 | font-size: 60px; 25 | } 26 | 27 | .desc { 28 | padding: 40px; 29 | font-size: 20px; 30 | color: #bae7ff; 31 | font-weight: 500; 32 | } 33 | 34 | .opt { 35 | button { 36 | width: 200px; 37 | height: 40px; 38 | line-height: 40px; 39 | color: #fff; 40 | border: solid 1px #fff; 41 | border-radius: 18px; 42 | background: rgba(217, 217, 217, 0); 43 | 44 | &:hover { 45 | background: #fff !important; 46 | color: #333; 47 | cursor: pointer; 48 | transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | #home-section2 { 56 | // min-height: 700px; 57 | padding: 50px; 58 | 59 | h4 { 60 | text-align: center; 61 | font-size: 32px; 62 | color: #333; 63 | letter-spacing: 1px; 64 | margin-bottom: 32px; 65 | } 66 | } 67 | 68 | #footer { 69 | height: 200px; 70 | background: #001529; 71 | color: #eee; 72 | 73 | .grid-fluid { 74 | max-width: 600px; 75 | width: 80%; 76 | margin: auto; 77 | height: 200px; 78 | padding-top: 24px; 79 | 80 | a, 81 | h3 { 82 | color: #eee; 83 | margin-bottom: 16px; 84 | } 85 | 86 | .grid { 87 | display: inline-block; 88 | width: 33.33%; 89 | vertical-align: top; 90 | 91 | ul { 92 | list-style: none; 93 | padding-left: 0; 94 | } 95 | } 96 | } 97 | } 98 | 99 | 100 | .section-contanier { 101 | position: relative; 102 | width: 100%; 103 | } 104 | 105 | .section-overlay { 106 | position: absolute; 107 | } 108 | 109 | 110 | .section-block, 111 | .section-overlay { 112 | height: 100%; 113 | width: 100%; 114 | } 115 | 116 | // Extra small devices (portrait phones, less than 576px) 117 | @media (max-width: 575.98px) { 118 | .title { 119 | font-size: 20px !important; 120 | } 121 | 122 | .opt { 123 | button { 124 | width: 100px !important; 125 | height: 24px !important; 126 | line-height: 24px !important; 127 | border-radius: 8px !important; 128 | } 129 | } 130 | } 131 | 132 | [nz-carousel-content] { 133 | text-align: center; 134 | height: 300px; 135 | color: #fff; 136 | overflow: hidden; 137 | } 138 | -------------------------------------------------------------------------------- /web/src/app/homepage/home-page/homepage.component.ts: -------------------------------------------------------------------------------- 1 | import { debounceTime } from "rxjs/operators"; 2 | import { Broadcaster } from "./../../monitor.common.service"; 3 | import { CookieService } from "ngx-cookie-service"; 4 | import { Router } from "@angular/router"; 5 | import { NzMessageService } from "ng-zorro-antd"; 6 | import { HttpClient } from "@angular/common/http"; 7 | import { Component, OnInit, Renderer2 } from "@angular/core"; 8 | import * as CryptoJS from "crypto-js"; 9 | import { UserService } from "../../monitor.common.service"; 10 | import { Subscription, fromEvent } from "rxjs"; 11 | 12 | interface ICardProps { 13 | avatar: string; 14 | title: string; 15 | description: string; 16 | } 17 | 18 | @Component({ 19 | selector: "app-homepage", 20 | templateUrl: "./homepage.component.html", 21 | styleUrls: ["./homepage.component.scss"], 22 | }) 23 | export class HomepageComponent implements OnInit { 24 | _KEY: "28756942659325487412569845231586"; //32位 25 | _IV: "8536874512548456"; //16位 26 | model = { 27 | email: "", 28 | password: "", 29 | }; 30 | model2 = { 31 | email: "", 32 | password: "", 33 | }; 34 | bussinessItems: Array = []; 35 | whoUsedItems: Array = []; 36 | usedProcessItems: Array = []; 37 | proItems: Array = []; 38 | isVisible_login: boolean = false; 39 | isVisible_register: boolean = false; 40 | isLogin: boolean = false; 41 | unsubscribe: Subscription; 42 | isAdmin = false; 43 | constructor( 44 | private http: HttpClient, 45 | private msg: NzMessageService, 46 | private router: Router, 47 | private cookie: CookieService, 48 | private user: UserService, 49 | private broadcaster: Broadcaster, 50 | private render: Renderer2 51 | ) {} 52 | 53 | ngOnInit() { 54 | this.isAdmin = this.user.isAdmin(); 55 | setTimeout(() => { 56 | this.isLogin = this.user.getToken() ? true : false; 57 | }, 1000); 58 | 59 | this.proItems = [ 60 | { 61 | avatar: "safe.png", 62 | title: "无业务侵入影响", 63 | description: 64 | "项目使用探针植入,自动上报数据,不影响业务系统使用。", 65 | }, 66 | { 67 | avatar: "multi.png", 68 | title: "捕获采集指标丰富", 69 | description: 70 | "日活跃、用户行为记录、访问日志、JS错误日志、API请求详情、访问性能评估。", 71 | }, 72 | { 73 | avatar: "q.png", 74 | title: "快速定位问题", 75 | description: 76 | "提供出错上下文,重现错误场景。", 77 | }, 78 | { 79 | avatar: "alarm.png", 80 | title: "智能报警服务", 81 | description: 82 | "灵活设置报警阈值,即时发现问题,不影响用户正常使用。", 83 | }, 84 | ]; 85 | 86 | this.bussinessItems = [ 87 | { 88 | avatar: "", 89 | title: "前端监控平台个性化定制", 90 | description: 91 | "根据用户要求定制开发前端监控系统,个性化设置图表分析,数据展示,助你及时解决线上问题。(微信/QQ:676022504)", 92 | }, 93 | { 94 | avatar: "", 95 | title: "个人/企业网站开发", 96 | description: 97 | "专注各类软件开发,打造安全优质的网络平台,价格实惠,低于同类竞品。", 98 | }, 99 | { 100 | avatar: "", 101 | title: "小程序开发", 102 | description: "引流新渠道,无需下载,扫码即可使用。", 103 | }, 104 | { 105 | avatar: "", 106 | title: "前/后端/架构咨询服务", 107 | description: 108 | "专注解决开发、线上过程中的各种疑难问题,服务费用低至10元。(微信/QQ:676022504)", 109 | }, 110 | ]; 111 | 112 | this.whoUsedItems = [ 113 | { 114 | avatar: "m.png", 115 | title: "前端性能监控平台", 116 | description: 117 | "从不同维度去统计用户真实访问站点的情况,再现用户访问场景,帮助你快速定位问题。", 118 | }, 119 | { 120 | avatar: "m.png", 121 | title: "cnbook", 122 | description: "【Kindle中国】_Amazon Kindle 完全购买攻略。", 123 | }, 124 | { 125 | avatar: "m.png", 126 | title: "bboss", 127 | description: 128 | "bboss是一个j2ee开源框架,为企业级应用开发提供一站式解决方案,并能有效地支撑移动应用开发。", 129 | }, 130 | { 131 | avatar: "m.png", 132 | title: "酷娃利息对比计算工具", 133 | description: "一款帮你选择贷款方式的多功能利息计算工具。", 134 | }, 135 | { 136 | avatar: "m.png", 137 | title: "IP转换工具", 138 | description: 139 | "可以快速,准确,可靠的将IP转换成位置的工具,并对外提供接口。", 140 | }, 141 | { 142 | avatar: "m.png", 143 | title: "面试一点通", 144 | description: 145 | "全球首个面试资源社区,收录全网面试题,涵盖IT.互联网、金融.财会、娱乐传媒、公务员、教育培训、法律行业、市场营销、职能行政...等等行业。", 146 | }, 147 | { 148 | avatar: "m.png", 149 | title: "小贝找房", 150 | description: "新房、二手房专业咨询网站。", 151 | }, 152 | { 153 | avatar: "m.png", 154 | title: "More", 155 | description: "更多私有项目入驻。", 156 | }, 157 | ]; 158 | 159 | this.usedProcessItems = [ 160 | { 161 | avatar: "app.png", 162 | title: "步骤1:创建应用", 163 | description: "点击立即体验,进入应用中心,创建应用。", 164 | }, 165 | { 166 | avatar: "SDK.png", 167 | title: "步骤2:复制探针", 168 | description: "选择需要监控的指标,复制探针到项目。", 169 | }, 170 | { 171 | avatar: "data.png", 172 | title: "步骤3:开启监控", 173 | description: "进入应用详情,查看各项指标数据。", 174 | }, 175 | ]; 176 | } 177 | 178 | _resizePageHeight() { 179 | this.render.setStyle( 180 | document.querySelector("#home-section"), 181 | "height", 182 | document.body.clientHeight - 50 + "px" 183 | ); 184 | } 185 | 186 | login() { 187 | let pwd = this.encrypt(this.model.password); 188 | this.http 189 | .post("User/login", { 190 | email: this.model.email, 191 | password: pwd, 192 | }) 193 | .subscribe((r: any) => { 194 | if (r.IsSuccess) { 195 | this.cookie.set( 196 | "user", 197 | JSON.stringify(r.Data), 198 | new Date(new Date().setMonth(new Date().getMonth() + 1)) 199 | ); 200 | this.broadcaster.broadcast("refreshUser"); 201 | this.router.navigate(["dashboard"]); 202 | } else { 203 | this.msg.error(r.Data, { nzDuration: 4000 }); 204 | } 205 | }); 206 | } 207 | 208 | ngOnDestroy(): void { 209 | // this.unsubscribe.unsubscribe(); 210 | } 211 | 212 | register() { 213 | let pwd = this.encrypt(this.model2.password); 214 | this.http 215 | .post("User/register", { 216 | email: this.model2.email, 217 | password: pwd, 218 | }) 219 | .subscribe((r: any) => { 220 | if (r.IsSuccess) { 221 | this.cookie.set( 222 | "user", 223 | JSON.stringify(r.Data), 224 | new Date(new Date().setMonth(new Date().getMonth() + 1)) 225 | ); 226 | this.broadcaster.broadcast("refreshUser"); 227 | this.router.navigate(["dashboard"]); 228 | } else { 229 | this.msg.error(r.Data, { nzDuration: 4000 }); 230 | } 231 | }); 232 | } 233 | 234 | private encrypt(str) { 235 | var key = CryptoJS.enc.Utf8.parse(this._KEY); 236 | var iv = CryptoJS.enc.Utf8.parse(this._IV); 237 | var encrypted = ""; 238 | var srcs = CryptoJS.enc.Utf8.parse(str); 239 | encrypted = CryptoJS.AES.encrypt(srcs, key, { 240 | iv: iv, 241 | mode: CryptoJS.mode.CBC, 242 | padding: CryptoJS.pad.Pkcs7, 243 | }).ciphertext; 244 | 245 | return encrypted.toString(); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /web/src/app/homepage/homepage.module.ts: -------------------------------------------------------------------------------- 1 | import { PublicModule } from './../public/public.module'; 2 | import { NgZorroAntdModule } from 'ng-zorro-antd'; 3 | import { HomepageComponent } from './home-page/homepage.component'; 4 | 5 | import { NgModule } from '@angular/core'; 6 | import { CommonModule } from '@angular/common'; 7 | import { RouterModule, Routes } from '@angular/router'; 8 | import { FormsModule } from '@angular/forms'; 9 | const routes: Routes = [ 10 | {path: '', component:HomepageComponent} 11 | ]; 12 | @NgModule({ 13 | imports: [ 14 | CommonModule, 15 | RouterModule, 16 | FormsModule, 17 | PublicModule, 18 | NgZorroAntdModule, 19 | RouterModule.forChild(routes) 20 | ], 21 | declarations: [HomepageComponent] 22 | }) 23 | export class HomepageModule { } 24 | -------------------------------------------------------------------------------- /web/src/app/model/entity.ts: -------------------------------------------------------------------------------- 1 | export class UrlNav{ 2 | value:string 3 | label:string 4 | isActive:boolean 5 | icon?:string 6 | constructor(label?:string,value?:string,isActive?:boolean,icon?:string){ 7 | this.value = value; 8 | this.label = label; 9 | this.isActive = isActive; 10 | this.icon = icon; 11 | } 12 | } 13 | 14 | export class HighchartConfig{ 15 | type:number 16 | ext:any 17 | extProps?:any 18 | } 19 | -------------------------------------------------------------------------------- /web/src/app/public/advise-bar/advise-bar.component.html: -------------------------------------------------------------------------------- 1 |
3 | {{setting?.title}} 5 |
6 |
7 | {{setting?.subTitle}} 8 |
10 |
11 | 12 | 13 | 14 |
15 |
-------------------------------------------------------------------------------- /web/src/app/public/advise-bar/advise-bar.component.scss: -------------------------------------------------------------------------------- 1 | .advise-container { 2 | position: fixed; 3 | z-index: 110; 4 | color: #fff; 5 | padding: 16px; 6 | border-radius: 50%; 7 | width: 60px; 8 | height: 60px; 9 | line-height: 1; 10 | font-size: 12px; 11 | border: solid 2px #fff; 12 | box-shadow: 0px 1px 6px 0px rgba(49, 50, 51, 0.47), 0px 1px 3px 0px rgba(186, 218, 255, 0.88); 13 | cursor: pointer; 14 | -webkit-touch-callout: none; 15 | /* iOS Safari */ 16 | -webkit-user-select: none; 17 | /* Chrome/Safari/Opera */ 18 | -khtml-user-select: none; 19 | /* Konqueror */ 20 | -moz-user-select: none; 21 | /* Firefox */ 22 | -ms-user-select: none; 23 | /* Internet Explorer/Edge */ 24 | user-select: none; 25 | 26 | /* Non-prefixed version, currently not supported by any browser */ 27 | .advise { 28 | // width: 600px; 29 | min-height: 200px; 30 | max-height: 400px; 31 | overflow: auto; 32 | position: absolute; 33 | left: 52px; 34 | border: solid 1px #3D93F4; 35 | background: #fff; 36 | color: #3D93F4; 37 | padding:0 4px; 38 | cursor: default; 39 | border-radius: 4px; 40 | box-shadow: 0px 0px 4px; 41 | 42 | a{ 43 | width: 250px; 44 | display: inline-block; 45 | margin: 16px; 46 | img{ 47 | width: 100%; 48 | } 49 | } 50 | } 51 | 52 | .advise-icon { 53 | position: absolute; 54 | left: 42px; 55 | top: 22px; 56 | font-size: 14px; 57 | } 58 | 59 | animation:mymove 2s infinite; 60 | -webkit-animation:mymove 2s infinite; 61 | /*Safari and Chrome*/ 62 | } 63 | 64 | @keyframes mymove { 65 | 0% { 66 | box-shadow: none; 67 | } 68 | 69 | 40% { 70 | box-shadow: 0px 0px 10px #fff; 71 | font-size: 12px; 72 | } 73 | 74 | 80% { 75 | box-shadow: 0px 0px 4px #fff; 76 | } 77 | 78 | 100% { 79 | box-shadow: 0px 0px 2px #fff; 80 | } 81 | } 82 | 83 | @-webkit-keyframes mymove 84 | 85 | /*Safari and Chrome*/ 86 | { 87 | 0% { 88 | box-shadow: none; 89 | } 90 | 91 | 40% { 92 | box-shadow: 0px 0px 10px gray; 93 | } 94 | 95 | 80% { 96 | box-shadow: 0px 0px 4px gray; 97 | } 98 | 99 | 100% { 100 | box-shadow: 0px 0px 2px gray; 101 | } 102 | } 103 | 104 | @media screen and (max-width: 500px) { 105 | .advise-container{ 106 | font-size: 10px; 107 | display:none; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /web/src/app/public/advise-bar/advise-bar.component.ts: -------------------------------------------------------------------------------- 1 | import { debounceTime } from 'rxjs/operators'; 2 | import { Component, OnInit, ViewChild, ElementRef, Renderer2, Input } from '@angular/core'; 3 | import { fromEvent, Subscription } from 'rxjs'; 4 | import { NzMessageService } from 'ng-zorro-antd'; 5 | 6 | @Component({ 7 | selector: 'app-advise-bar', 8 | templateUrl: './advise-bar.component.html', 9 | styleUrls: ['./advise-bar.component.scss'] 10 | }) 11 | export class AdviseBarComponent implements OnInit { 12 | @ViewChild("adviseContainer") adviseContainer: ElementRef; 13 | @Input("setting") setting:{ 14 | title:string, 15 | subTitle:string, 16 | color:string, 17 | left:string, 18 | top:string, 19 | with:number 20 | }; 21 | @Input("data") data:Array<{ 22 | title:string, 23 | url:string, 24 | src:string 25 | }>; 26 | tid; 27 | sub0: Subscription; 28 | showAdvise: boolean = false; 29 | pre = { 30 | pageX: 0, 31 | pageY: 0, 32 | }; 33 | cur = { 34 | pageX: 0, 35 | pageY: 0, 36 | }; 37 | constructor( 38 | private render: Renderer2, 39 | private msg: NzMessageService 40 | ) { } 41 | 42 | ngOnInit() { 43 | this.render.listen(this.adviseContainer.nativeElement, "mousedown", (event) => { 44 | this.pre.pageX = Math.abs((event as any).pageX); 45 | this.pre.pageY = Math.abs((event as any).pageY); 46 | if (event.target.className == "advise-container") 47 | this.tid = 1; 48 | }); 49 | this.render.listen(this.adviseContainer.nativeElement, "mouseup", (event) => { 50 | this.tid = 0; 51 | }); 52 | 53 | this.render.listen(window, "mousemove", (event) => { 54 | if (this.tid) { 55 | this.cur.pageX = Math.abs((event as any).pageX); 56 | this.cur.pageY = Math.abs((event as any).pageY); 57 | let hd = this.cur.pageX - this.pre.pageX; 58 | let vd = this.cur.pageY - this.pre.pageY; 59 | this.render.setStyle(this.adviseContainer.nativeElement, "left", this.adviseContainer.nativeElement.offsetLeft + hd + "px"); 60 | this.render.setStyle(this.adviseContainer.nativeElement, "top", this.adviseContainer.nativeElement.offsetTop + vd + "px"); 61 | this.pre.pageX = Math.abs((event as any).pageX); 62 | this.pre.pageY = Math.abs((event as any).pageY); 63 | } 64 | }); 65 | } 66 | 67 | show(event, flag) { 68 | event.stopPropagation(); 69 | this.showAdvise = flag; 70 | } 71 | 72 | ngOnDestroy(): void { 73 | if (this.sub0) 74 | this.sub0.unsubscribe(); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /web/src/app/public/custom-highchart/custom-highchart.component.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /web/src/app/public/custom-highchart/custom-highchart.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/app/public/custom-highchart/custom-highchart.component.scss -------------------------------------------------------------------------------- /web/src/app/public/line-progress/line-progress.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
{{item.name}}
4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 |
{{item.val}}ms
12 |
13 | 14 | -------------------------------------------------------------------------------- /web/src/app/public/line-progress/line-progress.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .line-progress{ 3 | display: inline-block; 4 | } 5 | 6 | .line-progress-left{ 7 | width: 15%; 8 | } 9 | 10 | .line-progress-center{ 11 | vertical-align: top; 12 | width: 69%; 13 | span{ 14 | display: inline-block; 15 | height: 6px; 16 | margin: 8px 0; 17 | transition: all .5s; 18 | } 19 | 20 | } 21 | 22 | .line-progress-right{ 23 | width: 10%; 24 | text-align: right; 25 | } -------------------------------------------------------------------------------- /web/src/app/public/line-progress/line-progress.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, SimpleChanges } from '@angular/core'; 2 | import * as _ from 'lodash'; 3 | @Component({ 4 | selector: 'app-line-progress', 5 | templateUrl: './line-progress.component.html', 6 | styleUrls: ['./line-progress.component.scss'] 7 | }) 8 | export class LineProgressComponent implements OnInit { 9 | @Input('data') data; 10 | globalColors = [ 11 | '#F0574D', '#C55661', '#9A6686', '#866F99', '#D48E4A', '#BC8D58', '#908D74' , '#588D99', '#378DAE', '#3ADDFA', '#46C4D9', '#AB8173', '#C3715A', '#EF5630', '#716ded', '#4b7696', '#4b968a', '#69bf8c', '#7abf69', '#aebf69', '#bf9d69', '#bf6969', '#bf699d', '#ae69bf', '#7a69bf', '#698cbf', '#69bfbf', '#6ccc6f', '#a2cc6c', '#ccbc6c', '#cc826c', '#cc6cc9', '#966ccc', '#32CD32', '#6c7ccc', '#6cb6cc', '#6ccca9' 12 | ]; 13 | lineData = []; 14 | constructor() { 15 | } 16 | 17 | ngOnInit() { 18 | 19 | } 20 | 21 | ngOnChanges(changes: SimpleChanges): void { 22 | if (changes.data.currentValue && changes.data.currentValue != changes.data.previousValue) { 23 | this.renderLine(); 24 | } 25 | } 26 | private renderLine() { 27 | let totalCount = 0; 28 | _.each(this.data, (d) => { 29 | totalCount += d.val; 30 | }); 31 | let frontDataTotal = 0; 32 | let zeroCount=0; 33 | _.each(this.data, (d, index) => { 34 | d.marginL = (frontDataTotal / totalCount) == 0 ? 0 : (frontDataTotal / totalCount).toFixed(3); 35 | d.percent = (d.val / totalCount) == 0 ? 0 : (d.val / totalCount).toFixed(3); 36 | if(d.percent>0.5){ 37 | d.percent= d.percent-0.01; 38 | } 39 | d.color = this.globalColors[index]; 40 | frontDataTotal += d.val; 41 | // d.percent = index == this.data.length - 1 ? (d.percent - zeroCount*0.002) : d.percent; 42 | }); 43 | _.each(this.data, (val, index) => { 44 | setTimeout(() => { 45 | this.lineData[index] = val; 46 | }, (1 + index) * 100 + 500); 47 | }); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /web/src/app/public/monitor-a-blank.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Host, HostListener, ElementRef, Renderer } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[appMonitorABlank]' 5 | }) 6 | export class MonitorABlankDirective { 7 | 8 | constructor( 9 | private elementRef: ElementRef 10 | ) { } 11 | 12 | @HostListener('click') 13 | onClick() { // 监听宿主元素的点击事件,设置元素背景色 14 | let temp = (window as any).__ml; 15 | if (temp && temp.focusClick) { 16 | temp.focusClick({ 17 | title: this.elementRef.nativeElement.title, 18 | href: this.elementRef.nativeElement.href, 19 | text: this.elementRef.nativeElement.innerText 20 | }); 21 | } 22 | } 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /web/src/app/public/public.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { CustomHighchartComponent } from './custom-highchart/custom-highchart.component'; 4 | import { TimeChoicePanelComponent } from './time-choice-panel/time-choice-panel.component'; 5 | import { NgZorroAntdModule } from 'ng-zorro-antd'; 6 | import { FormsModule } from '@angular/forms'; 7 | import { LineProgressComponent } from './line-progress/line-progress.component'; 8 | import { AdviseBarComponent } from './advise-bar/advise-bar.component'; 9 | import { MonitorABlankDirective } from './monitor-a-blank.directive'; 10 | 11 | @NgModule({ 12 | imports: [ 13 | CommonModule, 14 | NgZorroAntdModule, 15 | FormsModule 16 | ], 17 | declarations: [CustomHighchartComponent, TimeChoicePanelComponent, LineProgressComponent, AdviseBarComponent, MonitorABlankDirective], 18 | exports:[CustomHighchartComponent,TimeChoicePanelComponent,LineProgressComponent,AdviseBarComponent,MonitorABlankDirective] 19 | }) 20 | export class PublicModule { } 21 | -------------------------------------------------------------------------------- /web/src/app/public/time-choice-panel/time-choice-panel.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | 31 |
32 |
33 |
-------------------------------------------------------------------------------- /web/src/app/public/time-choice-panel/time-choice-panel.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/app/public/time-choice-panel/time-choice-panel.component.scss -------------------------------------------------------------------------------- /web/src/app/public/time-choice-panel/time-choice-panel.component.ts: -------------------------------------------------------------------------------- 1 | import { Broadcaster } from './../../monitor.common.service'; 2 | import { Component, OnInit,Input,Output,EventEmitter } from '@angular/core'; 3 | @Component({ 4 | selector: 'app-time-choice-panel', 5 | templateUrl: './time-choice-panel.component.html', 6 | styleUrls: ['./time-choice-panel.component.scss'] 7 | }) 8 | export class TimeChoicePanelComponent implements OnInit { 9 | @Input('time') time:string; 10 | @Output() selectOver = new EventEmitter(); 11 | visible:boolean=false; 12 | customTime:any=[] 13 | radioValue:string 14 | constructor( 15 | ) { } 16 | ngOnInit() { 17 | this.radioValue=this.time; 18 | } 19 | 20 | onOk(result: Date): void { 21 | this.selectOver.emit({ 22 | type:7, 23 | time:result 24 | }); 25 | this.customTime=result; 26 | this.visible=false; 27 | } 28 | ngModelChange(e){ 29 | this.radioValue=e; 30 | if(e!=7){ 31 | this.selectOver.emit({ 32 | type:e 33 | }); 34 | this.visible=false; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /web/src/app/web/web-sys/api-request/api-request.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/app/web/web-sys/api-request/api-request.component.scss -------------------------------------------------------------------------------- /web/src/app/web/web-sys/backend-log/backend-log.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 | 22 | 23 | 24 |
25 | 30 |
31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 |
45 |
46 | 48 | 49 | 50 | 编号 51 | 日志等级 52 | 时间 53 | 日志消息 54 | 操作 55 | 56 | 57 | 58 | 59 | {{data.Id}} 60 | {{data.Level}} 61 | {{data.LogDate.substr(6,13)|date:"yyyy/MM/dd HH:mm:ss"}} 62 | {{data.Message}} 63 | 64 | {{data.Message?.substr(0,100)}} 65 | 66 | 67 | 详细 68 | 69 | 70 | 71 | 72 |
73 | 74 | 75 | 76 | 77 | 日志等级 78 | 79 | {{logDetails?.Level}} 80 | 81 | 日期 82 | 83 | {{logDetails?.LogDate.substr(6,13)|date:"yyyy/MM/dd HH:mm:ss"}} 84 | 85 | Ip 86 | 87 | {{logDetails?.MessageObj.Ip}} 88 | 89 | 方法 90 | 91 | {{logDetails?.MessageObj.Methods||'未知'}} 92 | 93 | 接口 94 | 95 | {{logDetails?.MessageObj.RequestInterface||'未知'}} 96 | 97 | 参数 98 | 99 | {{logDetails?.MessageObj.Params||'未知'}} 100 | 101 | 详细信息 102 | 103 |
{{logDetails?.MessageObj.Exception}}
104 |
105 |
106 |
107 | 108 |
-------------------------------------------------------------------------------- /web/src/app/web/web-sys/backend-log/backend-log.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/app/web/web-sys/backend-log/backend-log.component.scss -------------------------------------------------------------------------------- /web/src/app/web/web-sys/backend-log/backend-log.component.ts: -------------------------------------------------------------------------------- 1 | import { slideInDownAnimation } from './../../../animations'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { HttpClient } from '@angular/common/http'; 4 | 5 | import { Component, OnInit } from '@angular/core'; 6 | import { NzMessageService,NzModalService } from 'ng-zorro-antd'; 7 | import * as _ from 'lodash'; 8 | @Component({ 9 | selector: 'app-backend-log', 10 | templateUrl: './backend-log.component.html', 11 | styleUrls: ['./backend-log.component.scss'], 12 | animations: [slideInDownAnimation] 13 | }) 14 | export class BackendLogComponent implements OnInit { 15 | dataSet = []; 16 | logDetails; 17 | loading = true; 18 | modalLoading = true; 19 | tableScrollSetting = { y: '320px' }; 20 | total = 0; 21 | searchModel = { 22 | bussnessId: '', 23 | logEnum: '', 24 | startTime: null, 25 | endTime: null, 26 | pageSize: 50, 27 | pageIndex: 1, 28 | msg:'' 29 | }; 30 | appKey; 31 | currentSite 32 | constructor( 33 | private http: HttpClient, 34 | private msg: NzMessageService, 35 | private route: ActivatedRoute, 36 | private modalService: NzModalService 37 | ) { } 38 | 39 | ngOnInit() { 40 | this.appKey = this.route.parent.snapshot.paramMap.get("appKey"); 41 | this.searchModel.startTime = new Date(new Date().setDate(new Date().getDate() - 1)); 42 | this.searchModel.endTime = new Date(); 43 | this.getSites(); 44 | } 45 | 46 | private getSites() { 47 | this.http.get("Monitor/SiteList").subscribe((d: any) => { 48 | if (d.IsSuccess) { 49 | let currentSys = _.filter(d.Data, { 'appKey': this.appKey }); 50 | this.currentSite = currentSys[0]; 51 | this.searchModel.bussnessId = this.currentSite.systemId; 52 | if(!this.searchModel.bussnessId){ 53 | this.msg.info("请先在应用设置页面添加业务系统ID"); 54 | return; 55 | } 56 | this.searchData(true); 57 | } 58 | }); 59 | } 60 | 61 | searchModelType(e) { 62 | this.searchModel.logEnum = e; 63 | this.searchData(true); 64 | } 65 | 66 | searchData(reset: boolean = false): void { 67 | if (reset) { 68 | this.searchModel.pageIndex = 1; 69 | } 70 | this.loading = true; 71 | this.http.post("SystemLog/List", this.searchModel).subscribe((data: any) => { 72 | if (data.IsSuccess) { 73 | this.total = data.Data.TotalCount 74 | this.loading = false; 75 | this.dataSet = data.Data.List; 76 | } else { 77 | this.msg.error("数据加载失败"); 78 | } 79 | }); 80 | } 81 | 82 | onOk(data) { 83 | this.searchModel.startTime = data[0]; 84 | this.searchModel.endTime = data[1]; 85 | } 86 | 87 | goDetails(data,tplContent){ 88 | this.modalLoading=true; 89 | this.modalService.info({ 90 | nzTitle: '详情', 91 | nzContent: tplContent, 92 | nzWidth:1000 93 | }); 94 | this.http.post("SystemLog/Details", { 95 | errorGuid:data.Id 96 | }).subscribe((data: any) => { 97 | if (data.IsSuccess) { 98 | this.logDetails = data.Data; 99 | } else { 100 | this.msg.error("数据加载失败"); 101 | } 102 | this.modalLoading=false; 103 | }); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /web/src/app/web/web-sys/js-error-track/js-error-track.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 |
提示:如果源文件经过压缩,可以使用此功能更快找到出错位置。要求上传对应的 *.map文件,此文件不会上传服务器。
7 |
8 |

9 | 10 | 12 | 14 |

15 |

16 | 17 | 18 |

19 |

解析结果:

20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
line:column ==>sourcelinecolumnname
{{currentFile.lineColumn}} ==> {{result?.source}}{{result?.line}}{{result?.column}}{{result?.name}}
38 |
39 |
40 | 41 |
42 |
43 |
44 | 45 |
46 | 47 | 48 | 49 |

HTTP请求 {{item.createTime|date:'yyyy-MM-dd HH:mm:ss'}}

50 |
Url:{{item.api}}
51 |
状态码:{{item.code}}
52 |
耗时:{{item.time}}ms
53 |
用户IP:{{item.onlineip}}
54 |
55 | 56 |

页面跳转 {{item.createTime|date:'yyyy-MM-dd HH:mm:ss'}}

57 |
From:{{item.prePage}}
58 |
To:{{item.page}}
59 |
60 | 61 |

控制台信息 {{item.createTime|date:'yyyy-MM-dd HH:mm:ss'}}

62 |
Type:{{item.cType}}
63 |
Msg:{{item.cMsg}}
64 |
65 |
66 | 67 |

JavaScript执行错误 {{data.createTime|date:'yyyy-MM-dd HH:mm:ss'}}

68 |
Error:{{data.error?.substr(0,100)}}
69 |
70 |
71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |
84 |
85 |
86 |
87 |
88 |
-------------------------------------------------------------------------------- /web/src/app/web/web-sys/js-error-track/js-error-track.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/app/web/web-sys/js-error-track/js-error-track.component.scss -------------------------------------------------------------------------------- /web/src/app/web/web-sys/js-error-track/js-error-track.component.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Component, OnInit, Input } from '@angular/core'; 3 | import { UploadXHRArgs, NzMessageService } from 'ng-zorro-antd'; 4 | import * as CodeMirror from 'codemirror/lib/codemirror'; 5 | import 'codemirror/mode/javascript/javascript'; 6 | @Component({ 7 | selector: 'app-js-error-track', 8 | templateUrl: './js-error-track.component.html', 9 | styleUrls: ['./js-error-track.component.scss'] 10 | }) 11 | export class JsErrorTrackComponent implements OnInit { 12 | @Input("data") data; 13 | currentFile = { 14 | lineColumn: '' 15 | }; 16 | result; 17 | dataSet = []; 18 | codeMirrorEditor; 19 | constructor( 20 | private msg: NzMessageService, 21 | private http: HttpClient 22 | ) { } 23 | 24 | ngOnInit() { 25 | setTimeout(() => { 26 | let myTextarea = document.getElementById('code'); 27 | this.codeMirrorEditor = CodeMirror.fromTextArea(myTextarea, { 28 | mode: 'javascript',//编辑器语言 29 | theme: 'monokai', //编辑器主题 30 | extraKeys: { "Ctrl": "autocomplete" },//ctrl可以弹出选择项 31 | lineNumbers: true//显示行号 32 | }); 33 | }); 34 | } 35 | 36 | resolve() { 37 | if (!this.currentFile.lineColumn) { 38 | this.msg.info('行号/列号不能为空'); 39 | return; 40 | } 41 | let files = (document.getElementById("sourceMapFile") as any).files; 42 | if (files.length < 1) { 43 | this.msg.info('请上传对应*.map文件'); 44 | return; 45 | } 46 | let [line, column] = this.currentFile.lineColumn.split(':'); 47 | const fileReader = new FileReader(); 48 | fileReader.onloadend = () => { 49 | const rawSourceMap = fileReader.result; 50 | // 查找 51 | (window as any).sourceMap.SourceMapConsumer.with(rawSourceMap, null, consumer => { 52 | let result = consumer.originalPositionFor({ 53 | source: "./", 54 | line: +line, 55 | column: +column 56 | }); 57 | let index = consumer._absoluteSources.indexOf(result.source); 58 | this.result = result; 59 | if (index != -1) { 60 | let sourceContent = consumer.sourcesContent[index]; 61 | this.codeMirrorEditor.setValue(sourceContent); 62 | } 63 | }); 64 | }; 65 | fileReader.readAsText(files[0]); 66 | } 67 | 68 | getPath() { 69 | this.http.post("Monitor/JsErrorTrackPath", { 70 | createTime: this.data.createTime, 71 | onlineip: this.data.onlineip, 72 | os: this.data.os, 73 | pageWh: this.data.pageWh, 74 | ua: this.data.ua, 75 | bs: this.data.bs, 76 | appKey: this.data.appKey 77 | }).subscribe((data: any) => { 78 | if (data.IsSuccess) { 79 | data.Data.List.forEach(element => { 80 | if(element.type=='console'){ 81 | element.cMsg=decodeURIComponent(element.cMsg); 82 | } 83 | }); 84 | this.dataSet = data.Data.List; 85 | } else { 86 | this.msg.error("数据加载失败"); 87 | } 88 | }) 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /web/src/app/web/web-sys/js-error/js-error.component.html: -------------------------------------------------------------------------------- 1 |
2 |
8 |

按JS错误时间排行

9 | 14 | 20 | 21 | 22 | 25 | 26 | 27 |
33 | {{ 34 | item.error.substr(0, 60) 35 | }} 36 | 37 | {{ item.createTime | date: "yyyy-MM-dd HH:mm" }} 38 | 39 | 40 |
41 |
46 | 加载更多 47 |
48 |
49 |
50 |
56 |
57 |
58 |
59 |
60 | 61 |
IP:{{ currentSelected?.onlineip }}
62 |
63 | 标识:{{ currentSelected?.visitedUserId }} 64 |
65 |
66 |
67 |
68 | 69 |

{{ currentSelected?.os }}

70 |
71 |
72 |
73 | 74 |

{{ currentSelected?.bs }}

75 |
76 |
77 |
78 | 79 |

{{ currentSelected?.pageWh }}

80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | 90 |

{{ currentSelected?.country_nameCN }}

91 |
92 |
93 |
94 | 95 |

{{ currentSelected?.mostSpecificSubdivision_nameCN }}

96 |
97 |
98 |
99 | 100 |

{{ currentSelected?.city_nameCN }}

101 |
102 |
103 |
104 | 105 |

{{ currentSelected?.isp }}

106 |
107 |
108 |
109 |
110 |
111 |
112 | 113 |
114 |
115 |
116 |
117 |
118 |
119 | -------------------------------------------------------------------------------- /web/src/app/web/web-sys/js-error/js-error.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/app/web/web-sys/js-error/js-error.component.scss -------------------------------------------------------------------------------- /web/src/app/web/web-sys/js-error/js-error.component.ts: -------------------------------------------------------------------------------- 1 | import { JsErrorTrackComponent } from './../js-error-track/js-error-track.component'; 2 | 3 | 4 | import { fromEvent as observableFromEvent, Observable } from 'rxjs'; 5 | import { debounceTime } from 'rxjs/operators'; 6 | import { ActivatedRoute } from '@angular/router'; 7 | import { Broadcaster } from './../../../monitor.common.service'; 8 | import { HttpClient } from '@angular/common/http'; 9 | import { Component, OnInit, ViewChildren, ElementRef, Renderer2 } from '@angular/core'; 10 | import * as _ from 'lodash'; 11 | import { NzMessageService, NzModalService } from 'ng-zorro-antd'; 12 | declare var window: any 13 | @Component({ 14 | selector: 'app-js-error', 15 | templateUrl: './js-error.component.html', 16 | styleUrls: ['./js-error.component.scss'] 17 | }) 18 | export class JsErrorComponent implements OnInit { 19 | @ViewChildren('mydetailsContent') mydetailsContent: Array 20 | unsubscribe = { 21 | sub0: null, 22 | sub1: null 23 | } 24 | isSpinning = { 25 | spin1: true, 26 | spin2: true 27 | }; 28 | appKey; 29 | keywords; 30 | pageIndex=1; 31 | pageSize=50; 32 | jsErrorListData = []; 33 | jsErrorListDataTotal = 0; 34 | currentSelected 35 | constructor( 36 | private render: Renderer2, 37 | private http: HttpClient, 38 | private broadcaster: Broadcaster, 39 | private route: ActivatedRoute, 40 | private msg:NzMessageService, 41 | private modalService:NzModalService 42 | ) { } 43 | 44 | ngOnInit() { 45 | 46 | this.appKey = this.route.parent.snapshot.paramMap.get("appKey"); 47 | this.unsubscribe.sub0 = observableFromEvent(window, "resize").pipe( 48 | debounceTime(100)) 49 | .subscribe((event) => { 50 | this._resizePageHeight(); 51 | }); 52 | 53 | this.unsubscribe.sub1 = this.broadcaster.on("choiceTimeToRender").subscribe((data: any) => { 54 | this.loadUserPathList(data.time, data.type); 55 | }); 56 | if (window.globalTime) { 57 | this.loadUserPathList(window.globalTime.time, window.globalTime.type); 58 | } else { 59 | this.loadUserPathList(null, 4); 60 | } 61 | 62 | } 63 | 64 | search() { 65 | this.currentSelected = null; 66 | if (window.globalTime) { 67 | this.loadUserPathList(window.globalTime.time, window.globalTime.type); 68 | } else { 69 | this.loadUserPathList(null, 4); 70 | } 71 | } 72 | 73 | 74 | loadMore() { 75 | let time; 76 | let type; 77 | if (window.globalTime) { 78 | time=window.globalTime.time; 79 | type=window.globalTime.type; 80 | } else { 81 | time=null; 82 | type=4; 83 | } 84 | this.isSpinning.spin1 = true; 85 | this.pageIndex+=1; 86 | this.http.post("Monitor/List", { 87 | TimeQuantum: type == '7' ? '' : type, 88 | type: "js", 89 | sTime: type == '7' ? time[0] : '', 90 | eTime: type == '7' ? time[1] : '', 91 | appKey: this.appKey, 92 | keywords: this.keywords||"", 93 | pageSize: this.pageSize, 94 | pageIndex: this.pageIndex, 95 | }).subscribe((data: any) => { 96 | if (data.IsSuccess) { 97 | if (data.Data.List && data.Data.List.length > 0) { 98 | _.each(data.Data.List, (d) => { 99 | d.error = decodeURIComponent(d.error).replace(/\\n/g, "
"); 100 | }); 101 | this.jsErrorListData = [...this.jsErrorListData,...data.Data.List]; 102 | } 103 | } 104 | this.isSpinning.spin1 = false; 105 | }); 106 | } 107 | 108 | 109 | //获取jsError列表 110 | loadUserPathList(time, type) { 111 | this.isSpinning.spin1 = true; 112 | this.pageIndex=1; 113 | this.http.post("Monitor/List", { 114 | TimeQuantum: type == '7' ? '' : type, 115 | type: "js", 116 | sTime: type == '7' ? time[0] : '', 117 | eTime: type == '7' ? time[1] : '', 118 | appKey: this.appKey, 119 | keywords: this.keywords||"", 120 | pageSize: this.pageSize, 121 | pageIndex: this.pageIndex, 122 | }).subscribe((data: any) => { 123 | if (data.IsSuccess) { 124 | if (data.Data.List && data.Data.List.length > 0) { 125 | data.Data.List[0]['select'] = true; 126 | _.each(data.Data.List, (d) => { 127 | d.error = decodeURIComponent(d.error).replace(/\\n/g, "
"); 128 | }); 129 | this.jsErrorListData = data.Data.List; 130 | this.jsErrorListDataTotal=data.Data.TotalCount; 131 | this.selectListItem(data.Data.List[0]); 132 | } else { 133 | this.currentSelected=null; 134 | this.jsErrorListData = []; 135 | } 136 | } 137 | this.isSpinning.spin1 = false; 138 | }); 139 | } 140 | 141 | selectListItem(data) { 142 | _.each(this.jsErrorListData, (val) => { 143 | val.select = false; 144 | }); 145 | data.select = true; 146 | this.currentSelected = data; 147 | } 148 | 149 | ngAfterViewInit() { 150 | this.broadcaster.broadcast('showGlobalTimer', true); 151 | this._resizePageHeight(); 152 | } 153 | 154 | ngOnDestroy(): void { 155 | this.unsubscribe.sub0.unsubscribe(); 156 | this.unsubscribe.sub1.unsubscribe(); 157 | } 158 | 159 | private _resizePageHeight() { 160 | this.mydetailsContent.forEach(element => { 161 | this.render.setStyle(element.nativeElement, "height", window.innerHeight - 50 - 70 + "px"); 162 | }); 163 | } 164 | 165 | errorTrack(){ 166 | if(!this.currentSelected){ 167 | this.msg.info('选择一条错误数据'); 168 | return; 169 | } 170 | const modal= this.modalService.create({ 171 | nzTitle: '错误场景还原', 172 | nzMaskClosable:false, 173 | nzWidth:1366, 174 | nzContent: JsErrorTrackComponent, 175 | nzZIndex:3000, 176 | nzBodyStyle:{ 177 | padding:0 178 | }, 179 | nzComponentParams: { 180 | data:this.currentSelected 181 | }, 182 | nzFooter:null 183 | }); 184 | } 185 | } 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /web/src/app/web/web-sys/resource-load-details/resource-load-details.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | 8 |
9 |
10 | 11 |
12 |
13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | 27 | 28 | 29 | Name 30 | DateTime 31 | 访客IP 32 | 用户标识 33 | InitiatorType 34 | EntryType 35 | Size 36 | Time 37 | 38 | 39 | 40 | 41 | 42 | 43 | {{data.rUrl?.substr(0,100)}} 44 | 45 |
{{data.rUrl}}
46 |
47 |
48 | 49 | {{data.createTime|date:"yyyy/MM/dd HH:mm:ss"}} 50 | {{data.onlineip}} 51 | {{ data.visitedUserId }} 52 | {{data.rInitiatorType}} 53 | {{data.rEntryType}} 54 | {{(data.rSize||data.rSize==0)&&(data.rSize===0?'from cache':data.rSize+'KB')||''}} 55 | {{data.rDuration}}ms 56 | 57 | 58 |
59 | 共 {{total}} 条数据 60 |
-------------------------------------------------------------------------------- /web/src/app/web/web-sys/resource-load-details/resource-load-details.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/app/web/web-sys/resource-load-details/resource-load-details.component.scss -------------------------------------------------------------------------------- /web/src/app/web/web-sys/resource-load-details/resource-load-details.component.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Broadcaster } from './../../../monitor.common.service'; 3 | import { slideInDownAnimation } from './../../../animations'; 4 | import { ActivatedRoute } from '@angular/router'; 5 | import { HttpClient } from '@angular/common/http'; 6 | import { Component, OnInit } from '@angular/core'; 7 | import { NzMessageService } from 'ng-zorro-antd'; 8 | import * as _ from 'lodash'; 9 | @Component({ 10 | selector: 'app-resource-load-details', 11 | templateUrl: './resource-load-details.component.html', 12 | styleUrls: ['./resource-load-details.component.scss'], 13 | animations: [slideInDownAnimation] 14 | }) 15 | export class ResourceLoadDetailsComponent implements OnInit { 16 | dataSet = []; 17 | loading = true; 18 | tableScrollSetting={ y: '280px',x:'1000px' }; 19 | total=0; 20 | appKey; 21 | searchModel={ 22 | keywords:'', 23 | type:'', 24 | sTime:null, 25 | eTime:null, 26 | appKey:'', 27 | pageSize:100, 28 | pageIndex:1 29 | }; 30 | constructor( 31 | private http:HttpClient, 32 | private msg:NzMessageService, 33 | private route: ActivatedRoute, 34 | private broadcaster:Broadcaster 35 | ) { } 36 | ngOnInit() { 37 | 38 | this.appKey = this.route.parent.snapshot.paramMap.get("appKey"); 39 | this.searchModel.sTime=new Date(new Date().setDate(new Date().getDate()-1)); 40 | this.searchModel.eTime=new Date(); 41 | this.searchModel.appKey=this.appKey; 42 | this.searchData(true); 43 | } 44 | 45 | searchModelType(e){ 46 | this.searchModel.type=e; 47 | this.searchData(true); 48 | } 49 | 50 | searchData(reset:boolean=false): void { 51 | if (reset) { 52 | this.searchModel.pageIndex = 1; 53 | } 54 | this.loading = true; 55 | this.searchModel.keywords=this.searchModel.keywords.trim(); 56 | this.http.post("Monitor/resourceList",this.searchModel).subscribe((data:any) => { 57 | if (data.IsSuccess) { 58 | this.loading = false; 59 | this.total=data.Data.TotalCount; 60 | data.Data.List.forEach(element => { 61 | if(element.rSize){ 62 | element.rSize=new Number(element.rSize/1024).toFixed(2); 63 | } 64 | if(element.rDuration){ 65 | element.rDuration=new Number(element.rDuration).toFixed(2); 66 | }else{ 67 | element.rDuration=0; 68 | } 69 | }); 70 | this.dataSet=data.Data.List; 71 | } else { 72 | this.msg.error("数据加载失败"); 73 | } 74 | }) 75 | } 76 | 77 | ngAfterContentInit(): void { 78 | this.broadcaster.broadcast('showGlobalTimer',false); 79 | (window as any).globalTime=null; 80 | } 81 | 82 | onOk(data){ 83 | this.searchModel.sTime=data[0]; 84 | this.searchModel.eTime=data[1]; 85 | } 86 | 87 | } 88 | 89 | -------------------------------------------------------------------------------- /web/src/app/web/web-sys/sys-index/sys-index.component.scss: -------------------------------------------------------------------------------- 1 | .total_pv_uv{ 2 | position: absolute; 3 | right: 32px; 4 | color: gray; 5 | z-index: 1; 6 | p{ 7 | margin-bottom: 4px; 8 | font-weight: bold; 9 | span{ 10 | color: #52c41a; 11 | padding-left: 4px; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /web/src/app/web/web-sys/sys-setting/sys-setting.component.html: -------------------------------------------------------------------------------- 1 |
2 |

SDK扩展配置项

3 |

4 | 注意:更改以下配置项后,BI探针内容也会相应的变化,需要将BI探针内容重新复制/粘贴到页面HTML中,并且部署后才会生效。 5 |

6 | 7 |
8 | 站点ID: 9 | 10 |
11 |
12 | 手动上报API数据: 13 | 19 |
20 |
21 | 手动上传JS错误: 22 | 28 |
29 |
30 | 上报资源加载情况: 31 | 37 |
38 | 39 |
40 |

报警设置

41 |

注意:修改报警配置后,需要点击保存配置才会生效。

42 |
43 | 站点ID: 44 | 45 |
46 |
47 |

48 | JS错误率报警:   54 |

55 |
56 | 最近分钟:N=,错误率大于 63 | 70 | %,触发JS错误报警。接收人: 保存配置 77 |
78 |
79 |
80 |

81 | API错误率报警:   87 |

88 | 89 |
90 | 最近分钟:N=,错误率大于 97 | %,触发API错误报警。接收人: 保存配置 111 |
112 |
113 |
114 |

115 | Perf访问速度报警:   121 |

122 | 123 |
124 | 最近分钟:N=,Perf访问速度大于 131 | ms,触发访问速度报警。接收人: 保存配置 145 |
146 |
147 | 148 |
149 |

复制/粘贴BI探针

150 |

151 | 复制下方的代码,将其粘贴在页面HTML的body中。需要将代码粘贴在body内容的第一行,重新部署后生效。如需帮助请联系作者。 152 |

153 |
154 | 155 | {{ setting.code }} 156 | 157 |
158 | 159 |

手动上报,勾选上面手动上报配置后,可以使用下面接口手动上报数据:

160 |

手动上报API数据

161 |
162 |
163 |        __ml.api(api,success, time, code, msg):
164 |         api:请求接口
165 |         success:上传是否成功(true/false )
166 |         time:耗时(ms)
167 |         code:返回码
168 |         msg:消息(string/object)
169 |     
170 |
171 |
172 |

手动上传JS错误

173 |
174 |
175 |         __ml.error(errorobj):
176 |         errorobj:js错误对象
177 |     
178 |
179 |
180 | -------------------------------------------------------------------------------- /web/src/app/web/web-sys/sys-setting/sys-setting.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .setting-gutter{ 3 | margin: 16px 8px; 4 | font-size: 14px; 5 | color: #333; 6 | label{ 7 | margin-left: 8px; 8 | } 9 | } 10 | 11 | .setting-code{ 12 | background-color: #f5f5f6; 13 | padding: 16px; 14 | } -------------------------------------------------------------------------------- /web/src/app/web/web-sys/user-path/user-path.component.html: -------------------------------------------------------------------------------- 1 |
2 |
8 |

按用户访问路径数排行

9 | 14 | 20 | 21 | 22 | 25 | 26 | 27 |
33 | {{ item.onlineip }}({{ item.mostSpecificSubdivision_nameCN }}) 36 | 37 | 总路径:{{ item.count }} 38 | 39 | 40 |
41 |
46 | 加载更多 47 |
48 |
49 |
50 |
56 |
57 |
58 |
59 |
60 | 61 |
IP:{{ currentSelectedUserPath?.onlineip }}
62 |
63 | 标识:{{ currentSelectedUserPath?.visitedUserId }} 64 |
65 |
66 |
67 |
68 | 69 |

{{ currentSelectedUserPath?.os }}

70 |
71 |
72 |
73 | 74 |

{{ currentSelectedUserPath?.bs }}

75 |
76 |
77 |
78 | 79 |

{{ currentSelectedUserPath?.pageWh }}

80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | 90 |

{{ currentSelectedUserPath?.country_nameCN }}

91 |
92 |
93 |
94 | 95 |

96 | {{ currentSelectedUserPath?.mostSpecificSubdivision_nameCN }} 97 |

98 |
99 |
100 |
101 | 102 |

{{ currentSelectedUserPath?.city_nameCN }}

103 |
104 |
105 |
106 | 107 |

{{ currentSelectedUserPath?.isp }}

108 |
109 |
110 |
111 |
112 |
113 |
118 |

用户关键访问路径记录:

119 |
120 |
121 | 126 | 141 | 142 |
143 |
144 |
145 | -------------------------------------------------------------------------------- /web/src/app/web/web-sys/user-path/user-path.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/app/web/web-sys/user-path/user-path.component.scss -------------------------------------------------------------------------------- /web/src/app/web/web-sys/user-path/user-path.component.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { fromEvent as observableFromEvent, Observable } from 'rxjs'; 4 | import { debounceTime } from 'rxjs/operators'; 5 | import { ActivatedRoute } from '@angular/router'; 6 | import { Broadcaster } from './../../../monitor.common.service'; 7 | import { HttpClient } from '@angular/common/http'; 8 | import { Component, OnInit, ViewChildren, ElementRef, Renderer2 } from '@angular/core'; 9 | import * as _ from 'lodash'; 10 | declare var window: any 11 | @Component({ 12 | selector: 'app-user-path', 13 | templateUrl: './user-path.component.html', 14 | styleUrls: ['./user-path.component.scss'] 15 | }) 16 | export class UserPathComponent implements OnInit { 17 | @ViewChildren('mydetailsContent') mydetailsContent: Array 18 | unsubscribe = { 19 | sub0: null, 20 | sub1: null 21 | } 22 | isSpinning = { 23 | spin1: true, 24 | spin2: true 25 | }; 26 | appKey; 27 | keywords; 28 | pageIndex=1; 29 | pageSize=100; 30 | userPathListData={ 31 | data:[], 32 | total:0 33 | }; 34 | currentSelectedUserPath 35 | constructor( 36 | private render: Renderer2, 37 | private http: HttpClient, 38 | private broadcaster: Broadcaster, 39 | private route: ActivatedRoute 40 | ) { } 41 | 42 | ngOnInit() { 43 | 44 | this.appKey = this.route.parent.snapshot.paramMap.get("appKey"); 45 | this.unsubscribe.sub0 = observableFromEvent(window, "resize").pipe( 46 | debounceTime(100)) 47 | .subscribe((event) => { 48 | this._resizePageHeight(); 49 | }); 50 | 51 | this.unsubscribe.sub1 = this.broadcaster.on("choiceTimeToRender").subscribe((data: any) => { 52 | this.loadUserPathList(data.time, data.type); 53 | }); 54 | if (window.globalTime) { 55 | this.loadUserPathList(window.globalTime.time, window.globalTime.type); 56 | } else { 57 | this.loadUserPathList(null, 4); 58 | } 59 | 60 | } 61 | 62 | search() { 63 | this.currentSelectedUserPath = null; 64 | if (window.globalTime) { 65 | this.loadUserPathList(window.globalTime.time, window.globalTime.type); 66 | } else { 67 | this.loadUserPathList(null, 4); 68 | } 69 | } 70 | 71 | loadMore() { 72 | let time; 73 | let type; 74 | if (window.globalTime) { 75 | time=window.globalTime.time; 76 | type=window.globalTime.type; 77 | } else { 78 | time=null; 79 | type=4; 80 | } 81 | this.isSpinning.spin1 = true; 82 | this.pageIndex+=1; 83 | this.http.post("Monitor/userPathListStatis", { 84 | TimeQuantum: type == '7' ? '' : type, 85 | sTime: type == '7' ? time[0] : '', 86 | eTime: type == '7' ? time[1] : '', 87 | appKey: this.appKey, 88 | keywords: this.keywords, 89 | pageIndex:this.pageIndex, 90 | pageSize:this.pageSize 91 | }).subscribe((d: any) => { 92 | if (d.IsSuccess) { 93 | if (d.Data && d.Data.data.length > 0) { 94 | this.userPathListData.data = [...this.userPathListData.data,...d.Data.data]; 95 | } 96 | } 97 | this.isSpinning.spin1 = false; 98 | }); 99 | } 100 | 101 | 102 | //获取用户访问列表 103 | loadUserPathList(time, type) { 104 | this.isSpinning.spin1 = true; 105 | this.pageIndex=1; 106 | this.http.post("Monitor/userPathListStatis", { 107 | TimeQuantum: type == '7' ? '' : type, 108 | sTime: type == '7' ? time[0] : '', 109 | eTime: type == '7' ? time[1] : '', 110 | appKey: this.appKey, 111 | keywords: this.keywords, 112 | pageIndex:this.pageIndex, 113 | pageSize:this.pageSize 114 | }).subscribe((d: any) => { 115 | if (d.IsSuccess) { 116 | if (d.Data && d.Data.data.length > 0) { 117 | d.Data.data[0]['select'] = true; 118 | this.userPathListData = d.Data; 119 | this.selectUserPathListItem(d.Data.data[0]); 120 | } else { 121 | this.userPathListData.data = []; 122 | this.userPathListData.total =0; 123 | } 124 | 125 | } 126 | this.isSpinning.spin1 = false; 127 | }); 128 | } 129 | 130 | selectUserPathListItem(data) { 131 | _.each(this.userPathListData.data, (val) => { 132 | val.select = false; 133 | }); 134 | data.select = true; 135 | this.currentSelectedUserPath = data; 136 | } 137 | 138 | 139 | ngAfterViewInit() { 140 | this.broadcaster.broadcast('showGlobalTimer', true); 141 | this._resizePageHeight(); 142 | } 143 | ngOnDestroy(): void { 144 | this.unsubscribe.sub0.unsubscribe(); 145 | this.unsubscribe.sub1.unsubscribe(); 146 | } 147 | 148 | private _resizePageHeight() { 149 | this.mydetailsContent.forEach(element => { 150 | this.render.setStyle(element.nativeElement, "height", window.innerHeight - 50 - 70 + "px"); 151 | }); 152 | 153 | } 154 | 155 | } 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /web/src/app/web/web-sys/visit-details/visit-details.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | 30 | 31 | 32 |
33 |
34 | 35 |
36 |
37 | 38 | 39 | 40 | 45 | 46 | 47 |
48 |
49 | 50 | 53 | 54 |
55 |
56 | 57 | 70 | 71 | 72 | 时间 73 | 日志类型 74 | 页面地址 75 | 浏览器 76 | 设备 77 | UA 78 | 地域 79 | 用户标识 80 | API 81 | Msg 82 | Code 83 | IsSuccess 84 | Time 85 | Error 86 | 87 | 88 | 89 | 90 | {{ data.createTime | date: "yyyy/MM/dd HH:mm:ss" }} 91 | {{ data.type }} 92 | {{ data.page }} 93 | {{ data.bs }} 94 | {{ data.os }} 95 | {{ data.ua }} 96 | 97 | {{ data.mostSpecificSubdivision_nameCN }}
98 | {{ data.onlineip }} 99 | 100 | {{ data.visitedUserId }} 101 | {{ data.api }} 102 | 103 | 104 | {{ data.msg?.substr(0, 100) }} 105 | 106 |
{{ data.msg }}
107 |
108 |
109 | 110 | {{ data.code }} 111 | {{ data.success }} 112 | {{ data.time }}ms 113 | 114 | 115 | 错误场景还原 118 | 125 | 126 | 127 | 128 |
129 | 共 {{ total }} 条数据 130 |
131 | -------------------------------------------------------------------------------- /web/src/app/web/web-sys/visit-details/visit-details.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/app/web/web-sys/visit-details/visit-details.component.scss -------------------------------------------------------------------------------- /web/src/app/web/web-sys/visit-details/visit-details.component.ts: -------------------------------------------------------------------------------- 1 | import { JsErrorTrackComponent } from './../js-error-track/js-error-track.component'; 2 | import { Broadcaster } from './../../../monitor.common.service'; 3 | import { slideInDownAnimation } from './../../../animations'; 4 | import { ActivatedRoute } from '@angular/router'; 5 | import { HttpClient } from '@angular/common/http'; 6 | import { Component, OnInit } from '@angular/core'; 7 | import { NzMessageService, NzModalService } from 'ng-zorro-antd'; 8 | import * as _ from 'lodash'; 9 | @Component({ 10 | selector: 'app-visit-details', 11 | templateUrl: './visit-details.component.html', 12 | styleUrls: ['./visit-details.component.scss'], 13 | animations: [slideInDownAnimation] 14 | }) 15 | export class VisitDetailsComponent implements OnInit { 16 | dataSet = []; 17 | loading = true; 18 | tableScrollSetting = { y: '320px', x: '1000px' }; 19 | total = 0; 20 | appKey 21 | searchModel = { 22 | keywords: '', 23 | type: 'pv', 24 | sTime: null, 25 | eTime: null, 26 | pageSize: 100, 27 | pageIndex: 1, 28 | appKey: '' 29 | }; 30 | constructor( 31 | private http: HttpClient, 32 | private msg: NzMessageService, 33 | private route: ActivatedRoute, 34 | private broadcaster: Broadcaster, 35 | private modalService:NzModalService 36 | ) { } 37 | ngOnInit() { 38 | this.appKey = this.route.parent.snapshot.paramMap.get("appKey"); 39 | this.searchModel.type = this.route.snapshot.queryParams["type"] || 'pv'; 40 | this.searchModel.keywords = this.route.snapshot.queryParams["keywords"]==undefined ? '' : decodeURIComponent(this.route.snapshot.queryParams["keywords"]); 41 | this.searchModel.sTime =this.route.snapshot.queryParams["sTime"]==undefined?new Date(new Date().setDate(new Date().getDate() - 1)):this.route.snapshot.queryParams["sTime"]; 42 | this.searchModel.eTime =this.route.snapshot.queryParams["sTime"]==undefined? new Date():new Date(new Date(this.route.snapshot.queryParams["sTime"]).setSeconds(new Date(this.route.snapshot.queryParams["sTime"]).getSeconds() +1)); 43 | this.searchModel.appKey = this.appKey; 44 | this.searchData(true); 45 | } 46 | 47 | searchModelType(e) { 48 | if (e == 'api') { 49 | this.tableScrollSetting = { y: '320px', x: '1500px' }; 50 | } else { 51 | this.tableScrollSetting = { y: '320px', x: '1000px' }; 52 | } 53 | this.searchModel.type = e; 54 | this.searchData(true); 55 | } 56 | 57 | searchData(reset: boolean = false): void { 58 | if (reset) { 59 | this.searchModel.pageIndex = 1; 60 | } 61 | this.loading = true; 62 | this.searchModel.keywords = this.searchModel.keywords.trim(); 63 | this.http.post("Monitor/List", this.searchModel).subscribe((data: any) => { 64 | if (data.IsSuccess) { 65 | this.total = data.Data.TotalCount 66 | this.loading = false; 67 | if (this.searchModel.type == 'js') { 68 | _.each(data.Data.List, (d) => { 69 | d.error = decodeURIComponent(d.error).replace(/\\n/g,"
"); 70 | }); 71 | } 72 | this.dataSet = data.Data.List; 73 | } else { 74 | this.msg.error("数据加载失败"); 75 | } 76 | }) 77 | } 78 | 79 | ngAfterContentInit(): void { 80 | this.broadcaster.broadcast('showGlobalTimer', false); 81 | (window as any).globalTime = null; 82 | } 83 | 84 | onOk(data) { 85 | this.searchModel.sTime = data[0]; 86 | this.searchModel.eTime = data[1]; 87 | } 88 | 89 | errorTrack(data){ 90 | const modal= this.modalService.create({ 91 | nzTitle: '错误场景还原', 92 | nzMaskClosable:false, 93 | nzWidth:1366, 94 | nzContent: JsErrorTrackComponent, 95 | nzZIndex:3000, 96 | nzBodyStyle:{ 97 | padding:0 98 | }, 99 | nzComponentParams: { 100 | data:data 101 | }, 102 | nzFooter:null 103 | }); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /web/src/app/web/web-sys/visit-geo/visit-geo.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

按访问量排行

4 | 5 |
6 | {{item.geo}} 7 | 8 | {{item.count}} ({{item.percent}}%) 9 | 10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | JS错误率 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
59 |
60 |
61 |
62 | 63 | -------------------------------------------------------------------------------- /web/src/app/web/web-sys/visit-geo/visit-geo.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/app/web/web-sys/visit-geo/visit-geo.component.scss -------------------------------------------------------------------------------- /web/src/app/web/web-sys/visit-os/visit-os.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 |
7 | {{item.terminal}} 8 | 9 | {{item.count}}({{(item.percent)}}%) 10 | 11 | 12 |
13 |
14 |
15 | 16 | 17 |
18 | {{item.terminal}} 19 | 20 | {{item.count}}({{(item.percent)}}%) 21 | 22 | 23 |
24 |
25 |
26 | 27 | 28 |
29 | {{item.terminal}} 30 | 31 | {{item.count}}({{(item.percent)}}%) 32 | 33 | 34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 |
52 |
53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | JS错误率 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
70 |
71 |
72 |
73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
83 |
84 |
85 |
-------------------------------------------------------------------------------- /web/src/app/web/web-sys/visit-os/visit-os.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/app/web/web-sys/visit-os/visit-os.component.scss -------------------------------------------------------------------------------- /web/src/app/web/web-sys/visit-page/visit-page.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/app/web/web-sys/visit-page/visit-page.component.scss -------------------------------------------------------------------------------- /web/src/app/web/web-sys/visit-speed/visit-speed.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

页面访问速度排行

4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 |
14 | {{item.page}} 15 | 16 | {{item.avgLoad}}ms 17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 |
56 | 66 |
67 |
68 | 69 | 70 |
71 | 72 |
73 |
74 | 75 | 76 | 77 | 地域 78 | 首次渲染(ms) 79 | 80 | 81 | 82 | 83 | {{data.name}} 84 | {{data.fpt}}ms 85 | 86 | 87 | 88 |
89 |
90 |
91 | 92 | 93 | 94 |
95 |
96 |
97 |
98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 浏览器 106 | 首次渲染(ms) 107 | 108 | 109 | 110 | 111 | {{data.terminal}} 112 | {{data.speed}}ms 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 操作系统 124 | 首次渲染(ms) 125 | 126 | 127 | 128 | 129 | {{data.terminal}} 130 | {{data.speed}}ms 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 分辨率 142 | 首次渲染(ms) 143 | 144 | 145 | 146 | 147 | {{data.terminal}} 148 | {{data.speed}}ms 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 |
161 |
162 |
163 |
-------------------------------------------------------------------------------- /web/src/app/web/web-sys/visit-speed/visit-speed.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/app/web/web-sys/visit-speed/visit-speed.component.scss -------------------------------------------------------------------------------- /web/src/app/web/web-sys/web-sys.component.html: -------------------------------------------------------------------------------- 1 | 9 | 13 | << 返回系统列表 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 |
22 | 25 | 28 | 29 | 32 | 34 | 35 |
36 |
37 |
38 | 39 |
40 |

关注也是情,说出您的想法!

41 |
42 | 微信 43 |
44 | 45 |

一分也是爱,期望您的支持!

46 |
47 | 微信: 48 | 微信 49 |
50 |
51 | 支付宝: 52 | 支付宝 53 |
54 |
55 |
56 |
57 | 58 | 59 |
    60 |
  • 61 | 62 | 63 | {{item.label}} 64 | 65 |
  • 66 |
67 |
68 |
69 |
70 |
71 | 72 | 73 | 74 | 75 | 76 |
-------------------------------------------------------------------------------- /web/src/app/web/web-sys/web-sys.component.scss: -------------------------------------------------------------------------------- 1 | .ant-layout-header{ 2 | padding: 0 !important; 3 | } 4 | 5 | .ant-menu-inline, .ant-menu-vertical, .ant-menu-vertical-left{ 6 | border-right: none !important; 7 | } 8 | 9 | .left-nav-container{ 10 | position: fixed; 11 | left: 0; 12 | overflow-y: auto; 13 | overflow-x: hidden; 14 | // border-right: solid 1px #eee; 15 | background:#EAEDF1; 16 | } 17 | 18 | .ant-menu-dark.ant-menu-horizontal{ 19 | border-bottom-color: #373d41; 20 | } 21 | 22 | .ant-menu-dark, .ant-menu-dark .ant-menu-sub{ 23 | background: #373d41; 24 | } 25 | .ant-menu.ant-menu-root.ant-menu-light.ant-menu-inline{ 26 | background:#EAEDF1; 27 | color: #333; 28 | li.ant-menu-item{ 29 | margin: 0; 30 | font-size: 12px; 31 | &:hover{ 32 | color: #333; 33 | background: #F4F6F8; 34 | } 35 | &:after{ 36 | border-right: 3px solid #fff; 37 | } 38 | } 39 | .ant-menu-item-selected{ 40 | background-color: #fff; 41 | color: #333; 42 | &:after{ 43 | border-right: 1px solid #fff; 44 | margin-right: -1px; 45 | } 46 | } 47 | } 48 | 49 | .ant-layout-header{ 50 | height: 50px; 51 | line-height: 50px; 52 | } 53 | 54 | .sys-header{ 55 | margin-left: 200px;height: 70px;background:#fff; 56 | line-height: 70px;border-bottom: solid 2px #e6ebef; 57 | border-left: solid 16px #fff; 58 | border-right: solid 16px #fff;margin-top:50px;border-radius: 24px; 59 | } 60 | 61 | -------------------------------------------------------------------------------- /web/src/app/web/web-sys/web-sys.component.ts: -------------------------------------------------------------------------------- 1 | 2 | import { fromEvent as observableFromEvent, Observable } from 'rxjs'; 3 | 4 | import { debounceTime } from 'rxjs/operators'; 5 | import { Broadcaster } from './../../monitor.common.service'; 6 | import { ActivatedRoute, Router } from '@angular/router'; 7 | import { HttpClient } from '@angular/common/http'; 8 | import { UrlNav } from './../../model/entity'; 9 | 10 | import { Component, ElementRef, ViewChild, Renderer2 } from '@angular/core'; 11 | 12 | 13 | import { NzMessageService } from 'ng-zorro-antd'; 14 | declare var window: any 15 | @Component({ 16 | selector: 'app-web-sys', 17 | templateUrl: './web-sys.component.html', 18 | styleUrls: ['./web-sys.component.scss'] 19 | }) 20 | export class WebSysComponent { 21 | @ViewChild('centerContent') centerContent: ElementRef 22 | @ViewChild('leftNav') leftNav: ElementRef 23 | leftNavItems: Array = []; 24 | sysItems: Array = []; 25 | currentSelectedSys; 26 | showGlobalTimer = true; 27 | isFullScreen: boolean = false; 28 | unsubscribe = { 29 | sub0: null, 30 | sub1: null 31 | }; 32 | constructor( 33 | private render: Renderer2, 34 | private http: HttpClient, 35 | private msg: NzMessageService, 36 | private route: ActivatedRoute, 37 | private router: Router, 38 | private broadcaster: Broadcaster 39 | ) { 40 | this.leftNavItems = [{ 41 | label: '应用总览', 42 | value: 'index', 43 | isActive: false, 44 | icon: 'laptop' 45 | }, { 46 | label: '访问页面', 47 | value: 'visitPage', 48 | isActive: false, 49 | icon: 'switcher' 50 | }, { 51 | label: '访问速度', 52 | value: 'visitSpeed', 53 | isActive: false, 54 | icon: 'line-chart' 55 | }, 56 | { 57 | label: 'JS错误', 58 | value: 'jsError', 59 | isActive: false, 60 | icon: 'exception' 61 | }, 62 | { 63 | label: 'API请求', 64 | value: 'apiReq', 65 | isActive: false, 66 | icon: 'api' 67 | }, { 68 | label: '地理', 69 | value: 'visitGeo', 70 | isActive: false, 71 | icon: 'global' 72 | }, { 73 | label: '终端', 74 | value: 'visitOs', 75 | isActive: false, 76 | icon: 'ie' 77 | }, { 78 | label: '用户行为追踪', 79 | value: 'userPath', 80 | isActive: false, 81 | icon: 'radius-setting' 82 | }, { 83 | label: '资源加载详情', 84 | value: 'resourceDetails', 85 | isActive: false, 86 | icon: 'radar-chart' 87 | }, { 88 | label: '访问明细', 89 | value: 'visitDetails', 90 | isActive: false, 91 | icon: 'file' 92 | }, { 93 | label: '应用设置', 94 | value: 'setting', 95 | isActive: false, 96 | icon: 'setting' 97 | }]; 98 | } 99 | ngOnInit(): void { 100 | this.currentSelectedSys = this.route.snapshot.paramMap.get('appKey'); 101 | this.getSites(); 102 | this.unsubscribe.sub0 = observableFromEvent(window, "resize").pipe( 103 | debounceTime(100)) 104 | .subscribe((event) => { 105 | this._resizePageHeight(); 106 | }); 107 | this.unsubscribe.sub0 = observableFromEvent(document, "fullscreenchange") 108 | .subscribe((event) => { 109 | this.isFullScreen = this.isfull(); 110 | }); 111 | let temp2 = observableFromEvent(document, "mozfullscreenchange") 112 | .subscribe((event) => { 113 | this.isFullScreen = this.isfull(); 114 | }); 115 | this.unsubscribe.sub0.add(temp2); 116 | let temp3 = observableFromEvent(document, "webkitfullscreenchange") 117 | .subscribe((event) => { 118 | this.isFullScreen = this.isfull(); 119 | }); 120 | this.unsubscribe.sub0.add(temp3); 121 | let temp4 = observableFromEvent(document, "msfullscreenchange") 122 | .subscribe((event) => { 123 | this.isFullScreen = this.isfull(); 124 | }); 125 | 126 | } 127 | ngAfterViewInit() { 128 | this.unsubscribe.sub1 = this.broadcaster.on("showGlobalTimer").subscribe((r: boolean) => { 129 | let temp = setTimeout(() => { 130 | this.showGlobalTimer = r; 131 | clearTimeout(temp); 132 | }); 133 | }); 134 | this._resizePageHeight(); 135 | } 136 | 137 | ngOnDestroy(): void { 138 | this.unsubscribe.sub0.unsubscribe(); 139 | this.unsubscribe.sub1.unsubscribe(); 140 | } 141 | 142 | selectedSysChange(e) { 143 | this.router.navigate(['/sys/' + e + '/index']); 144 | setTimeout(() => { 145 | location.reload(); 146 | }); 147 | } 148 | 149 | selectOver(e) { 150 | window.globalTime = e; 151 | this.broadcaster.broadcast("choiceTimeToRender", e); 152 | } 153 | 154 | fullScreen() { 155 | var el: any = document.documentElement; 156 | var rfs = el.requestFullScreen || el.webkitRequestFullScreen || el.mozRequestFullScreen || el.msRequestFullscreen; 157 | if (typeof rfs != "undefined" && rfs) { 158 | rfs.call(el); 159 | this.isFullScreen = true; 160 | }; 161 | } 162 | 163 | exitFullScreen() { 164 | if (document.exitFullscreen) { 165 | document.exitFullscreen(); 166 | this.isFullScreen = false; 167 | } 168 | } 169 | 170 | private _resizePageHeight() { 171 | this.render.setStyle(this.centerContent.nativeElement, "height", window.innerHeight - 50 - 70 + "px"); 172 | this.render.setStyle(this.leftNav.nativeElement, "height", window.innerHeight - 50 - 70 + "px"); 173 | } 174 | 175 | private getSites() { 176 | this.http.get("Monitor/SiteList").subscribe((d: any) => { 177 | if (d.IsSuccess) { 178 | this.sysItems = d.Data; 179 | } else { 180 | this.msg.error("系统列表加载失败"); 181 | } 182 | }) 183 | } 184 | private isfull() { 185 | return ( 186 | (document as any).fullscreen || 187 | (document as any).fullscreenElement || 188 | (document as any).mozFullScreenElement || 189 | (document as any).webkitFullscreenElement || 190 | null); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /web/src/app/web/web.module.ts: -------------------------------------------------------------------------------- 1 | 2 | import { BackendLogComponent } from './web-sys/backend-log/backend-log.component'; 3 | import { PublicModule } from './../public/public.module'; 4 | 5 | import { NgModule } from '@angular/core'; 6 | import { CommonModule } from '@angular/common'; 7 | import { RouterModule, Routes } from '@angular/router'; 8 | import { FormsModule } from '@angular/forms'; 9 | import { NgZorroAntdModule } from 'ng-zorro-antd'; 10 | import { WebSysComponent } from './web-sys/web-sys.component'; 11 | import { SysIndexComponent } from './web-sys/sys-index/sys-index.component'; 12 | import { SysSettingComponent } from './web-sys/sys-setting/sys-setting.component'; 13 | import { VisitPageComponent } from './web-sys/visit-page/visit-page.component'; 14 | import { VisitSpeedComponent } from './web-sys/visit-speed/visit-speed.component'; 15 | import { JsErrorComponent } from './web-sys/js-error/js-error.component'; 16 | import { ApiRequestComponent } from './web-sys/api-request/api-request.component'; 17 | import { VisitDetailsComponent } from './web-sys/visit-details/visit-details.component'; 18 | import { VisitGeoComponent } from './web-sys/visit-geo/visit-geo.component'; 19 | import { VisitOsComponent } from './web-sys/visit-os/visit-os.component'; 20 | import { ResourceLoadDetailsComponent } from './web-sys/resource-load-details/resource-load-details.component'; 21 | import { UserPathComponent } from './web-sys/user-path/user-path.component'; 22 | import { JsErrorTrackComponent } from './web-sys/js-error-track/js-error-track.component'; 23 | const routes: Routes = [ 24 | { 25 | path: '', component: WebSysComponent, children: [ 26 | { path: '', redirectTo: 'index' }, 27 | //应用总览 28 | { path: 'index', component: SysIndexComponent }, 29 | // 设置 30 | { path: 'setting', component: SysSettingComponent }, 31 | // 用户访问路径 32 | { path: 'userPath', component: UserPathComponent }, 33 | // 访问页面 34 | { path: 'visitPage', component: VisitPageComponent }, 35 | // 访问速度 36 | { path: 'visitSpeed', component: VisitSpeedComponent }, 37 | // 访问明细 38 | { path: 'visitDetails', component: VisitDetailsComponent }, 39 | // js错误 40 | { path: 'jsError', component: JsErrorComponent }, 41 | // api请求 42 | { path: 'apiReq', component: ApiRequestComponent }, 43 | // 地理 44 | { path: 'visitGeo', component: VisitGeoComponent }, 45 | // 终端 46 | { path: 'visitOs', component: VisitOsComponent }, 47 | // 资源加载详情 48 | { path: 'resourceDetails', component: ResourceLoadDetailsComponent }, 49 | // 后端日志 50 | { path: 'backendLog', component: BackendLogComponent } 51 | ] 52 | } 53 | ]; 54 | @NgModule({ 55 | imports: [ 56 | CommonModule, 57 | RouterModule, 58 | FormsModule, 59 | NgZorroAntdModule, 60 | RouterModule.forChild(routes), 61 | PublicModule 62 | ], 63 | entryComponents:[JsErrorTrackComponent], 64 | declarations: [BackendLogComponent,WebSysComponent, SysIndexComponent, SysSettingComponent, VisitPageComponent, VisitSpeedComponent, JsErrorComponent, ApiRequestComponent, ResourceLoadDetailsComponent, VisitDetailsComponent, VisitGeoComponent, VisitOsComponent, UserPathComponent, JsErrorTrackComponent] 65 | }) 66 | export class WebModule { } -------------------------------------------------------------------------------- /web/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/assets/.gitkeep -------------------------------------------------------------------------------- /web/src/assets/images/error404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/assets/images/error404.png -------------------------------------------------------------------------------- /web/src/assets/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/assets/images/github.png -------------------------------------------------------------------------------- /web/src/assets/images/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/assets/images/home.jpg -------------------------------------------------------------------------------- /web/src/assets/images/icon/SDK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/assets/images/icon/SDK.png -------------------------------------------------------------------------------- /web/src/assets/images/icon/alarm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/assets/images/icon/alarm.png -------------------------------------------------------------------------------- /web/src/assets/images/icon/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/assets/images/icon/app.png -------------------------------------------------------------------------------- /web/src/assets/images/icon/data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/assets/images/icon/data.png -------------------------------------------------------------------------------- /web/src/assets/images/icon/m.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/assets/images/icon/m.png -------------------------------------------------------------------------------- /web/src/assets/images/icon/monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/assets/images/icon/monitor.png -------------------------------------------------------------------------------- /web/src/assets/images/icon/multi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/assets/images/icon/multi.png -------------------------------------------------------------------------------- /web/src/assets/images/icon/q.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/assets/images/icon/q.png -------------------------------------------------------------------------------- /web/src/assets/images/icon/safe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/assets/images/icon/safe.png -------------------------------------------------------------------------------- /web/src/assets/images/weixin-gongzhonghao.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/assets/images/weixin-gongzhonghao.jpg -------------------------------------------------------------------------------- /web/src/assets/images/weixin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/assets/images/weixin.png -------------------------------------------------------------------------------- /web/src/assets/images/zhifubao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/assets/images/zhifubao.png -------------------------------------------------------------------------------- /web/src/environments/environment.docker.ts: -------------------------------------------------------------------------------- 1 | 2 | export const environment = { 3 | production: true, 4 | apiUrl:'http://localhost:9000/', 5 | uploadDataUrl:'http://localhost:9000/Up', 6 | jsHackUrl:'http://localhost:9010/bundle.js' 7 | }; -------------------------------------------------------------------------------- /web/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | 2 | export const environment = { 3 | production: true, 4 | apiUrl:'https://backsite.hubing.online/', 5 | uploadDataUrl:'https://backsite.hubing.online/Up', 6 | jsHackUrl:'https://hubing.online/bundle.js' 7 | }; -------------------------------------------------------------------------------- /web/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false, 8 | apiUrl: "http://localhost:9000/", 9 | uploadDataUrl: "http://localhost:9000/Up", 10 | jsHackUrl: "http://localhost:8080/bundle.js", 11 | // jsHackUrl: "https://hubing.online/bundle.js", 12 | }; 13 | -------------------------------------------------------------------------------- /web/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kisslove/web-monitoring/770572cbed4687158e4d405e27e055e090ad3424/web/src/favicon.ico -------------------------------------------------------------------------------- /web/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 前端监控平台 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | -------------------------------------------------------------------------------- /web/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | if (environment.production) { 7 | enableProdMode(); 8 | } 9 | 10 | platformBrowserDynamic().bootstrapModule(AppModule) 11 | .catch(err => console.log(err)); 12 | -------------------------------------------------------------------------------- /web/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | import 'core-js/es6/symbol'; 23 | import 'core-js/es6/object'; 24 | import 'core-js/es6/function'; 25 | import 'core-js/es6/parse-int'; 26 | import 'core-js/es6/parse-float'; 27 | import 'core-js/es6/number'; 28 | import 'core-js/es6/math'; 29 | import 'core-js/es6/string'; 30 | import 'core-js/es6/date'; 31 | import 'core-js/es6/array'; 32 | import 'core-js/es6/regexp'; 33 | import 'core-js/es6/map'; 34 | import 'core-js/es6/weak-map'; 35 | import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Required to support Web Animations `@angular/platform-browser/animations`. 51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 52 | **/ 53 | import 'web-animations-js'; // Run `npm install --save web-animations-js`. 54 | 55 | 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | 64 | /*************************************************************************************************** 65 | * APPLICATION IMPORTS 66 | */ 67 | -------------------------------------------------------------------------------- /web/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import 'codemirror/lib/codemirror.css'; 3 | @import 'codemirror/theme/monokai.css'; 4 | ::-webkit-scrollbar { 5 | height: 6px; 6 | width: 6px; 7 | } 8 | 9 | 10 | ::-webkit-scrollbar-thumb { 11 | background-color: #7777777a; 12 | outline: 1px solid #333; 13 | } 14 | 15 | :focus { 16 | outline: none; 17 | } 18 | 19 | html,body{ 20 | min-width: 1000px !important; 21 | } 22 | 23 | .ant-select-selection, 24 | .ant-input, 25 | .ant-popover-inner, 26 | .ant-btn, 27 | input, 28 | .ant-modal-content, 29 | .ant-input-number, 30 | .ant-radio-button-wrapper:first-child, 31 | .ant-radio-button-wrapper:last-child { 32 | border-radius: 0 !important; 33 | } 34 | 35 | .ant-radio-button-wrapper { 36 | margin: 0 4px !important; 37 | } 38 | 39 | .custom-select { 40 | .ant-select-selection { 41 | color: #000; 42 | font-size: 14px; 43 | border: none !important; 44 | .ant-select-arrow { 45 | color: #000; 46 | font-weight: 900; 47 | } 48 | } 49 | &.ant-select-open .ant-select-selection { 50 | border: none !important; 51 | box-shadow: none !important; 52 | } 53 | } 54 | 55 | .ant-card-head-title { 56 | font-size: 12px !important; 57 | } 58 | 59 | .my-list-card { 60 | .ant-card-body { 61 | padding: 0 !important; 62 | } 63 | } 64 | 65 | .ant-card-head { 66 | min-height: 40px !important; 67 | height: 40px !important; 68 | line-height: 20px !important; 69 | } 70 | 71 | .ant-card { 72 | min-height: 200px !important; 73 | } 74 | 75 | .custom-default-list { 76 | background-color: #f6fafe; 77 | cursor: pointer; 78 | height: 40px; 79 | margin: 4px 0; 80 | padding: 8px; 81 | .left{ 82 | overflow: hidden; 83 | text-overflow: ellipsis; 84 | display: inline-block; 85 | // word-break: break-all; 86 | height: 20px; 87 | } 88 | .right { 89 | position: absolute; 90 | right: 0; 91 | 92 | } 93 | &.selected { 94 | background-color: #40a9ff; 95 | color: #fff; 96 | cursor: default; 97 | } 98 | &:hover:not(.selected) { 99 | background-color: #E6F9FC; 100 | } 101 | } 102 | 103 | .my-ellipsis{ 104 | text-overflow: ellipsis; 105 | overflow: hidden; 106 | display: inline-block; 107 | height: 20px; 108 | word-break: break-all; 109 | } 110 | 111 | .ant-table-thead > tr > th, .ant-table-tbody > tr > td{ 112 | padding: 8px !important; 113 | } 114 | 115 | .ant-tabs-nav .ant-tabs-tab{ 116 | margin: 0 0 0 0 !important; 117 | } 118 | 119 | .ant-list-item-content{ 120 | text-overflow: ellipsis; 121 | overflow: hidden; 122 | word-break: break-all; 123 | } 124 | 125 | .ant-layout-header,.ant-menu-dark, .ant-menu-dark .ant-menu-sub { 126 | color: rgba(255, 255, 255, 0.65); 127 | background: #096dd9 !important; 128 | } 129 | 130 | .CodeMirror{ 131 | height: 220px; 132 | } 133 | 134 | .custom-card{ 135 | display: inline-block !important; 136 | vertical-align: top; 137 | margin: 16px !important; 138 | box-shadow: 0 0 20px 0 rgb(223,229,232); 139 | transition: all .3s; 140 | transform: translateY(0); 141 | border: none !important; 142 | &:hover{ 143 | box-shadow: 0 10px 10px #aaa; 144 | transform: translateY(-8px); 145 | } 146 | } -------------------------------------------------------------------------------- /web/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /web/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /web/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "types": [ 8 | "jasmine", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "test.ts", 14 | "polyfills.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /web/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2017", 16 | "dom" 17 | ], 18 | "module": "es2015", 19 | "baseUrl": "./" 20 | } 21 | } -------------------------------------------------------------------------------- /web/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "directive-selector": [ 120 | true, 121 | "attribute", 122 | "app", 123 | "camelCase" 124 | ], 125 | "component-selector": [ 126 | true, 127 | "element", 128 | "app", 129 | "kebab-case" 130 | ], 131 | "no-output-on-prefix": true, 132 | "use-input-property-decorator": true, 133 | "use-output-property-decorator": true, 134 | "use-host-property-decorator": true, 135 | "no-input-rename": true, 136 | "no-output-rename": true, 137 | "use-life-cycle-interface": true, 138 | "use-pipe-transform-interface": true, 139 | "component-class-suffix": true, 140 | "directive-class-suffix": true 141 | } 142 | } 143 | --------------------------------------------------------------------------------