├── .autod.conf.js ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── app.js ├── app ├── cache │ ├── web_ip_city_cache_file.txt │ └── wx_ip_city_cache_file.txt ├── controller │ ├── api │ │ ├── emails.js │ │ ├── errors.js │ │ ├── remove.js │ │ ├── system.js │ │ ├── user.js │ │ ├── web │ │ │ ├── ajax.js │ │ │ ├── analysis.js │ │ │ ├── environment.js │ │ │ ├── error.js │ │ │ ├── pages.js │ │ │ ├── pvuvip.js │ │ │ ├── report.js │ │ │ └── resource.js │ │ └── wx │ │ │ ├── ajax.js │ │ │ ├── analysis.js │ │ │ ├── error.js │ │ │ ├── pages.js │ │ │ ├── pvuvip.js │ │ │ └── report.js │ └── web │ │ ├── home.js │ │ ├── web.js │ │ └── wx.js ├── extend │ └── application.js ├── middleware │ └── token_required.js ├── model │ ├── Web │ │ ├── web_ajaxs.js │ │ ├── web_environment.js │ │ ├── web_errors.js │ │ ├── web_pages.js │ │ ├── web_pvuvip.js │ │ ├── web_report.js │ │ ├── web_resource.js │ │ └── web_statis.js │ ├── Wx │ │ ├── wx_ajaxs.js │ │ ├── wx_errors.js │ │ ├── wx_pages.js │ │ ├── wx_pvuvip.js │ │ ├── wx_report.js │ │ └── wx_statis.js │ ├── email.js │ ├── ip_library.js │ ├── system.js │ └── user.js ├── public │ ├── css │ │ └── base.css │ ├── file │ │ ├── web-report-axios.min.js.zip │ │ ├── web-report-default.min.js.zip │ │ ├── web-report-fetch.min.js.zip │ │ ├── web-report-jquery.min.js.zip │ │ ├── web-report-none.min.js.zip │ │ ├── web-report-vue.min.zip │ │ └── wx-report-sdk.min.js.zip │ ├── font │ │ └── JTUSjIg1_i6t8kCHKm459WlhzQ.woff │ ├── img │ │ ├── PopLayer-close.png │ │ ├── github.png │ │ ├── icon-1.png │ │ ├── icon-2.png │ │ ├── icon-3.png │ │ ├── loading.gif │ │ ├── logo.png │ │ ├── logo_1.png │ │ ├── wechat.png │ │ └── weibo.png │ ├── js │ │ ├── china.js │ │ ├── config.js │ │ ├── util.js │ │ ├── vue-components.js │ │ └── vue-filters.js │ └── lib │ │ ├── bootstrap │ │ ├── css │ │ │ ├── bootstrap-theme.css │ │ │ ├── bootstrap-theme.css.map │ │ │ ├── bootstrap-theme.min.css │ │ │ ├── bootstrap-theme.min.css.map │ │ │ ├── bootstrap.css │ │ │ ├── bootstrap.css.map │ │ │ ├── bootstrap.min.css │ │ │ └── bootstrap.min.css.map │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ └── js │ │ │ ├── bootstrap.js │ │ │ ├── bootstrap.min.js │ │ │ └── npm.js │ │ ├── calendar │ │ ├── zane-calendar.min.css │ │ └── zane-calendar.min.js │ │ ├── page │ │ ├── page.css │ │ └── page.js │ │ └── popup │ │ ├── popup.css │ │ └── popup.js ├── router.js ├── router │ ├── api.js │ ├── home.js │ ├── web │ │ ├── api.js │ │ └── web.js │ └── wx │ │ ├── api.js │ │ └── web.js ├── schedule │ ├── delete_report.js │ ├── ip_task.js │ ├── pvuvip_pre_day.js │ ├── pvuvip_pre_minute.js │ ├── report_task_mongodb.js │ └── report_task_redis.js ├── service │ ├── cache.js │ ├── emails.js │ ├── errors.js │ ├── ldap.js │ ├── remove.js │ ├── system.js │ ├── user.js │ ├── web │ │ ├── ajaxs.js │ │ ├── analysis.js │ │ ├── environment.js │ │ ├── errors.js │ │ ├── ip_task.js │ │ ├── pages.js │ │ ├── pvuvip.js │ │ ├── pvuvip_task.js │ │ ├── report.js │ │ ├── report_task.js │ │ ├── resource.js │ │ └── send_email.js │ └── wx │ │ ├── ajaxs.js │ │ ├── analysis.js │ │ ├── errors.js │ │ ├── ip_task.js │ │ ├── pages.js │ │ ├── pvuvip.js │ │ ├── pvuvip_task.js │ │ ├── report.js │ │ ├── report_task.js │ │ └── send_email.js ├── util.js └── view │ ├── emails.html │ ├── errors.html │ ├── footer.html │ ├── github.html │ ├── header.html │ ├── home.html │ ├── layout.html │ ├── login.html │ ├── selectype.html │ ├── systems.html │ ├── users.html │ ├── web │ ├── addsystem.html │ ├── ajaxavg.html │ ├── ajaxdetail.html │ ├── ajaxitemdetail.html │ ├── alarm.html │ ├── analysisdetail.html │ ├── analysislist.html │ ├── diagram.html │ ├── erroravg.html │ ├── errordetail.html │ ├── erroritemdetail.html │ ├── home.html │ ├── pagedetails.html │ ├── pagesavg.html │ ├── pageslist.html │ ├── resourcesavg.html │ ├── resourcesdetail.html │ ├── resourcesitemdetail.html │ ├── setting.html │ ├── side.html │ ├── slowpageslist.html │ └── top.html │ └── wx │ ├── addsystem.html │ ├── ajaxavg.html │ ├── ajaxdetail.html │ ├── ajaxitemdetail.html │ ├── analysisdetail.html │ ├── analysislist.html │ ├── diagram.html │ ├── erroravg.html │ ├── errordetail.html │ ├── erroritemdetail.html │ ├── home.html │ ├── pagedetails.html │ ├── pagesavg.html │ ├── pageslist.html │ ├── setting.html │ ├── side.html │ └── top.html ├── appveyor.yml ├── config ├── config.default.js ├── config.prod.js └── plugin.js ├── demo ├── 01.png ├── 02.png ├── 03.png ├── 04.png ├── 05.png ├── 06.png ├── 07.png ├── 08.png ├── 09.png ├── 10.png ├── 11.png ├── 12.png ├── 20.png ├── 21.png └── ewm.jpg ├── docker-compose.yml ├── docs ├── SDK.html ├── colony.md ├── configs.md ├── github.md ├── img │ └── github-login.png ├── index.md ├── iptask.html ├── oneservers.html ├── project.html ├── repeart_task.html ├── replica_set.html ├── simple_deployment.md ├── tasks.md ├── vue.md └── wait.html ├── dump.rdb ├── lib └── plugin │ ├── egg-email │ ├── LICENSE │ ├── README.md │ ├── app.js │ ├── config │ │ └── config.default.js │ ├── lib │ │ └── email.js │ └── package.json │ └── egg-kafka │ ├── LICENSE │ ├── README.md │ ├── app.js │ ├── config │ └── config.default.js │ ├── lib │ └── kafka.js │ └── package.json ├── mongodb-restart.sh ├── package.json ├── servers-restart.sh ├── start-docker-compose.sh ├── test └── app │ └── controller │ └── home.test.js └── yarn.lock /.autod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | write: true, 5 | prefix: '^', 6 | plugin: 'autod-egg', 7 | test: [ 8 | 'test', 9 | 'benchmark', 10 | ], 11 | dep: [ 12 | 'egg', 13 | 'egg-scripts', 14 | ], 15 | devdep: [ 16 | 'egg-ci', 17 | 'egg-bin', 18 | 'egg-mock', 19 | 'autod', 20 | 'autod-egg', 21 | 'eslint', 22 | 'eslint-config-egg', 23 | 'webstorm-disable-index', 24 | ], 25 | exclude: [ 26 | './test/fixtures', 27 | './dist', 28 | ], 29 | }; 30 | 31 | 32 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg", 3 | "rules": { 4 | "indent": [ 5 | "error", 6 | 4 7 | ], 8 | "linebreak-style": 0, 9 | }, 10 | "plugins": [ 11 | "html" 12 | ], 13 | "settings": { 14 | "html/html-extensions": [ 15 | ".html" 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | config/config.prod_1.js 3 | npm-debug.log 4 | yarn-error.log 5 | node_modules/ 6 | package-lock.json 7 | coverage/ 8 | .idea/ 9 | run/ 10 | .DS_Store 11 | *.sw* 12 | *.un~ 13 | buildlogs/ 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: node_js 3 | node_js: 4 | - '8' 5 | before_install: 6 | - npm i npminstall -g 7 | install: 8 | - npminstall 9 | script: 10 | - npm run ci 11 | after_script: 12 | - npminstall codecov && codecov 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileheader.Author": "zane", 3 | "fileheader.LastModifiedBy": "zane" 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2019 zane 4 | - zane ([@wangweianger](https://github.com/wangweianger)) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async app => { 4 | app.models = {}; 5 | app.beforeStart(async () => { 6 | const ctx = app.createAnonymousContext(); 7 | if (app.config.report_data_type === 'kafka') { 8 | // kafka消费者 9 | ctx.service.web.reportTask.saveWebReportDatasForKafka(); 10 | ctx.service.wx.reportTask.saveWxReportDatasForKafka(); 11 | } else if (app.config.report_data_type === 'redisPubSub') { 12 | ctx.service.wx.reportTask.saveWxReportDatasForRedisPubSub(); 13 | } 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /app/cache/wx_ip_city_cache_file.txt: -------------------------------------------------------------------------------- 1 | "210.21.228":{"city":"深圳市","province":"广东省"},"58.250.17":{"city":"深圳市","province":"广东省"},"223.74.151":{"city":"深圳市","province":"广东省"},"112.80.25":{"city":"苏州市","province":"江苏省"},"113.87.116":{"city":"深圳市","province":"广东省"}, -------------------------------------------------------------------------------- /app/controller/api/emails.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class EmailsController extends Controller { 6 | 7 | async getList() { 8 | const { ctx } = this; 9 | const query = ctx.request.query; 10 | const pageNo = query.pageNo; 11 | const pageSize = query.pageSize || this.app.config.pageSize; 12 | const email = query.email; 13 | 14 | const result = await ctx.service.emails.getList(pageNo, pageSize, email); 15 | 16 | ctx.body = this.app.result({ 17 | data: result, 18 | }); 19 | } 20 | 21 | async addEmail() { 22 | const { ctx } = this; 23 | const query = ctx.request.body; 24 | const email = query.email; 25 | const name = query.name; 26 | if (!email) throw new Error('新增邮件:邮件地址不能为空!'); 27 | if (!name) throw new Error('新增邮件:邮件所属人不能为空!'); 28 | 29 | const result = await ctx.service.emails.addEmail(email, name); 30 | 31 | ctx.body = this.app.result({ 32 | data: result, 33 | }); 34 | } 35 | 36 | async deleteEmail() { 37 | const { ctx } = this; 38 | const query = ctx.request.body; 39 | const id = query.id; 40 | const systemIds = query.systemIds || []; 41 | const email = query.email; 42 | 43 | if (!id) throw new Error('删除邮件:id不能为空!'); 44 | 45 | const result = await ctx.service.emails.deleteEmail(id, systemIds, email); 46 | 47 | ctx.body = this.app.result({ 48 | data: result, 49 | }); 50 | } 51 | } 52 | 53 | module.exports = EmailsController; 54 | -------------------------------------------------------------------------------- /app/controller/api/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class ErrorsController extends Controller { 6 | 7 | // 获得系统重启信息 8 | async getSysDbErrorList() { 9 | const { ctx } = this; 10 | const result = await ctx.service.errors.getErrorList(); 11 | 12 | ctx.body = this.app.result({ 13 | data: result, 14 | }); 15 | } 16 | } 17 | 18 | module.exports = ErrorsController; 19 | -------------------------------------------------------------------------------- /app/controller/api/remove.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class RemoveController extends Controller { 6 | 7 | // 清空db1 1日之前无用数据 8 | async deleteDb1WebData() { 9 | const { ctx } = this; 10 | const query = ctx.request.body; 11 | 12 | const result = await ctx.service.remove.deleteDb1WebData(query.type); 13 | ctx.body = this.app.result({ 14 | data: result, 15 | }); 16 | } 17 | 18 | // 清空db2 number日之前所有性能数据 19 | async deleteDb2WebData() { 20 | const { ctx } = this; 21 | const query = ctx.request.body; 22 | const number = query.number || 10; 23 | const appId = query.appId; 24 | 25 | if (!appId) throw new Error('获得error分类列表:appId不能为空'); 26 | 27 | const result = await ctx.service.remove.deleteDb2WebData(appId, number, query.type); 28 | ctx.body = this.app.result({ 29 | data: result, 30 | }); 31 | } 32 | 33 | } 34 | 35 | module.exports = RemoveController; 36 | -------------------------------------------------------------------------------- /app/controller/api/system.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class SystemController extends Controller { 6 | 7 | // 新增系统 8 | async addNewSystem() { 9 | const { ctx } = this; 10 | return await ctx.service.system.saveSystemData(ctx); 11 | } 12 | 13 | // 修改系统信息 14 | async updateSystem() { 15 | const { ctx } = this; 16 | return await ctx.service.system.updateSystemData(ctx); 17 | } 18 | 19 | // 根据用户id获取系统列表 20 | async getSysForUserId() { 21 | const { ctx } = this; 22 | const result = await ctx.service.system.getSysForUserId(ctx); 23 | ctx.body = this.app.result({ 24 | data: result, 25 | }); 26 | } 27 | 28 | // 根据系统ID获得单个系统信息 29 | async getSystemForId() { 30 | const { ctx } = this; 31 | const query = ctx.request.query; 32 | const appId = query.appId; 33 | 34 | const result = await ctx.service.system.getSystemForDb(appId); 35 | ctx.body = this.app.result({ 36 | data: result, 37 | }); 38 | } 39 | 40 | // 系统列表 41 | async getWebSystemList() { 42 | const { ctx } = this; 43 | 44 | const result = await ctx.service.system.getWebSystemList(); 45 | ctx.body = this.app.result({ 46 | data: result, 47 | }); 48 | } 49 | 50 | // 删除系统中某个用户 51 | async deleteWebSystemUser() { 52 | const { ctx } = this; 53 | const query = ctx.request.body; 54 | const appId = query.appId; 55 | const userToken = query.userToken; 56 | 57 | if (!appId) throw new Error('删除系统中某个用户:appId不能为空'); 58 | if (!userToken) throw new Error('删除系统中某个用户:userToken不能为空'); 59 | 60 | const result = await ctx.service.system.deleteWebSystemUser(appId, userToken); 61 | 62 | ctx.body = this.app.result({ 63 | data: result, 64 | }); 65 | } 66 | 67 | // 系统中新增某个用户 68 | async addWebSystemUser() { 69 | const { ctx } = this; 70 | const query = ctx.request.body; 71 | const appId = query.appId; 72 | const userToken = query.userToken; 73 | 74 | if (!appId) throw new Error('系统中新增某个用户:appId不能为空'); 75 | if (!userToken) throw new Error('系统中新增某个用户:userToken不能为空'); 76 | 77 | const result = await ctx.service.system.addWebSystemUser(appId, userToken); 78 | 79 | ctx.body = this.app.result({ 80 | data: result, 81 | }); 82 | } 83 | 84 | // 删除某个系统 85 | async deleteSystem() { 86 | const { ctx } = this; 87 | const query = ctx.request.body; 88 | const appId = query.appId; 89 | 90 | if (!appId) throw new Error('删除某个系统:appId不能为空'); 91 | 92 | const result = await ctx.service.system.deleteSystem(appId); 93 | 94 | ctx.body = this.app.result({ 95 | data: result, 96 | }); 97 | } 98 | 99 | // 新增 | 删除 日报邮件 100 | async handleDaliyEmail() { 101 | const { ctx } = this; 102 | const query = ctx.request.body; 103 | const appId = query.appId; 104 | const email = query.email; 105 | const type = query.type || 1; 106 | const item = query.item || 1; 107 | 108 | if (!appId) throw new Error('appId不能为空'); 109 | 110 | const result = await ctx.service.system.handleDaliyEmail(appId, email, type, true, item); 111 | 112 | ctx.body = this.app.result({ 113 | data: result, 114 | }); 115 | } 116 | } 117 | 118 | module.exports = SystemController; 119 | -------------------------------------------------------------------------------- /app/controller/api/web/ajax.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class AjaxsController extends Controller { 6 | 7 | // 平均页面性能列表 8 | async getPageAjaxsAvg() { 9 | const { ctx } = this; 10 | const query = ctx.request.query; 11 | const appId = query.appId; 12 | const url = query.url; 13 | const beginTime = query.beginTime; 14 | const endTime = query.endTime; 15 | 16 | if (!appId) throw new Error('页面ajax信息:appId不能为空'); 17 | if (!url) throw new Error('页面ajax信息:url不能为空'); 18 | 19 | const result = await ctx.service.web.ajaxs.getPageAjaxsAvg(appId, url, beginTime, endTime); 20 | 21 | ctx.body = this.app.result({ 22 | data: result, 23 | }); 24 | } 25 | 26 | // 平均AJAX性能列表 27 | async getAverageAjaxList() { 28 | const { ctx } = this; 29 | const query = ctx.request.query; 30 | const appId = query.appId; 31 | 32 | if (!appId) throw new Error('平均AJAX性能列表:appId不能为空'); 33 | 34 | const result = await ctx.service.web.ajaxs.getAverageAjaxList(ctx); 35 | 36 | ctx.body = this.app.result({ 37 | data: result, 38 | }); 39 | } 40 | // 获得单个api的平均性能数据 41 | async getOneAjaxAvg() { 42 | const { ctx } = this; 43 | const query = ctx.request.query; 44 | const appId = query.appId; 45 | const url = query.url; 46 | const beginTime = query.beginTime; 47 | const endTime = query.endTime; 48 | const type = query.type; 49 | 50 | if (!appId) throw new Error('单个AJAX平均性能数据:appId不能为空'); 51 | if (!url) throw new Error('单个AJAX平均性能数据:api地址不能为空'); 52 | 53 | const result = await ctx.service.web.ajaxs.getOneAjaxAvg(appId, url, beginTime, endTime, type); 54 | 55 | ctx.body = this.app.result({ 56 | data: result, 57 | }); 58 | } 59 | // 获得单个api的性能列表数据 60 | async getOneAjaxList() { 61 | const { ctx } = this; 62 | const query = ctx.request.query; 63 | const appId = query.appId; 64 | const url = query.url; 65 | const pageNo = query.pageNo || 1; 66 | const pageSize = query.pageSize || this.app.config.pageSize; 67 | const beginTime = query.beginTime; 68 | const endTime = query.endTime; 69 | const type = query.type; 70 | 71 | if (!appId) throw new Error('单个AJAX平均性能数据:appId不能为空'); 72 | if (!url) throw new Error('单个AJAX平均性能数据:api地址不能为空'); 73 | 74 | const result = await ctx.service.web.ajaxs.getOneAjaxList(appId, url, pageNo, pageSize, beginTime, endTime, type); 75 | 76 | ctx.body = this.app.result({ 77 | data: result, 78 | }); 79 | } 80 | 81 | // 获得单个ajax详情信息 82 | async getOneAjaxDetail() { 83 | const { ctx } = this; 84 | const query = ctx.request.query; 85 | const id = query.id; 86 | const appId = query.appId; 87 | 88 | if (!id) throw new Error('获得单个ajax详情信息:id不能为空'); 89 | if (!appId) throw new Error('获得单个ajax详情信息:appId不能为空'); 90 | 91 | const result = await ctx.service.web.ajaxs.getOneAjaxDetail(appId, id); 92 | 93 | ctx.body = this.app.result({ 94 | data: result, 95 | }); 96 | } 97 | } 98 | 99 | module.exports = AjaxsController; 100 | -------------------------------------------------------------------------------- /app/controller/api/web/analysis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Controller = require('egg').Controller; 3 | 4 | class AnalysisController extends Controller { 5 | 6 | // 用户漏斗分析列表 7 | async getAnalysislist() { 8 | const { ctx } = this; 9 | const query = ctx.request.query; 10 | const appId = query.appId; 11 | const beginTime = query.beginTime; 12 | const endTime = query.endTime; 13 | const pageNo = query.pageNo; 14 | const ip = query.ip; 15 | const pageSize = query.pageSize || this.app.config.pageSize; 16 | 17 | if (!appId) throw new Error('用户漏斗分析列表:appId不能为空'); 18 | 19 | const result = await ctx.service.web.analysis.getAnalysislist(appId, beginTime, endTime, ip, pageNo, pageSize); 20 | 21 | ctx.body = this.app.result({ 22 | data: result, 23 | }); 24 | } 25 | 26 | // 单个用户行为轨迹列表 27 | async getAnalysisOneList() { 28 | const { ctx } = this; 29 | const query = ctx.request.query; 30 | const appId = query.appId; 31 | const markuser = query.markuser; 32 | 33 | if (!appId) throw new Error('单个用户行为轨迹列表:appId不能为空'); 34 | if (!markuser) throw new Error('单个用户行为轨迹列表:markuser不能为空'); 35 | 36 | const result = await ctx.service.web.analysis.getAnalysisOneList(appId, markuser); 37 | 38 | ctx.body = this.app.result({ 39 | data: result, 40 | }); 41 | } 42 | 43 | // top datas 44 | async getTopDatas() { 45 | const { ctx } = this; 46 | const query = ctx.request.query; 47 | const appId = query.appId; 48 | const beginTime = query.beginTime; 49 | const endTime = query.endTime; 50 | const type = query.type || 1; 51 | if (!appId) throw new Error('top页面:appId不能为空'); 52 | 53 | const result = await ctx.service.web.analysis.getTopDatas(appId, beginTime, endTime, type); 54 | ctx.body = this.app.result({ 55 | data: result, 56 | }); 57 | } 58 | 59 | // top datas 60 | async getProvinceAvgCount() { 61 | const { ctx } = this; 62 | const query = ctx.request.query; 63 | const appId = query.appId; 64 | const beginTime = query.beginTime; 65 | const endTime = query.endTime; 66 | const type = query.type || 1; 67 | if (!appId) throw new Error('top页面:appId不能为空'); 68 | 69 | const result = await ctx.service.web.analysis.getProvinceAvgCount(appId, beginTime, endTime, type); 70 | ctx.body = this.app.result({ 71 | data: result, 72 | }); 73 | } 74 | } 75 | 76 | module.exports = AnalysisController; 77 | -------------------------------------------------------------------------------- /app/controller/api/web/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class EnfironmentController extends Controller { 6 | 7 | // 获得用户系统、地址位置、浏览器分类 8 | async getDataGroupBy() { 9 | const { ctx } = this; 10 | const query = ctx.request.query; 11 | const appId = query.appId; 12 | const beginTime = query.beginTime; 13 | const endTime = query.endTime; 14 | const type = query.type || 1; 15 | const url = query.url; 16 | 17 | if (!appId) throw new Error('页面性能列表:appId不能为空'); 18 | if (!url) throw new Error('页面性能列表:url不能为空'); 19 | 20 | const result = await ctx.service.web.environment.getDataGroupBy(type, url, appId, beginTime, endTime); 21 | 22 | ctx.body = this.app.result({ 23 | data: result, 24 | }); 25 | } 26 | 27 | // 根据mark_page获得用户系统信息 28 | async getEnvironmentForPage() { 29 | const { ctx } = this; 30 | const query = ctx.request.query; 31 | const appId = query.appId; 32 | const markPage = query.markPage; 33 | 34 | if (!appId) throw new Error('根据mark_page获得用户系统信息:appId不能为空'); 35 | if (!markPage) throw new Error('根据mark_page获得用户系统信息:markPage不能为空'); 36 | 37 | const result = await ctx.service.web.environment.getEnvironmentForPage(appId, markPage); 38 | 39 | ctx.body = this.app.result({ 40 | data: result, 41 | }); 42 | } 43 | } 44 | 45 | module.exports = EnfironmentController; 46 | -------------------------------------------------------------------------------- /app/controller/api/web/error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class ErrorController extends Controller { 6 | 7 | // 获得error分类列表 8 | async getAverageErrorList() { 9 | const { ctx } = this; 10 | const query = ctx.request.query; 11 | const appId = query.appId; 12 | 13 | if (!appId) throw new Error('获得error分类列表:appId不能为空'); 14 | 15 | const result = await ctx.service.web.errors.getAverageErrorList(ctx); 16 | 17 | ctx.body = this.app.result({ 18 | data: result, 19 | }); 20 | } 21 | 22 | // 获得单个ERROR资源列表信息 23 | async getOneErrorList() { 24 | const { ctx } = this; 25 | const query = ctx.request.query; 26 | const appId = query.appId; 27 | const url = query.url; 28 | const category = query.category || 'resource'; 29 | const pageNo = query.pageNo || 1; 30 | const pageSize = query.pageSize || this.app.config.pageSize; 31 | const beginTime = query.beginTime; 32 | const endTime = query.endTime; 33 | 34 | if (!appId) throw new Error('获得单个ERROR资源列表信息:appId不能为空'); 35 | if (!url) throw new Error('获得单个ERROR资源列表信息:url地址不能为空'); 36 | 37 | const result = await ctx.service.web.errors.getOneErrorList(appId, url, category, pageNo, pageSize, beginTime, endTime); 38 | 39 | ctx.body = this.app.result({ 40 | data: result, 41 | }); 42 | } 43 | 44 | // 获得单个api的性能列表数据 45 | async getOneResourceList() { 46 | const { ctx } = this; 47 | const query = ctx.request.query; 48 | const appId = query.appId; 49 | const url = query.url; 50 | const pageNo = query.pageNo || 1; 51 | const pageSize = query.pageSize || this.app.config.pageSize; 52 | 53 | if (!appId) throw new Error('单个Resource性能列表数据:appId不能为空'); 54 | if (!url) throw new Error('单个Resource性能列表数据:api地址不能为空'); 55 | 56 | 57 | const result = await ctx.service.web.resource.getOneResourceList(appId, url, pageNo, pageSize); 58 | 59 | ctx.body = this.app.result({ 60 | data: result, 61 | }); 62 | } 63 | 64 | // 单个error详情信息 65 | async getErrorDetail() { 66 | const { ctx } = this; 67 | const query = ctx.request.query; 68 | const id = query.id; 69 | const appId = query.appId; 70 | 71 | if (!id) throw new Error('单个error详情信息:id不能为空'); 72 | if (!appId) throw new Error('单个error详情信息:appId不能为空'); 73 | 74 | const result = await ctx.service.web.errors.getErrorDetail(appId, id); 75 | 76 | ctx.body = this.app.result({ 77 | data: result, 78 | }); 79 | } 80 | } 81 | 82 | module.exports = ErrorController; 83 | -------------------------------------------------------------------------------- /app/controller/api/web/pages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class PagesController extends Controller { 6 | 7 | // 平均页面性能列表 8 | async getAveragePageList() { 9 | const { ctx } = this; 10 | const query = ctx.request.query; 11 | const appId = query.appId; 12 | 13 | if (!appId) throw new Error('平均页面性能列表:appId不能为空'); 14 | 15 | const result = await ctx.service.web.pages.getAveragePageList(ctx); 16 | 17 | ctx.body = this.app.result({ 18 | data: result, 19 | }); 20 | } 21 | 22 | // 单个页面性能数据列表 23 | async getOnePageList() { 24 | const { ctx } = this; 25 | const query = ctx.request.query; 26 | const appId = query.appId; 27 | const url = query.url; 28 | if (!appId) throw new Error('单个页面性能列表:appId不能为空'); 29 | if (!url) throw new Error('单个页面性能列表:url不能为空'); 30 | 31 | const result = await ctx.service.web.pages.getOnePageList(ctx); 32 | 33 | ctx.body = this.app.result({ 34 | data: result, 35 | }); 36 | } 37 | 38 | // 单个页面列表(简单版本) 39 | async getPagesForType() { 40 | const { ctx } = this; 41 | const query = ctx.request.query; 42 | const appId = query.appId; 43 | const url = query.url; 44 | const speedType = query.type || 1; 45 | const pageNo = query.pageNo; 46 | const pageSize = query.pageSize; 47 | 48 | if (!appId) throw new Error('单个页面性能列表:appId不能为空'); 49 | if (!url) throw new Error('单个页面性能列表:url不能为空'); 50 | 51 | const result = await ctx.service.web.pages.getPagesForType(appId, url, speedType, pageNo, pageSize); 52 | 53 | ctx.body = this.app.result({ 54 | data: result, 55 | }); 56 | } 57 | 58 | // 单个页面详情 59 | async getPageDetails() { 60 | const { ctx } = this; 61 | const query = ctx.request.query; 62 | const id = query.id; 63 | const appId = query.appId; 64 | 65 | if (!id) throw new Error('单个页面详情:id不能为空'); 66 | if (!appId) throw new Error('单个页面详情:appId不能为空'); 67 | 68 | const result = await ctx.service.web.pages.getPageDetails(appId, id); 69 | 70 | ctx.body = this.app.result({ 71 | data: result, 72 | }); 73 | } 74 | } 75 | 76 | module.exports = PagesController; 77 | -------------------------------------------------------------------------------- /app/controller/api/web/report.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | const { getUserIp } = require('../../../util') 4 | const Controller = require('egg').Controller; 5 | 6 | class ReportController extends Controller { 7 | constructor(params) { 8 | super(params); 9 | } 10 | // web用户数据上报 11 | async webReport() { 12 | const { ctx } = this; 13 | ctx.set('Access-Control-Allow-Origin', '*'); 14 | ctx.set('Content-Type', 'application/json;charset=UTF-8'); 15 | ctx.set('X-Response-Time', '2s'); 16 | ctx.set('Connection', 'close'); 17 | ctx.status = 200; 18 | 19 | const query = ctx.request.body; 20 | if (!query.appId) throw new Error('web端上报数据操作:app_id不能为空'); 21 | 22 | query.ip = getUserIp(ctx); 23 | query.url = query.url || ctx.headers.referer; 24 | query.user_agent = ctx.headers['user-agent']; 25 | 26 | if (this.app.config.report_data_type === 'redis') this.saveWebReportDataForRedis(query); 27 | if (this.app.config.report_data_type === 'kafka') this.saveWebReportDataForKafka(query); 28 | if (this.app.config.report_data_type === 'mongodb') this.saveWebReportDataForMongodb(ctx); 29 | } 30 | 31 | // 通过redis 消息队列消费数据 32 | async saveWebReportDataForRedis(query) { 33 | try { 34 | if (this.app.config.redis_consumption.total_limit_web) { 35 | // 限流 36 | const length = await this.app.redis.llen('web_repore_datas'); 37 | if (length >= this.app.config.redis_consumption.total_limit_web) return; 38 | } 39 | // 生产者 40 | this.app.redis.lpush('web_repore_datas', JSON.stringify(query)); 41 | } catch (e) { console.log(e); } 42 | } 43 | 44 | // 通过kafka 消息队列消费数据 45 | async saveWebReportDataForKafka(query) { 46 | // 生产者 47 | this.app.kafka.send( 48 | 'web', 49 | JSON.stringify(query) 50 | ); 51 | } 52 | 53 | // 通过mongodb 数据库存储数据 54 | async saveWebReportDataForMongodb(ctx) { 55 | ctx.service.web.report.saveWebReportData(ctx); 56 | } 57 | } 58 | 59 | module.exports = ReportController; 60 | -------------------------------------------------------------------------------- /app/controller/api/web/resource.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class ResourceController extends Controller { 6 | 7 | // 根据某个页面获得资源列表 8 | async getResourceForType() { 9 | const { ctx } = this; 10 | const query = ctx.request.query; 11 | const appId = query.appId; 12 | const url = query.url; 13 | const speedType = query.type || 1; 14 | const pageNo = query.pageNo; 15 | const pageSize = query.pageSize; 16 | 17 | if (!appId) throw new Error('单个页面资源性能列表:appId不能为空'); 18 | if (!url) throw new Error('单个页面资源性能列表:url不能为空'); 19 | 20 | const result = await ctx.service.web.resource.getResourceForType(appId, url, speedType, pageNo, pageSize); 21 | 22 | ctx.body = this.app.result({ 23 | data: result, 24 | }); 25 | } 26 | 27 | // 获得resource平均性能列表 28 | async getAverageResourceList() { 29 | const { ctx } = this; 30 | const query = ctx.request.query; 31 | const appId = query.appId; 32 | 33 | if (!appId) throw new Error('获得resource平均性能列表:appId不能为空'); 34 | 35 | const result = await ctx.service.web.resource.getAverageResourceList(ctx); 36 | 37 | ctx.body = this.app.result({ 38 | data: result, 39 | }); 40 | } 41 | 42 | // 获得单个Resource的平均性能数据 43 | async getOneResourceAvg() { 44 | const { ctx } = this; 45 | const query = ctx.request.query; 46 | const appId = query.appId; 47 | const url = query.url; 48 | const beginTime = query.beginTime; 49 | const endTime = query.endTime; 50 | 51 | if (!appId) throw new Error('单个Resource平均性能数据:appId不能为空'); 52 | if (!url) throw new Error('单个Resource平均性能数据:api地址不能为空'); 53 | 54 | const result = await ctx.service.web.resource.getOneResourceAvg(appId, url, beginTime, endTime); 55 | 56 | ctx.body = this.app.result({ 57 | data: result, 58 | }); 59 | } 60 | 61 | // 获得单个api的性能列表数据 62 | async getOneResourceList() { 63 | const { ctx } = this; 64 | const query = ctx.request.query; 65 | const appId = query.appId; 66 | const url = query.url; 67 | const pageNo = query.pageNo || 1; 68 | const pageSize = query.pageSize || this.app.config.pageSize; 69 | const beginTime = query.beginTime; 70 | const endTime = query.endTime; 71 | 72 | if (!appId) throw new Error('单个Resource性能列表数据:appId不能为空'); 73 | if (!url) throw new Error('单个Resource性能列表数据:api地址不能为空'); 74 | 75 | 76 | const result = await ctx.service.web.resource.getOneResourceList(appId, url, pageNo, pageSize, beginTime, endTime); 77 | 78 | ctx.body = this.app.result({ 79 | data: result, 80 | }); 81 | } 82 | 83 | // 获得单个Resource详情信息 84 | async getOneResourceDetail() { 85 | const { ctx } = this; 86 | const query = ctx.request.query; 87 | const id = query.id; 88 | const appId = query.appId; 89 | 90 | if (!id) throw new Error('获得单个Resource详情信息:id不能为空'); 91 | if (!appId) throw new Error('获得单个Resource详情信息:appId不能为空'); 92 | 93 | const result = await ctx.service.web.resource.getOneResourceDetail(appId, id); 94 | 95 | ctx.body = this.app.result({ 96 | data: result, 97 | }); 98 | } 99 | } 100 | 101 | module.exports = ResourceController; 102 | -------------------------------------------------------------------------------- /app/controller/api/wx/ajax.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class AjaxsController extends Controller { 6 | 7 | // 平均页面性能列表 8 | async getPageAjaxsAvg() { 9 | const { ctx } = this; 10 | const query = ctx.request.query; 11 | const appId = query.appId; 12 | const url = query.url; 13 | const beginTime = query.beginTime; 14 | const endTime = query.endTime; 15 | 16 | if (!appId) throw new Error('页面ajax信息:appId不能为空'); 17 | if (!url) throw new Error('页面ajax信息:url不能为空'); 18 | 19 | const result = await ctx.service.wx.ajaxs.getPageAjaxsAvg(appId, url, beginTime, endTime); 20 | 21 | ctx.body = this.app.result({ 22 | data: result, 23 | }); 24 | } 25 | 26 | // 平均AJAX性能列表 27 | async getAverageAjaxList() { 28 | const { ctx } = this; 29 | const query = ctx.request.query; 30 | const appId = query.appId; 31 | 32 | if (!appId) throw new Error('平均AJAX性能列表:appId不能为空'); 33 | 34 | const result = await ctx.service.wx.ajaxs.getAverageAjaxList(ctx); 35 | 36 | ctx.body = this.app.result({ 37 | data: result, 38 | }); 39 | } 40 | // 获得单个api的平均性能数据 41 | async getOneAjaxAvg() { 42 | const { ctx } = this; 43 | const query = ctx.request.query; 44 | const appId = query.appId; 45 | const url = query.url; 46 | const beginTime = query.beginTime; 47 | const endTime = query.endTime; 48 | const type = query.type; 49 | 50 | if (!appId) throw new Error('单个AJAX平均性能数据:appId不能为空'); 51 | if (!url) throw new Error('单个AJAX平均性能数据:api地址不能为空'); 52 | 53 | const result = await ctx.service.wx.ajaxs.getOneAjaxAvg(appId, url, beginTime, endTime, type); 54 | 55 | ctx.body = this.app.result({ 56 | data: result, 57 | }); 58 | } 59 | // 获得单个api的性能列表数据 60 | async getOneAjaxList() { 61 | const { ctx } = this; 62 | const query = ctx.request.query; 63 | const appId = query.appId; 64 | const url = query.url; 65 | const pageNo = query.pageNo || 1; 66 | const pageSize = query.pageSize || this.app.config.pageSize; 67 | const beginTime = query.beginTime; 68 | const endTime = query.endTime; 69 | const type = query.type; 70 | 71 | if (!appId) throw new Error('单个AJAX平均性能数据:appId不能为空'); 72 | if (!url) throw new Error('单个AJAX平均性能数据:api地址不能为空'); 73 | 74 | const result = await ctx.service.wx.ajaxs.getOneAjaxList(appId, url, pageNo, pageSize, beginTime, endTime, type); 75 | 76 | ctx.body = this.app.result({ 77 | data: result, 78 | }); 79 | } 80 | 81 | // 获得单个ajax详情信息 82 | async getOneAjaxDetail() { 83 | const { ctx } = this; 84 | const query = ctx.request.query; 85 | const id = query.id; 86 | const appId = query.appId; 87 | 88 | if (!id) throw new Error('获得单个ajax详情信息:id不能为空'); 89 | if (!appId) throw new Error('获得单个ajax详情信息:appId不能为空'); 90 | 91 | const result = await ctx.service.wx.ajaxs.getOneAjaxDetail(appId, id); 92 | 93 | ctx.body = this.app.result({ 94 | data: result, 95 | }); 96 | } 97 | } 98 | 99 | module.exports = AjaxsController; 100 | -------------------------------------------------------------------------------- /app/controller/api/wx/analysis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Controller = require('egg').Controller; 3 | 4 | class AnalysisController extends Controller { 5 | 6 | // 用户漏斗分析列表 7 | async getAnalysislist() { 8 | const { ctx } = this; 9 | const query = ctx.request.query; 10 | const appId = query.appId; 11 | const beginTime = query.beginTime; 12 | const endTime = query.endTime; 13 | const pageNo = query.pageNo; 14 | const ip = query.ip; 15 | const pageSize = query.pageSize || this.app.config.pageSize; 16 | 17 | if (!appId) throw new Error('用户漏斗分析列表:appId不能为空'); 18 | 19 | const result = await ctx.service.wx.analysis.getAnalysislist(appId, beginTime, endTime, ip, pageNo, pageSize); 20 | 21 | ctx.body = this.app.result({ 22 | data: result, 23 | }); 24 | } 25 | 26 | // 单个用户行为轨迹列表 27 | async getAnalysisOneList() { 28 | const { ctx } = this; 29 | const query = ctx.request.query; 30 | const appId = query.appId; 31 | const markuser = query.markuser; 32 | 33 | if (!appId) throw new Error('单个用户行为轨迹列表:appId不能为空'); 34 | if (!markuser) throw new Error('单个用户行为轨迹列表:markuser不能为空'); 35 | 36 | const result = await ctx.service.wx.analysis.getAnalysisOneList(appId, markuser); 37 | 38 | ctx.body = this.app.result({ 39 | data: result, 40 | }); 41 | } 42 | 43 | // top datas 44 | async getTopDatas() { 45 | const { ctx } = this; 46 | const query = ctx.request.query; 47 | const appId = query.appId; 48 | const beginTime = query.beginTime; 49 | const endTime = query.endTime; 50 | const type = query.type || 1; 51 | if (!appId) throw new Error('top页面:appId不能为空'); 52 | 53 | const result = await ctx.service.wx.analysis.getTopDatas(appId, beginTime, endTime, type); 54 | ctx.body = this.app.result({ 55 | data: result, 56 | }); 57 | } 58 | 59 | // top datas 60 | async getProvinceAvgCount() { 61 | const { ctx } = this; 62 | const query = ctx.request.query; 63 | const appId = query.appId; 64 | const beginTime = query.beginTime; 65 | const endTime = query.endTime; 66 | const type = query.type || 1; 67 | if (!appId) throw new Error('top页面:appId不能为空'); 68 | 69 | const result = await ctx.service.wx.analysis.getProvinceAvgCount(appId, beginTime, endTime, type); 70 | ctx.body = this.app.result({ 71 | data: result, 72 | }); 73 | } 74 | } 75 | 76 | module.exports = AnalysisController; 77 | -------------------------------------------------------------------------------- /app/controller/api/wx/error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class ErrorController extends Controller { 6 | 7 | // 获得error分类列表 8 | async getAverageErrorList() { 9 | const { ctx } = this; 10 | const query = ctx.request.query; 11 | const appId = query.appId; 12 | 13 | if (!appId) throw new Error('获得error分类列表:appId不能为空'); 14 | 15 | const result = await ctx.service.wx.errors.getAverageErrorList(ctx); 16 | 17 | ctx.body = this.app.result({ 18 | data: result, 19 | }); 20 | } 21 | 22 | // 获得单个ERROR资源列表信息 23 | async getOneErrorList() { 24 | const { ctx } = this; 25 | const query = ctx.request.query; 26 | const appId = query.appId; 27 | const url = query.url; 28 | const category = query.category || 'resource'; 29 | const pageNo = query.pageNo || 1; 30 | const pageSize = query.pageSize || this.app.config.pageSize; 31 | const beginTime = query.beginTime; 32 | const endTime = query.endTime; 33 | 34 | if (!appId) throw new Error('获得单个ERROR资源列表信息:appId不能为空'); 35 | if (!url) throw new Error('获得单个ERROR资源列表信息:url地址不能为空'); 36 | 37 | const result = await ctx.service.wx.errors.getOneErrorList(appId, url, category, pageNo, pageSize, beginTime, endTime); 38 | 39 | ctx.body = this.app.result({ 40 | data: result, 41 | }); 42 | } 43 | 44 | // 获得单个api的性能列表数据 45 | async getOneResourceList() { 46 | const { ctx } = this; 47 | const query = ctx.request.query; 48 | const appId = query.appId; 49 | const url = query.url; 50 | const pageNo = query.pageNo || 1; 51 | const pageSize = query.pageSize || this.app.config.pageSize; 52 | 53 | if (!appId) throw new Error('单个Resource性能列表数据:appId不能为空'); 54 | if (!url) throw new Error('单个Resource性能列表数据:api地址不能为空'); 55 | 56 | 57 | const result = await ctx.service.web.resource.getOneResourceList(appId, url, pageNo, pageSize); 58 | 59 | ctx.body = this.app.result({ 60 | data: result, 61 | }); 62 | } 63 | 64 | // 单个error详情信息 65 | async getErrorDetail() { 66 | const { ctx } = this; 67 | const query = ctx.request.query; 68 | const id = query.id; 69 | const appId = query.appId; 70 | 71 | if (!id) throw new Error('单个error详情信息:id不能为空'); 72 | if (!appId) throw new Error('单个error详情信息:appId不能为空'); 73 | 74 | const result = await ctx.service.wx.errors.getErrorDetail(appId, id); 75 | 76 | ctx.body = this.app.result({ 77 | data: result, 78 | }); 79 | } 80 | } 81 | 82 | module.exports = ErrorController; 83 | -------------------------------------------------------------------------------- /app/controller/api/wx/pages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class PagesController extends Controller { 6 | 7 | // 平均页面性能列表 8 | async getAveragePageList() { 9 | const { ctx } = this; 10 | const query = ctx.request.query; 11 | const appId = query.appId; 12 | 13 | if (!appId) throw new Error('平均页面性能列表:appId不能为空'); 14 | 15 | const result = await ctx.service.wx.pages.getAveragePageList(ctx); 16 | 17 | ctx.body = this.app.result({ 18 | data: result, 19 | }); 20 | } 21 | 22 | // 单个页面性能数据列表 23 | async getOnePageList() { 24 | const { ctx } = this; 25 | const query = ctx.request.query; 26 | const appId = query.appId; 27 | const url = query.url; 28 | if (!appId) throw new Error('单个页面性能列表:appId不能为空'); 29 | if (!url) throw new Error('单个页面性能列表:url不能为空'); 30 | 31 | const result = await ctx.service.wx.pages.getOnePageList(ctx); 32 | 33 | ctx.body = this.app.result({ 34 | data: result, 35 | }); 36 | } 37 | 38 | // 单个页面列表(简单版本) 39 | async getPagesForType() { 40 | const { ctx } = this; 41 | const query = ctx.request.query; 42 | const appId = query.appId; 43 | const url = query.url; 44 | const speedType = query.type || 1; 45 | const pageNo = query.pageNo; 46 | const pageSize = query.pageSize; 47 | 48 | if (!appId) throw new Error('单个页面性能列表:appId不能为空'); 49 | if (!url) throw new Error('单个页面性能列表:url不能为空'); 50 | 51 | const result = await ctx.service.wx.pages.getPagesForType(appId, url, speedType, pageNo, pageSize); 52 | 53 | ctx.body = this.app.result({ 54 | data: result, 55 | }); 56 | } 57 | 58 | // 单个页面详情 59 | async getPageDetails() { 60 | const { ctx } = this; 61 | const query = ctx.request.query; 62 | const appId = query.appId; 63 | const id = query.id; 64 | 65 | if (!appId) throw new Error('单个页面详情:appId不能为空'); 66 | if (!id) throw new Error('单个页面详情:id不能为空'); 67 | 68 | const result = await ctx.service.wx.pages.getPageDetails(appId, id, 1); 69 | 70 | ctx.body = this.app.result({ 71 | data: result, 72 | }); 73 | } 74 | 75 | // 获得用户系统、地址位置、浏览器分类 76 | async getDataGroupBy() { 77 | const { ctx } = this; 78 | const query = ctx.request.query; 79 | const appId = query.appId; 80 | const beginTime = query.beginTime; 81 | const endTime = query.endTime; 82 | const type = query.type || 1; 83 | const url = query.url; 84 | 85 | if (!appId) throw new Error('页面性能列表:appId不能为空'); 86 | if (!url) throw new Error('页面性能列表:url不能为空'); 87 | 88 | const result = await ctx.service.wx.pages.getDataGroupBy(type, url, appId, beginTime, endTime); 89 | 90 | ctx.body = this.app.result({ 91 | data: result, 92 | }); 93 | } 94 | 95 | // 单个页面详情(markpage) 96 | async getPageForMarkpage() { 97 | const { ctx } = this; 98 | const query = ctx.request.query; 99 | const appId = query.appId; 100 | const markPage = query.markPage; 101 | 102 | if (!appId) throw new Error('单个页面详情:appId不能为空'); 103 | if (!markPage) throw new Error('单个页面详情:markPage不能为空'); 104 | 105 | const result = await ctx.service.wx.pages.getPageDetails(appId, markPage, 2); 106 | 107 | ctx.body = this.app.result({ 108 | data: result, 109 | }); 110 | } 111 | 112 | } 113 | 114 | module.exports = PagesController; 115 | -------------------------------------------------------------------------------- /app/controller/api/wx/report.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { getUserIp } = require('../../../util'); 3 | const Controller = require('egg').Controller; 4 | 5 | class ReportController extends Controller { 6 | 7 | // 微信端用户数据上报 8 | async wxReport() { 9 | const { ctx } = this; 10 | ctx.set('Access-Control-Allow-Origin', '*'); 11 | ctx.set('Content-Type', 'application/json;charset=UTF-8'); 12 | ctx.set('X-Response-Time', '2s'); 13 | ctx.set('Connection', 'close'); 14 | ctx.status = 200; 15 | 16 | const query = ctx.request.body; 17 | if (!query.appId) throw new Error('wx端上报数据操作:app_id不能为空'); 18 | 19 | query.ip = getUserIp(ctx); 20 | 21 | if (this.app.config.report_data_type === 'redis') this.saveWxReportDataForRedis(query); 22 | if (this.app.config.report_data_type === 'kafka') this.saveWxReportDataForKafka(query); 23 | if (this.app.config.report_data_type === 'mongodb') this.saveWxReportDataForMongodb(ctx); 24 | } 25 | 26 | // 通过redis 消费者模式存储数据 27 | async saveWxReportDataForRedis(query) { 28 | try { 29 | if (this.app.config.redis_consumption.total_limit_wx) { 30 | // 限流 31 | const length = await this.app.redis.llen('wx_repore_datas'); 32 | if (length >= this.app.config.redis_consumption.total_limit_wx) return; 33 | } 34 | this.app.redis.lpush('wx_repore_datas', JSON.stringify(query)); 35 | } catch (e) { console.log(e); } 36 | } 37 | 38 | // 通过kafka 消息队列消费数据 39 | async saveWxReportDataForKafka(query) { 40 | // 生产者 41 | this.app.kafka.send( 42 | 'wx', 43 | JSON.stringify(query) 44 | ); 45 | } 46 | 47 | // 通过mongodb 数据库存储数据 48 | async saveWxReportDataForMongodb(ctx) { 49 | ctx.service.wx.report.saveWxReportData(ctx); 50 | } 51 | 52 | } 53 | 54 | module.exports = ReportController; 55 | -------------------------------------------------------------------------------- /app/controller/web/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class HomeController extends Controller { 6 | 7 | // 系统应用列表 8 | async systemlist() { 9 | const { ctx } = this; 10 | await ctx.render('home', { 11 | data: { 12 | title: '应用列表', 13 | }, 14 | }); 15 | } 16 | 17 | // 新增系统选择应用类型 18 | async selectype() { 19 | const { ctx } = this; 20 | await ctx.render('selectype', { 21 | data: { 22 | title: '选择应用类型', 23 | }, 24 | }); 25 | } 26 | 27 | // 用户登录 28 | async login() { 29 | const { ctx } = this; 30 | await ctx.render('login', { 31 | data: { 32 | title: '登录系统', 33 | gh_client_id: this.app.config.github.client_id, 34 | gh_scope: this.app.config.github.scope, 35 | wb_client_id: this.app.config.weibo.client_id, 36 | wb_scope: this.app.config.weibo.scope, 37 | wx_client_id: this.app.config.wechat.client_id, 38 | }, 39 | }); 40 | } 41 | 42 | // 系统列表 43 | async systems() { 44 | const { ctx } = this; 45 | await ctx.render('systems', { 46 | data: { 47 | title: '系统列表', 48 | }, 49 | }); 50 | } 51 | 52 | // 用户管理 53 | async users() { 54 | const { ctx } = this; 55 | await ctx.render('users', { 56 | data: { 57 | title: '用户列表', 58 | }, 59 | }); 60 | } 61 | 62 | // 系统重启信息 63 | async errors() { 64 | const { ctx } = this; 65 | await ctx.render('errors', { 66 | data: { 67 | title: '系统重启信息', 68 | }, 69 | }); 70 | } 71 | 72 | // 系统重启信息 73 | async emails() { 74 | const { ctx } = this; 75 | await ctx.render('emails', { 76 | data: { 77 | title: '邮件管理', 78 | }, 79 | }); 80 | } 81 | 82 | } 83 | 84 | module.exports = HomeController; 85 | -------------------------------------------------------------------------------- /app/controller/web/wx.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class WxController extends Controller { 6 | 7 | // 新增系统 8 | async wxaddsystem() { 9 | const { ctx } = this; 10 | await ctx.render('wx/addsystem', { 11 | data: { 12 | title: '新增系统', 13 | }, 14 | }); 15 | } 16 | // 小程序首页 17 | async wxhome() { 18 | const { ctx } = this; 19 | await ctx.render('wx/home', { 20 | data: { 21 | title: '数据分析', 22 | }, 23 | }); 24 | } 25 | // 小程序设置页面 26 | async wxsetting() { 27 | const { ctx } = this; 28 | await ctx.render('wx/setting', { 29 | data: { 30 | title: '系统设置', 31 | }, 32 | }); 33 | } 34 | 35 | // 访问页面性能数据 36 | async wxpagesavg() { 37 | const { ctx } = this; 38 | await ctx.render('wx/pagesavg', { 39 | data: { 40 | title: '页面平均性能指标', 41 | }, 42 | }); 43 | } 44 | 45 | // 单页面访问页面列表性能 46 | async wxpageslist() { 47 | const { ctx } = this; 48 | await ctx.render('wx/pageslist', { 49 | data: { 50 | title: '页面性能数据列表', 51 | }, 52 | }); 53 | } 54 | 55 | async wxpagedetails() { 56 | const { ctx } = this; 57 | await ctx.render('wx/pagedetails', { 58 | data: { 59 | title: '页面性能详情数据', 60 | }, 61 | }); 62 | } 63 | 64 | // ajax请求平均性能数据 65 | async wxajaxavg() { 66 | const { ctx } = this; 67 | await ctx.render('wx/ajaxavg', { 68 | data: { 69 | title: 'ajax平均性能指标', 70 | }, 71 | }); 72 | } 73 | 74 | // ajax详情 75 | async wxajaxdetail() { 76 | const { ctx } = this; 77 | await ctx.render('wx/ajaxdetail', { 78 | data: { 79 | title: 'ajax详情', 80 | }, 81 | }); 82 | } 83 | 84 | async wxajaxitemdetail() { 85 | const { ctx } = this; 86 | await ctx.render('wx/ajaxitemdetail', { 87 | data: { 88 | title: '单个ajax详情信息', 89 | }, 90 | }); 91 | } 92 | 93 | // 错误分类列表 94 | async wxerroravg() { 95 | const { ctx } = this; 96 | await ctx.render('wx/erroravg', { 97 | data: { 98 | title: '错误分类列表', 99 | }, 100 | }); 101 | } 102 | 103 | // 错误详情列表 104 | async wxerrordetail() { 105 | const { ctx } = this; 106 | await ctx.render('wx/errordetail', { 107 | data: { 108 | title: '错误详情列表', 109 | }, 110 | }); 111 | } 112 | // 错误页面详情信息 113 | async wxerroritemdetail() { 114 | const { ctx } = this; 115 | await ctx.render('wx/erroritemdetail', { 116 | data: { 117 | title: '错误页面详情信息', 118 | }, 119 | }); 120 | } 121 | 122 | // 用户访问轨迹 123 | async analysislist() { 124 | const { ctx } = this; 125 | await ctx.render('wx/analysislist', { 126 | data: { 127 | title: '用户行为访问轨迹', 128 | }, 129 | }); 130 | } 131 | 132 | // 访问轨迹详情 133 | async analysisdetail() { 134 | const { ctx } = this; 135 | await ctx.render('wx/analysisdetail', { 136 | data: { 137 | title: '用户访问轨迹详情', 138 | }, 139 | }); 140 | } 141 | // TOP分析 142 | async wxtop() { 143 | const { ctx } = this; 144 | await ctx.render('wx/top', { 145 | data: { 146 | title: 'TOP指标', 147 | }, 148 | }); 149 | } 150 | 151 | async wxdiagram() { 152 | const { ctx } = this; 153 | await ctx.render('wx/diagram', { 154 | data: { 155 | title: '全国省份访问量热力图', 156 | }, 157 | }); 158 | } 159 | } 160 | 161 | module.exports = WxController; 162 | -------------------------------------------------------------------------------- /app/middleware/token_required.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { URL } = require('url'); 3 | 4 | // 校验用户是否登录 5 | module.exports = () => { 6 | return async function(ctx, next) { 7 | const referer = ctx.request.header.referer || ''; 8 | const url = new URL(referer); 9 | if (ctx.app.config.origin && ctx.app.config.origin.indexOf(url.origin) === -1) { 10 | ctx.body = { 11 | code: 1004, 12 | desc: '域名来源有误,请检查config的origin配置', 13 | }; 14 | return; 15 | } 16 | const usertoken = ctx.cookies.get('usertoken', { 17 | encrypt: true, 18 | signed: true, 19 | }) || ''; 20 | if (!usertoken) { 21 | ctx.body = { 22 | code: 1004, 23 | desc: '用户未登录,请重新登录', 24 | }; 25 | return; 26 | } 27 | const data = await ctx.service.user.finUserForToken(usertoken); 28 | if (!data || !data.user_name) { 29 | ctx.cookies.set('usertoken', ''); 30 | const descr = data && !data.user_name ? data.desc : '登录用户无效,请重新登录!'; 31 | ctx.body = { 32 | code: 1004, 33 | desc: descr, 34 | }; 35 | return; 36 | } 37 | await next(); 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /app/model/Web/web_ajaxs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const conn = app.mongooseDB.get('db3'); 7 | 8 | const WebAjaxsSchema = new Schema({ 9 | app_id: { type: String }, // 所属系统ID 10 | create_time: { type: Date, default: Date.now }, 11 | url: { type: String }, // 访问的ajaxUrl 12 | speed_type: { type: Number }, // 访问速度类型 1:正常 2:慢 13 | method: { type: String }, // 资源请求方式 14 | duration: { type: Number, default: 0 }, // AJAX响应时间 单位:ms 15 | decoded_body_size: { type: Number, default: 0 }, // 返回字段大小 单位:B 16 | options: { type: String }, // ajax请求参数 17 | full_url: { type: String }, // 完整url 18 | call_url: { type: String }, // 调用页面的URL 19 | mark_page: { type: String }, // 所有资源页面统一标识 html img css js 用户系统信息等 20 | mark_user: { type: String }, // 统一某一时间段用户标识 21 | }, { 22 | shardKey: { _id: 'hashed' }, 23 | }); 24 | 25 | WebAjaxsSchema.index({ speed_type: 1, url: 1, create_time: -1 }); 26 | WebAjaxsSchema.index({ speed_type: 1, call_url: 1, create_time: -1 }); 27 | 28 | app.models.WebAjaxs = function(appId) { 29 | return conn.model(`web_ajaxs_${appId}`, WebAjaxsSchema); 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /app/model/Web/web_environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const conn = app.mongooseDB.get('db3'); 7 | 8 | const WebEnvironmentSchema = new Schema({ 9 | app_id: { type: String }, // 所属系统 10 | create_time: { type: Date, default: Date.now }, // 用户访问时间 11 | url: { type: String }, // 访问页面的url 12 | mark_page: { type: String }, // 所有资源页面统一标识 html img css js 用户系统信息等 13 | mark_user: { type: String }, // 统一某一时间段用户标识 14 | mark_uv: { type: String }, // 统一uv标识 15 | browser: { type: String }, // 浏览器名称 16 | borwser_version: { type: String }, // 浏览器版本 17 | system: { type: String }, // 操作系统 18 | system_version: { type: String }, // 系统版本 19 | ip: { type: String }, // 访问者IP 20 | county: { type: String }, // 国家 21 | province: { type: String }, // 省 22 | city: { type: String }, // 市 23 | }, { 24 | shardKey: { _id: 'hashed' }, 25 | }); 26 | WebEnvironmentSchema.index({ url: 1, create_time: -1 }); 27 | WebEnvironmentSchema.index({ ip: 1, create_time: -1 }); 28 | WebEnvironmentSchema.index({ create_time: -1 }); 29 | WebEnvironmentSchema.index({ mark_page: 1 }); 30 | WebEnvironmentSchema.index({ mark_user: 1 }); 31 | 32 | app.models.WebEnvironment = function(appId) { 33 | return conn.model(`web_environment_${appId}`, WebEnvironmentSchema); 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /app/model/Web/web_errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const conn = app.mongooseDB.get('db3'); 7 | 8 | const WebErrorsSchema = new Schema({ 9 | app_id: { type: String }, // 所属系统 10 | url: { type: String }, // 访问页面的url 11 | create_time: { type: Date, default: Date.now }, // 用户访问时间 12 | msg: { type: String }, // 错误信息 13 | category: { type: String }, // 错误类型 14 | resource_url: { type: String }, // 错误资源URL 15 | target: { type: String }, // 资源类型 16 | type: { type: String }, // 错误类型 17 | status: { type: String }, // HTTP状态码 18 | text: { type: String }, // 资源错误提示 19 | col: { type: String }, // js错误列号 20 | line: { type: String }, // js错误行号 21 | querydata: { type: String }, // http请求参数 22 | method: { type: String }, // 资源请求方式 23 | fullurl: { type: String }, // 完整url 24 | mark_page: { type: String }, // 所有资源页面统一标识 html img css js 用户系统信息等 25 | mark_user: { type: String }, // 统一某一时间段用户标识 26 | }, { 27 | shardKey: { _id: 'hashed' }, 28 | }); 29 | 30 | WebErrorsSchema.index({ category: 1, resource_url: 1, create_time: -1 }); 31 | WebErrorsSchema.index({ resource_url: 1, create_time: -1 }); 32 | WebErrorsSchema.index({ create_time: -1 }); 33 | 34 | app.models.WebErrors = function(appId) { 35 | return conn.model(`web_errors_${appId}`, WebErrorsSchema); 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /app/model/Web/web_pages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const conn = app.mongooseDB.get('db3'); 7 | 8 | const WebPagesSchema = new Schema({ 9 | app_id: { type: String }, // 所属系统id 10 | create_time: { type: Date, default: Date.now }, // 访问时间 11 | url: { type: String }, // url域名 12 | full_url: { type: String }, // 完整域名 13 | pre_url: { type: String }, // 用户访问的上一个页面,本页面来源 14 | speed_type: { type: Number }, // 访问速度类型 1:正常 2:慢 15 | is_first_in: { type: Number }, // 是否是某次会话的首次进入 2: 首次 1:非首次 16 | mark_page: { type: String }, // 所有资源页面统一标识 html img css js 用户系统信息等 17 | mark_user: { type: String }, // 统一某一时间段用户标识 18 | load_time: { type: Number }, // 页面完全加载时间 单位:ms 19 | dns_time: { type: Number }, // dns解析时间 单位:ms 20 | tcp_time: { type: Number }, // TCP连接时间 21 | dom_time: { type: Number }, // DOM构建时间 单位:ms 22 | resource_list: { type: Array }, // 资源性能数据列表 23 | total_res_size: { type: Number }, // 页面资源大小 24 | white_time: { type: Number }, // 白屏时间 单位:ms 25 | redirect_time: { type: Number }, // 页面重定向时间 26 | unload_time: { type: Number }, // unload 时间 27 | request_time: { type: Number }, // request请求耗时 28 | analysisDom_time: { type: Number }, // 解析dom耗时 29 | ready_time: { type: Number }, // 页面准备时间 30 | screenwidth: { type: Number }, // 屏幕宽度 31 | screenheight: { type: Number }, // 屏幕高度 32 | }, { 33 | shardKey: { _id: 'hashed' }, 34 | }); 35 | 36 | WebPagesSchema.index({ speed_type: 1, is_first_in: 1, url: 1, create_time: -1 }); 37 | WebPagesSchema.index({ create_time: -1 }); 38 | 39 | app.models.WebPages = function(appId) { 40 | return conn.model(`web_pages_${appId}`, WebPagesSchema); 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /app/model/Web/web_pvuvip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const conn = app.mongooseDB.get('db3'); 7 | 8 | const WebPvUvIpSchema = new Schema({ 9 | app_id: { type: String }, // 所属系统ID 10 | pv: { type: Number }, // PV统计 11 | uv: { type: Number }, // uv统计 12 | ip: { type: Number }, // ip统计 13 | ajax: { type: Number }, // ajax访问量统计 14 | bounce: { type: String }, // 跳出率 15 | depth: { type: Number }, // 平均访问深度 16 | flow: { type: Number }, // 流量消费总额 17 | type: { type: Number, default: 1 }, // 1:每分钟数据 2:每小时数据 18 | create_time: { type: Date, default: Date.now }, 19 | }); 20 | 21 | WebPvUvIpSchema.index({ type: 1, app_id: 1, bounce: 1, depth: 1, create_time: 1 }); 22 | WebPvUvIpSchema.index({ type: 1, app_id: 1, create_time: 1 }); 23 | 24 | return conn.model('WebPvUvIp', WebPvUvIpSchema); 25 | }; 26 | -------------------------------------------------------------------------------- /app/model/Web/web_report.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const Mixed = Schema.Types.Mixed; 7 | const conn = app.mongooseDB.get('db1'); 8 | if (!conn) return; 9 | 10 | const WebReportSchema = new Schema({ 11 | app_id: { type: String }, // 系统标识 12 | create_time: { type: Date, default: Date.now }, // 创建时间 13 | user_agent: { type: String }, // 用户浏览器信息标识 14 | ip: { type: String }, // 用户ip 15 | mark_page: { type: String }, // 所有资源页面统一标识 html img css js 用户系统信息等 16 | mark_user: { type: String }, // 统一某一时间段用户标识 17 | mark_uv: { type: String }, // 统一uv标识 18 | url: { type: String }, // 访问url 19 | pre_url: { type: String }, // 上一页面来源 20 | performance: { type: Mixed }, // 用户浏览器性能数据 21 | error_list: { type: Mixed }, // 错误信息列表 22 | resource_list: { type: Mixed }, // 资源性能数据列表 23 | screenwidth: { type: Number }, // 屏幕宽度 24 | screenheight: { type: Number }, // 屏幕高度 25 | type: { type: Number }, // 1:网页性能上报 2:后续操作ajax上报 3:后续操作错误上报 26 | }); 27 | WebReportSchema.index({ create_time: 1 }); 28 | 29 | return conn.model('WebReport', WebReportSchema); 30 | }; 31 | -------------------------------------------------------------------------------- /app/model/Web/web_resource.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const Mixed = Schema.Types.Mixed; 7 | const conn = app.mongooseDB.get('db3'); 8 | 9 | const WebResourceSchema = new Schema({ 10 | app_id: { type: String }, // 所属系统 11 | create_time: { type: Date, default: Date.now }, // 用户访问时间 12 | url: { type: String }, // 访问页面的url 13 | full_url: { type: String }, // 完整的资源名称 14 | speed_type: { type: Number }, // 访问速度类型 1:正常 2:慢 15 | resource_datas: { type: Mixed }, // 页面所有加载资源json对象 16 | name: { type: String }, // 资源名称 17 | method: { type: String, default: 'GET' }, // 资源请求方式 18 | type: { type: String }, // 资源类型 19 | duration: { type: Number, default: 0 }, // 资源请求耗时 20 | decoded_body_size: { type: Number, default: 0 }, // 资源请求返回大小 21 | next_hop_protocol: { type: String, default: 'http/1.1' }, // 资源请求类型 22 | mark_page: { type: String }, // 所有资源页面统一标识 html img css js 用户系统信息等 23 | mark_user: { type: String }, // 统一某一时间段用户标识 24 | }, { 25 | shardKey: { _id: 'hashed' }, 26 | }); 27 | 28 | WebResourceSchema.index({ speed_type: 1, name: 1, create_time: -1 }); 29 | WebResourceSchema.index({ name: 1, create_time: -1 }); 30 | WebResourceSchema.index({ speed_type: 1, url: 1 }); 31 | 32 | app.models.WebResource = function(appId) { 33 | return conn.model(`web_resources_${appId}`, WebResourceSchema); 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /app/model/Web/web_statis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const conn = app.mongooseDB.get('db3'); 7 | 8 | const WebStatisSchema = new Schema({ 9 | app_id: { type: String }, // 所属系统ID 10 | top_pages: { type: Array }, // top访问page列表 11 | top_jump_out: { type: Array }, // top访问跳出率页面列表 12 | top_browser: { type: Array }, // top浏览器排行 13 | provinces: { type: Array }, // 省份访问流量排行 14 | create_time: { type: Date, default: Date.now }, 15 | }); 16 | 17 | WebStatisSchema.index({ app_id: 1, create_time: 1 }); 18 | 19 | return conn.model('WebStatis', WebStatisSchema); 20 | }; 21 | -------------------------------------------------------------------------------- /app/model/Wx/wx_ajaxs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const Mixed = Schema.Types.Mixed; 7 | const conn = app.mongooseDB.get('db3'); 8 | 9 | const WxAjaxsSchema = new Schema({ 10 | app_id: { type: String }, // 系统标识 11 | create_time: { type: Date, default: Date.now }, // 创建时间 12 | mark_page: { type: String }, // 所有资源页面统一标识 13 | mark_user: { type: String }, // 统一某一时间段用户标识 14 | duration: { type: Number }, // 请求耗时 15 | name: { type: String }, // api路径 16 | full_name: { type: String }, // api路径 17 | method: { type: String }, // 请求方式 18 | body_size: { type: Number }, // 返回资源大小 19 | options: { type: Mixed }, // 请求参数 20 | speed_type: { type: Number }, // 访问速度类型 1:正常 2:慢 21 | path: { type: String }, // 所属path路径 22 | }, { 23 | shardKey: { _id: 'hashed' }, 24 | }); 25 | 26 | WxAjaxsSchema.index({ speed_type: 1, name: 1, create_time: -1 }); 27 | WxAjaxsSchema.index({ speed_type: 1, path: 1, create_time: -1 }); 28 | 29 | app.models.WxAjaxs = function(appId) { 30 | return conn.model(`wx_ajaxs_${appId}`, WxAjaxsSchema); 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /app/model/Wx/wx_errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const Mixed = Schema.Types.Mixed; 7 | const conn = app.mongooseDB.get('db3'); 8 | 9 | const WxErrorsSchema = new Schema({ 10 | app_id: { type: String }, // 系统标识 11 | create_time: { type: Date, default: Date.now }, // 创建时间 12 | mark_page: { type: String }, // 所有资源页面统一标识 13 | mark_user: { type: String }, // 统一某一时间段用户标识 14 | col: { type: Number }, // 错误行 15 | line: { type: Number }, // 错误列 16 | name: { type: String }, // 错误资源名称 17 | msg: { type: String }, // 错误信息 18 | type: { type: String }, // 错误类型 19 | method: { type: String }, // ajax请求方式 20 | status: { type: String }, // ajax请求返回状态 21 | options: { type: Mixed }, // ajax请求参数 22 | path: { type: String }, // 所属path路径 23 | }, { 24 | shardKey: { _id: 'hashed' }, 25 | }); 26 | 27 | WxErrorsSchema.index({ type: 1, name: 1, create_time: 1 }); 28 | WxErrorsSchema.index({ type: 1, path: 1, create_time: 1 }); 29 | WxErrorsSchema.index({ name: 1, create_time: -1 }); 30 | 31 | app.models.WxErrors = function(appId) { 32 | return conn.model(`wx_errors_${appId}`, WxErrorsSchema); 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /app/model/Wx/wx_pages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const Mixed = Schema.Types.Mixed; 7 | const conn = app.mongooseDB.get('db3'); 8 | 9 | const WxPagesSchema = new Schema({ 10 | app_id: { type: String }, // 系统标识 11 | create_time: { type: Date, default: Date.now }, // 创建时间 12 | path: { type: String }, // 当前路径 13 | options: { type: Mixed }, // 路径参数 14 | mark_page: { type: String }, // 所有资源页面统一标识 15 | mark_user: { type: String }, // 统一某一时间段用户标识 16 | mark_uv: { type: String }, // 统一uv标识 17 | net: { type: String }, // 网络类型 18 | ip: { type: String }, // 用户ip 19 | county: { type: String }, // 国家 20 | province: { type: String }, // 省 21 | city: { type: String }, // 市 22 | brand: { type: String }, // 手机品牌 23 | model: { type: String }, // 手机型号 24 | screenWidth: { type: String }, // 屏幕宽度 25 | screenHeight: { type: String }, // 屏幕高度 26 | language: { type: String }, // 微信设置的语言 27 | version: { type: String }, // 微信版本号 28 | system: { type: String }, // 操作系统版本 29 | platform: { type: String }, // 客户端平台 30 | SDKVersion: { type: String }, // 客户端基础库版本 31 | }, { 32 | shardKey: { _id: 'hashed' }, 33 | }); 34 | 35 | WxPagesSchema.index({ path: 1, create_time: -1 }); 36 | WxPagesSchema.index({ create_time: -1 }); 37 | WxPagesSchema.index({ mark_page: -1 }); 38 | WxPagesSchema.index({ mark_user: -1 }); 39 | 40 | app.models.WxPages = function(appId) { 41 | return conn.model(`wx_pages_${appId}`, WxPagesSchema); 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /app/model/Wx/wx_pvuvip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const conn = app.mongooseDB.get('db3'); 7 | 8 | const WxPvUvIpSchema = new Schema({ 9 | app_id: { type: String }, // 所属系统ID 10 | pv: { type: Number }, // PV统计 11 | uv: { type: Number }, // uv统计 12 | ip: { type: Number }, // ip统计 13 | ajax: { type: Number }, // ajax访问量统计 14 | bounce: { type: String }, // 跳出率 15 | depth: { type: Number }, // 平均访问深度 16 | flow: { type: Number }, // 流量消费总额 17 | type: { type: Number, default: 1 }, // 1:每分钟数据 2:每天数据 18 | create_time: { type: Date, default: Date.now }, 19 | }); 20 | 21 | WxPvUvIpSchema.index({ type: 1, app_id: 1, create_time: 1 }); 22 | WxPvUvIpSchema.index({ type: 1, app_id: 1, bounce: 1, depth: 1, create_time: 1 }); 23 | 24 | return conn.model('WxPvUvIp', WxPvUvIpSchema); 25 | }; 26 | -------------------------------------------------------------------------------- /app/model/Wx/wx_report.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const Mixed = Schema.Types.Mixed; 7 | const conn = app.mongooseDB.get('db1'); 8 | if (!conn) return; 9 | 10 | const WxReportSchema = new Schema({ 11 | app_id: { type: String }, // 系统标识 12 | create_time: { type: Date, default: Date.now }, // 创建时间 13 | errs: { type: Array }, // 用户浏览器信息标识 14 | ip: { type: String }, // 用户ip 15 | mark_page: { type: String }, // 所有资源页面统一标识 html img css js 用户系统信息等 16 | mark_user: { type: String }, // 统一某一时间段用户标识 17 | mark_uv: { type: String }, // 统一uv标识 18 | net: { type: String }, // 网络类型 19 | system: { type: Mixed }, // 用户手机信息 20 | loc: { type: Mixed }, // 用户地理位置信息 21 | userInfo: { type: Mixed }, // 用户信息 22 | pages: { type: Mixed }, // 小程序path路径信息 23 | ajaxs: { type: Array }, // 当前path页面ajax相关信息 24 | type: { type: Number }, // 1:网页级别上报 2:后续操作ajax|error上报 25 | }); 26 | WxReportSchema.index({ create_time: 1 }); 27 | 28 | return conn.model('WxReport', WxReportSchema); 29 | }; 30 | -------------------------------------------------------------------------------- /app/model/Wx/wx_statis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const conn = app.mongooseDB.get('db3'); 7 | 8 | const WxStatisSchema = new Schema({ 9 | app_id: { type: String }, // 所属系统ID 10 | top_pages: { type: Array }, // top访问page列表 11 | top_jump_out: { type: Array }, // top访问跳出率页面列表 12 | top_brand: { type: Array }, // top手机品牌排行 13 | provinces: { type: Array }, // 省份访问流量排行 14 | create_time: { type: Date, default: Date.now }, 15 | }); 16 | 17 | WxStatisSchema.index({ app_id: 1, create_time: 1 }); 18 | 19 | return conn.model('WxStatis', WxStatisSchema); 20 | }; 21 | -------------------------------------------------------------------------------- /app/model/email.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const conn = app.mongooseDB.get('db3'); 7 | 8 | const EmailSchema = new Schema({ 9 | email: { type: String }, // 用户名称 10 | name: { type: String }, // 用户名 11 | system_ids: { type: Array }, // 用户所拥有的应用信息 12 | create_time: { type: Date, default: Date.now }, // 用户访问时间 13 | }); 14 | 15 | EmailSchema.index({ email: -1 }); 16 | 17 | return conn.model('Email', EmailSchema); 18 | }; 19 | -------------------------------------------------------------------------------- /app/model/ip_library.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const conn = app.mongooseDB.get('db3'); 7 | 8 | const ipLibrarySchema = new Schema({ 9 | ip: { type: String }, // ip 10 | country: { type: String }, // 国家 11 | province: { type: String }, // 省 12 | city: { type: String }, // 市 13 | latitude: { type: Number }, // 纬度 14 | longitude: { type: Number }, // 经度 15 | }); 16 | 17 | ipLibrarySchema.index({ ip: -1 }); 18 | 19 | return conn.model('IpLibrary', ipLibrarySchema); 20 | }; 21 | -------------------------------------------------------------------------------- /app/model/system.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const conn = app.mongooseDB.get('db3'); 7 | 8 | const WebResourceSchema = new Schema({ 9 | system_domain: { type: String }, // 系统 域名 10 | system_name: { type: String }, // 系统名称 11 | app_id: { type: String }, // 系统appId标识 12 | type: { type: String, default: 'web' }, // 浏览器:web 微信小程序 :wx 13 | user_id: { type: Array }, // 应用所属用户ID 14 | create_time: { type: Date, default: Date.now }, // 用户访问时间 15 | is_use: { type: Number, default: 0 }, // 是否需要统计 0:是 1:否 16 | slow_page_time: { type: Number, default: 5 }, // 页面加载页面阀值 单位:s 17 | slow_js_time: { type: Number, default: 2 }, // js慢资源阀值 单位:s 18 | slow_css_time: { type: Number, default: 2 }, // 慢加载css资源阀值 单位:S 19 | slow_img_time: { type: Number, default: 2 }, // 慢图片加载资源阀值 单位:S 20 | slow_ajax_time: { type: Number, default: 2 }, // AJAX加载阀值 21 | is_statisi_pages: { type: Number, default: 0 }, // 是否统计页面性能信息 0:是 1:否 22 | is_statisi_ajax: { type: Number, default: 0 }, // 是否统计页面Ajax性能资源 0:是 1:否 23 | is_statisi_resource: { type: Number, default: 0 }, // 是否统计页面加载资源性能信息 0:是 1:否 24 | is_statisi_system: { type: Number, default: 0 }, // 是否存储用户系统信息资源信息 0:是 1:否 25 | is_statisi_error: { type: Number, default: 0 }, // 是否上报页面错误信息 0:是 1:否 26 | is_daily_use: { type: Number, default: 0 }, // 是否发送日报 0:是 1:否 27 | daliy_list: { type: Array }, // 日报列表 28 | is_highest_use: { type: Number, default: 0 }, // 是否发送pv邮件 0:是 1:否 29 | highest_list: { type: Array }, // 突破历史pv峰值时发送邮件列表 30 | }); 31 | 32 | WebResourceSchema.index({ app_id: -1, create_time: 1, system_domain: -1, user_id: -1 }); 33 | 34 | return conn.model('System', WebResourceSchema); 35 | }; 36 | -------------------------------------------------------------------------------- /app/model/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const mongoose = app.mongoose; 5 | const Schema = mongoose.Schema; 6 | const conn = app.mongooseDB.get('db3'); 7 | 8 | const UserSchema = new Schema({ 9 | user_name: { type: String }, // 用户名称 10 | pass_word: { type: String }, // 用户密码 11 | system_ids: { type: Array }, // 用户所拥有的系统Id 12 | is_use: { type: Number, default: 0 }, // 是否禁用 0:正常 1:禁用 13 | level: { type: Number, default: 1 }, // 用户等级(0:管理员,1:普通用户) 14 | token: { type: String }, // 用户秘钥 15 | usertoken: { type: String }, // 用户登录态秘钥 16 | create_time: { type: Date, default: Date.now }, // 用户访问时间 17 | }); 18 | 19 | UserSchema.index({ user_name: -1, token: -1 }); 20 | 21 | return conn.model('User', UserSchema); 22 | }; 23 | -------------------------------------------------------------------------------- /app/public/file/web-report-axios.min.js.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/file/web-report-axios.min.js.zip -------------------------------------------------------------------------------- /app/public/file/web-report-default.min.js.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/file/web-report-default.min.js.zip -------------------------------------------------------------------------------- /app/public/file/web-report-fetch.min.js.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/file/web-report-fetch.min.js.zip -------------------------------------------------------------------------------- /app/public/file/web-report-jquery.min.js.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/file/web-report-jquery.min.js.zip -------------------------------------------------------------------------------- /app/public/file/web-report-none.min.js.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/file/web-report-none.min.js.zip -------------------------------------------------------------------------------- /app/public/file/web-report-vue.min.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/file/web-report-vue.min.zip -------------------------------------------------------------------------------- /app/public/file/wx-report-sdk.min.js.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/file/wx-report-sdk.min.js.zip -------------------------------------------------------------------------------- /app/public/font/JTUSjIg1_i6t8kCHKm459WlhzQ.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/font/JTUSjIg1_i6t8kCHKm459WlhzQ.woff -------------------------------------------------------------------------------- /app/public/img/PopLayer-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/img/PopLayer-close.png -------------------------------------------------------------------------------- /app/public/img/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/img/github.png -------------------------------------------------------------------------------- /app/public/img/icon-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/img/icon-1.png -------------------------------------------------------------------------------- /app/public/img/icon-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/img/icon-2.png -------------------------------------------------------------------------------- /app/public/img/icon-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/img/icon-3.png -------------------------------------------------------------------------------- /app/public/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/img/loading.gif -------------------------------------------------------------------------------- /app/public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/img/logo.png -------------------------------------------------------------------------------- /app/public/img/logo_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/img/logo_1.png -------------------------------------------------------------------------------- /app/public/img/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/img/wechat.png -------------------------------------------------------------------------------- /app/public/img/weibo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/img/weibo.png -------------------------------------------------------------------------------- /app/public/js/config.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const config = { 5 | // 登陆页面 6 | loginUrl: '/login', 7 | 8 | // 登陆成功后需要跳转到的页面 9 | homeUrl: '/', 10 | 11 | // 根接口 12 | baseApi: '/', 13 | 14 | // ajax 请求超时时间 15 | ajaxtimeout: 15000, 16 | 17 | // 发送验证码时间间隔 18 | msgTime: 60, 19 | 20 | // 七牛图片根地址 21 | imgBaseUrl: 'http://ormfcl92t.bkt.clouddn.com/', 22 | 23 | // 隐藏显示时间 24 | containerShowTime: 10, 25 | 26 | // pagesize 分页数量 27 | pageSize: 50, 28 | }; 29 | 30 | window.config = config; // eslint-disable-line 31 | -------------------------------------------------------------------------------- /app/public/js/vue-components.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | let Component = { 4 | commonsearch:{ 5 | template: ``, 22 | props:{ 23 | done:{ 24 | type:Function, 25 | default:()=>{} 26 | }, 27 | }, 28 | data:function(){ 29 | return{ 30 | timeText:'全部' 31 | } 32 | }, 33 | mounted(){ 34 | let _this=this; 35 | // 添加active样式 36 | let selecttimes = util.getStorage('local', 'userselectTime') || 60000 37 | let objs = $('.select-time li') 38 | for(let i=0,len=objs.length;i { 48 | e.stopPropagation(); 49 | $('.select-time').show(); 50 | }); 51 | $(document).on('click',function(e){ 52 | $('.select-time').hide(); 53 | }); 54 | $('.select-time').click(function(e){ 55 | e.stopPropagation(); 56 | }) 57 | $('.select-time li').on('click',function(e){ 58 | $('.select-time li').removeClass('active') 59 | $(this).addClass('active') 60 | let time = $(this).attr('data-time') 61 | let text = $(this).attr('data-text') 62 | _this.timeText = text 63 | util.setStorage('local','userselectTime',time) 64 | }) 65 | }, 66 | methods:{ 67 | timeSure(){ 68 | $('.select-time').hide(); 69 | this.done&&this.done() 70 | } 71 | }, 72 | } 73 | } 74 | for(let n in Component){ 75 | Vue.component(n, Component[n]) 76 | } 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /app/public/js/vue-filters.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // 时间格式化 3 | if(!new Date().format){ 4 | Date.prototype.format = function (fmt) { //author: meizz 5 | var o = { 6 | "M+": this.getMonth() + 1, //月份 7 | "d+": this.getDate(), //日 8 | "h+": this.getHours(), //小时 9 | "H+":this.getHours()>12?this.getHours()-12:this.getHours(), 10 | "m+": this.getMinutes(), //分 11 | "s+": this.getSeconds(), //秒 12 | "q+": Math.floor((this.getMonth() + 3) / 3), //季度 13 | "S": this.getMilliseconds() //毫秒 14 | }; 15 | if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); 16 | for (var k in o) 17 | if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); 18 | return fmt; 19 | } 20 | }; 21 | 22 | let Filter = { 23 | // 图片地址过滤器 24 | imgBaseUrl:function(img) { 25 | if (!img) return '../images/index/bg-0.png'; 26 | if (img.indexOf('http:') !== -1 || img.indexOf('HTTP:') !== -1 || img.indexOf('https:') !== -1 || img.indexOf('HTTPS:') !== -1) { 27 | return img + '?imageslim'; 28 | } else { 29 | return config.imgBaseUrl + img + '?imageslim'; 30 | } 31 | }, 32 | toFixed(val,type=false){ 33 | val = parseFloat(val) 34 | if(type){ 35 | val = val/1000 36 | return val>0?val.toFixed(3)+' s':val.toFixed(2); 37 | }else{ 38 | return val.toFixed(2)+' ms'; 39 | } 40 | }, 41 | toSize(val){ 42 | val=val*1 43 | if(val>=1024){ 44 | return (val/1024).toFixed(2)+' KB' 45 | }else if(val>0){ 46 | return val.toFixed(2)+' B' 47 | }else{ 48 | return 0 49 | } 50 | }, 51 | // 时间过滤器 52 | date(value, gengefu, full) { 53 | if (!value) return; 54 | let ty = gengefu || '-'; 55 | if (full) { 56 | return new Date(value).format('yyyy' + ty + 'MM' + ty + 'dd hh:mm:ss'); 57 | } else { 58 | return new Date(value).format('yyyy' + ty + 'MM' + ty + 'dd'); 59 | }; 60 | }, 61 | //limitTo过滤器 62 | limitTo(value, num) { 63 | if (!value) return; 64 | var text = ""; 65 | if (value.length < num) { 66 | text = value; 67 | } else { 68 | text = value.substring(0, num) + '···'; 69 | } 70 | return text; 71 | }, 72 | // 应用类型过滤器 73 | systemType(val) { 74 | let result = ''; 75 | switch (val) { 76 | case 'web': 77 | result = 'WEB浏览器'; 78 | break; 79 | case 'wx': 80 | result = '微信小程序'; 81 | break; 82 | } 83 | return result; 84 | }, 85 | // 流量单位 86 | flow(val = 0) { 87 | let result = 0; 88 | let value = val; 89 | let index = 0; 90 | while (value >= 1024) { 91 | value = value / 1024 92 | index++; 93 | } 94 | value = value.toFixed(2); 95 | if (index >= 4) { 96 | value = value + 'T' 97 | } else if (index >= 3) { 98 | value = value + 'G' 99 | } else if (index >= 2) { 100 | value = value + 'M' 101 | } else if (index >= 1) { 102 | value = value + 'KB' 103 | } else { 104 | value = value + 'B' 105 | } 106 | return value; 107 | }, 108 | } 109 | 110 | window.Filter = {}; 111 | 112 | for(let n in Filter){ 113 | window['Filter'][n] = Filter[n]; 114 | } 115 | -------------------------------------------------------------------------------- /app/public/lib/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/lib/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /app/public/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /app/public/lib/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/lib/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /app/public/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/app/public/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /app/public/lib/bootstrap/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /app/public/lib/page/page.css: -------------------------------------------------------------------------------- 1 | /*分页插件样式*/ 2 | /**width: 1180px; margin: 0 auto; text-align: center; */ 3 | .copot-page{width: 100%;color:#555555;font-size:12px;} 4 | #copot-page{padding: 10px 0; color: #a9a9a9;} 5 | .copot-page a{display:inline-block;padding:3px 8px;border:solid 1px #E7E7E7;border-radius:3px;margin-right:5px;text-decoration:none;font-size:12px;background:#fff; color: #a9a9a9;} 6 | .copot-page a:hover{background:#887FFE;color:#fff;} 7 | .copot-page a.nowPage{background:#887FFE;border:solid 1px #887FFE;color:#fff;} 8 | .copot-page .Page-search-span{margin:0 5px 0 15px;} 9 | .copot-page #Page-search{width:40px;height:28px;padding-left:10px;margin:0 3px;border:solid 1px #E7E7E7;} 10 | .copot-page #pageSearch{width:40px;height:28px;font-size:12px;border:solid 1px #887FFE;background:#887FFE;color:#fff;cursor:pointer; } -------------------------------------------------------------------------------- /app/public/lib/page/page.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | function Page(json) { 4 | this.nowPage = parseInt(json.nowPage); //当前页 5 | this.parent = json.parent; //分页内容所放的div元素 6 | this.call = json.callback; 7 | this.totalCount = json.totalCount; 8 | this.pageSize = json.pageSize; 9 | this.totalPage = Math.ceil(this.totalCount / this.pageSize); //总页数 10 | this.html = ""; 11 | this.setting = json.setting || { 12 | defaultPage: 5, //默认展示的页数 13 | firstPageText: "首页", //第一页的字 (可以是:Home) 14 | prevPageText: "上一页", //上一页的字 15 | nextPageText: "下一页", //下一页的字 16 | lastPageText: "尾页" //尾页的字 17 | }; 18 | this.centPage = Math.ceil(this.setting.defaultPage / 2); //中间页 19 | this.init(); //初始化 20 | } 21 | //初始化函数 22 | Page.prototype.init = function () { 23 | this.parent.html(""); //清空 24 | this.firstPage(); //首页 25 | this.prevPageText(); //上一页 26 | this.everyPage(); //分页 27 | this.nextPage(); //下一页 28 | this.lastPage(); //尾页 29 | this.totalPageText(); //页数显示信息 30 | this.parent.append(this.html); 31 | this.callback(); 32 | } 33 | 34 | //循环页数 35 | Page.prototype.everyPage = function () { 36 | if (this.totalPage <= this.setting.defaultPage) { 37 | for (var i = 1; i <= this.totalPage; i++) { 38 | if (this.nowPage == i) { 39 | this.html += "" + i + ""; 40 | } else { 41 | this.html += "" + i + ""; 42 | } 43 | } 44 | } else { 45 | for (var i = 1; i <= this.setting.defaultPage; i++) { 46 | var page1 = this.nowPage - this.centPage + i; 47 | var page2 = this.totalPage - this.setting.defaultPage + i; 48 | 49 | if (this.nowPage < this.centPage) { 50 | if (this.nowPage == i) { 51 | this.html += "" + i + ""; 52 | } else { 53 | this.html += "" + i + ""; 54 | } 55 | } else if (this.nowPage > this.totalPage - this.centPage) { 56 | if (this.setting.defaultPage - (this.totalPage - this.nowPage) == i) { 57 | this.html += "" + this.nowPage + ""; 58 | } else { 59 | this.html += "" + page2 + ""; 60 | } 61 | } else { 62 | if (this.centPage == i) { 63 | this.html += "" + page1 + ""; 64 | } else { 65 | this.html += "" + page1 + ""; 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | //首页 73 | Page.prototype.firstPage = function () { 74 | if (this.nowPage > this.centPage && this.totalPage >= this.setting.defaultPage + 1) { 75 | this.html += "" + this.setting.firstPageText + ""; 76 | } 77 | } 78 | //上一页 79 | Page.prototype.prevPageText = function () { 80 | if (this.nowPage >= 2) { 81 | this.html += "" + this.setting.prevPageText + ""; 82 | } 83 | } 84 | 85 | //下一页 86 | Page.prototype.nextPage = function () { 87 | if (this.totalPage - this.nowPage >= 1) { 88 | this.html += "" + this.setting.nextPageText + ""; 89 | } 90 | } 91 | 92 | //尾页 93 | Page.prototype.lastPage = function () { 94 | if (this.totalPage - this.nowPage >= this.centPage && this.totalPage > this.setting.defaultPage) { 95 | this.html += "" + this.setting.lastPageText + ""; 96 | } 97 | } 98 | 99 | //总共页数 100 | Page.prototype.totalPageText = function () { 101 | this.html += "" + this.totalCount + "条记录 每页" + this.pageSize + "条 第" + this.nowPage + "页/共" + this.totalPage + ""; 102 | } 103 | 104 | //点击分页执行的函数 105 | Page.prototype.callback = function () { 106 | var This = this; 107 | 108 | //给每个a绑定事件 109 | this.parent.find("a").bind("click", function () { 110 | window.scrollTo(0, 0) 111 | // Layer.loading({width:300,height:110,srcType:3,title:"正在加载中,请稍后..."}); 112 | This.parent.html(""); //清空 113 | var nowPage = $(this).attr("href").substring(1); 114 | //写入分页 115 | new Page({ 116 | parent: This.parent, 117 | nowPage: nowPage, 118 | totalPage: This.totalPage, 119 | pageSize: This.pageSize, 120 | totalCount: This.totalCount, 121 | setting: This.setting, 122 | callback: This.call 123 | }); 124 | 125 | This.call(nowPage, This.totalPage); //传参 126 | 127 | return false; 128 | }); 129 | 130 | } -------------------------------------------------------------------------------- /app/public/lib/popup/popup.css: -------------------------------------------------------------------------------- 1 | /*layer-mobile-css*/ 2 | @keyframes popScaleShow{from{opacity: 0;}to{opacity: 1;}} 3 | @-webkit-keyframes popScaleShow{from{opacity: 0;}to{opacity: 1;}} 4 | @keyframes popScale{from{opacity: 0;transform:scale(0.5,0.5);}to{opacity: 1;transform:scale(1,1);}} 5 | @-webkit-keyframes popScale{from{opacity: 0;-webkit-transform:scale(0.5,0.5);}to{opacity: 1;-webkit-transform:scale(1,1);}} 6 | iframe { display: block;} 7 | .popup{width:100%;box-sizing:border-box;height:100%;position: fixed;left:0;top:0;z-index:100000;animation:popScaleShow 300ms linear;-webkit-animation:popScaleShow 300ms linear;} 8 | .popup .mask{width:100%;height:100%;position: absolute;left:0;top:0;background: rgba(0,0,0,.6);z-index:-1;} 9 | .popup .popup_main{position:fixed;left:50%;top:50%;min-width:350px;min-height:100px;overflow:hidden;max-width:90%!important; 10 | transform:translateX(-50%);-webkit-transform:translateX(-50%);background:transparent; 11 | } 12 | .maminContent{width:100%;height:100%;background: #fff;overflow: hidden;animation:popScale 300ms;-webkit-animation:popScale 300ms;} 13 | .popup .maskMain{background:rgba(0,0,0,.6);color:#fff;} 14 | .popup .header_poup{height:30px;padding-left:46px;margin-top:10px;line-height:50px;position: relative;font-size:16px;} 15 | .popup .header_poup span{display:block;width:15px;height:15px;cursor:pointer;background: url('/public/img/PopLayer-close.png') no-repeat top center;background-size:80% 80%;position: absolute;right:20px;top:15px;} 16 | .popup .header_poup i{width:25px;height:25px;display:inline-block;background: url('/public/img/icon-1.png') no-repeat center center;position: absolute; 17 | left:15px;top:10px; 18 | } 19 | .popup .header_poup i.icon-1{background: url('/public/img/icon-1.png') no-repeat center center;} 20 | .popup .header_poup i.icon-2{background: url('/public/img/icon-2.png') no-repeat center center;} 21 | .popup .header_poup i.icon-3{background: url('/public/img/icon-3.png') no-repeat center center;} 22 | .popup .content{line-height:25px;padding:30px 20px 10px 50px;color:#888;} 23 | .popup .footer{height:60px;width:100%;line-height: 40px;position: relative;margin-top:20px;padding-left:50px;} 24 | .popup .footer span{font-size:14px;height:35px;line-height:35px;cursor:pointer;display:inline-block;text-align:center;color:#4a4a4a;} 25 | .popup .footer .yes{padding:0px 25px;background: #ff4200;color:#fff;border-radius:5px;} 26 | .popup .footer .yes:hover, 27 | .popup .footer .yes5:hover{background:#ef6536;} 28 | .popup .footer .yes5{} 29 | .popup .footer .yesok{background:#ff4200;} 30 | .popup .footer .no{padding:0 15px;border:solid 1px #999;border-radius: 5px;margin-left:15px;} 31 | .popup .footer .no:hover{background:#e8e3e3;} 32 | .popup-hide .popup_main{min-width:100px;} 33 | .popup-hide .popup_main .content{line-height:50px;} 34 | .popup-hide .content{text-align:center;} 35 | .popup-loading{cursor: wait;} 36 | .popup-loading .popup_main{min-width:150px;height:60px;padding:0 15px 0 5px;} 37 | .popup-loading .popup_main .content{line-height:60px;} 38 | .popup-loading .popup_main .content:before{content: "";display: block;width:40px;height:60px;background: url('/public/img/loading.gif') no-repeat center center;float:left; 39 | background-size: 60%;margin-top:-15px; 40 | } 41 | .popup-loading .content{text-align:center;} 42 | .popup .html{line-height:25px;} 43 | .popup-iframe .popup_main{width:80%;height:80%;left:10%;top:10%;} 44 | .popup-iframe .popup_main .contentios{overflow-scrolling:touch;-webkit-overflow-scrolling:touch;overflow-y: scroll; } 45 | .popup .yesHtml{text-align:center;cursor:pointer;color:#f48e08;padding-top:15px;border-top:solid 1px #dcdcdc;} 46 | .popup .yesHtml:hover{color:#b76b07;} 47 | .popup .popup_main .content-no{padding-top:12px;padding-bottom:12px;line-height:25px;} 48 | .popup .popup_main .miss_popup{border-radius: 5px!important;color:#fff;} 49 | .popup .popup_main .miss_popup .content-no{padding-left:20px;color:#fff;} -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | require('./router/home')(app); 5 | require('./router/api')(app); 6 | require('./router/web/web')(app); 7 | require('./router/web/api')(app); 8 | require('./router/wx/web')(app); 9 | require('./router/wx/api')(app); 10 | }; 11 | -------------------------------------------------------------------------------- /app/router/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const apiV1Router = app.router.namespace('/api/v1/'); 5 | const { controller, middleware } = app; 6 | const { 7 | user, 8 | remove, 9 | system, 10 | errors, 11 | emails, 12 | } = controller.api; 13 | 14 | // 校验用户是否登录中间件 15 | const tokenRequired = middleware.tokenRequired(); 16 | 17 | // -----------------用户相关------------------ 18 | // 用户登录 19 | apiV1Router.post('user/login', user.login); 20 | // 用户注册 21 | apiV1Router.post('user/register', user.register); 22 | // 退出登录 23 | apiV1Router.get('user/logout', tokenRequired, user.logout); 24 | // 获得用户列表 25 | apiV1Router.post('user/getUserList', tokenRequired, user.getUserList); 26 | // 冻结解冻用户 27 | apiV1Router.post('user/setIsUse', tokenRequired, user.setIsUse); 28 | // 删除用户 29 | apiV1Router.post('user/delete', tokenRequired, user.delete); 30 | 31 | // -----------------github 登录------------------ 32 | apiV1Router.get('github/callback', user.githubLogin); 33 | 34 | // -----------------新浪微博 登录------------------ 35 | apiV1Router.get('weibo/callback', user.weiboLogin); 36 | 37 | // -----------------微信微博 登录------------------ 38 | apiV1Router.get('wechat/callback', user.wechatLogin); 39 | 40 | // ----------------系统配置相关--------------- 41 | // 新增系统 42 | apiV1Router.post('system/add', tokenRequired, system.addNewSystem); 43 | // 修改系统 44 | apiV1Router.post('system/update', tokenRequired, system.updateSystem); 45 | // 根据用户ID获得系统信息 46 | apiV1Router.get('system/getSysForUserId', tokenRequired, system.getSysForUserId); 47 | // 根据系统ID获得单个系统信息 48 | apiV1Router.get('system/getSystemForId', tokenRequired, system.getSystemForId); 49 | // 获得系统列表 50 | apiV1Router.get('system/web/list', tokenRequired, system.getWebSystemList); 51 | // 删除系统中某个用户 52 | apiV1Router.post('system/deleteUser', tokenRequired, system.deleteWebSystemUser); 53 | // 新增系统中某个用户 54 | apiV1Router.post('system/addUser', tokenRequired, system.addWebSystemUser); 55 | // 删除某个系统 56 | apiV1Router.post('system/deleteSystem', tokenRequired, system.deleteSystem); 57 | // 日报邮件操作 58 | apiV1Router.post('system/handleDaliyEmail', tokenRequired, system.handleDaliyEmail); 59 | 60 | // -------------------清空数据----------------------------- 61 | // 清空db1 1日之前无用数据 62 | apiV1Router.post('remove/deleteDb1WebData', tokenRequired, remove.deleteDb1WebData); 63 | // 清空db2 number日之前所有性能数据 64 | apiV1Router.post('remove/deleteDb2WebData', tokenRequired, remove.deleteDb2WebData); 65 | 66 | // -------------------系统错误信息----------------------------- 67 | apiV1Router.get('errors/getSysDbErrorList', tokenRequired, errors.getSysDbErrorList); 68 | 69 | // -------------------邮件信息----------------------------- 70 | apiV1Router.get('emails/list', tokenRequired, emails.getList); 71 | 72 | apiV1Router.post('emails/add', tokenRequired, emails.addEmail); 73 | 74 | apiV1Router.post('emails/delete', tokenRequired, emails.deleteEmail); 75 | 76 | }; 77 | -------------------------------------------------------------------------------- /app/router/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const { router, controller } = app; 5 | const { home } = controller.web; 6 | 7 | // 应用列表 8 | router.get('/', home.systemlist); 9 | 10 | // 新增系统选择系统类型 11 | router.get('/selectype', home.selectype); 12 | 13 | // 用户登录 14 | router.get('/login', home.login); 15 | 16 | // 系统列表 17 | router.get('/systems', home.systems); 18 | 19 | // 用户管理 20 | router.get('/users', home.users); 21 | 22 | // 系统重启错误信息 23 | router.get('/errors', home.errors); 24 | 25 | // 邮件管理 26 | router.get('/emails', home.emails); 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /app/router/web/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const apiV1Router = app.router.namespace('/api/v1/'); 5 | const { controller, middleware } = app; 6 | const { 7 | report, 8 | pvuvip, 9 | pages, 10 | environment, 11 | ajax, 12 | resource, 13 | error, 14 | analysis, 15 | } = controller.api.web; 16 | 17 | // 校验用户是否登录中间件 18 | const tokenRequired = middleware.tokenRequired(); 19 | 20 | // 浏览器用户数据上报 21 | apiV1Router.post('report/web', report.webReport); 22 | 23 | // ----------------pv uv ip--------------- 24 | // 获得实时统计概况 25 | apiV1Router.get('pvuvip/getPvUvIpSurvey', tokenRequired, pvuvip.getPvUvIpSurvey); 26 | // 获得某日统计概况 27 | apiV1Router.get('pvuvip/getPvUvIpSurveyOne', tokenRequired, pvuvip.getPvUvIpSurveyOne); 28 | // 实时获取pv uv ip信息 (多条数据) 29 | apiV1Router.post('pvuvip/getPvUvIpList', tokenRequired, pvuvip.getPvUvIpList); 30 | // 实时获取pv uv ip信息 (单条数据) 31 | apiV1Router.post('pvuvip/getPvUvIpOne', tokenRequired, pvuvip.getPvUvIpOne); 32 | // 获得历史pvuvip统计列表 33 | apiV1Router.get('pvuvip/getHistoryPvUvIplist', tokenRequired, pvuvip.getHistoryPvUvIplist); 34 | 35 | // ----------------- 用户漏斗分析 ---------------- 36 | // 用户行为轨迹列表 37 | apiV1Router.get('analysis/getAnalysislist', tokenRequired, analysis.getAnalysislist); 38 | // 单个用户行为轨迹列表 39 | apiV1Router.get('analysis/getAnalysisOneList', tokenRequired, analysis.getAnalysisOneList); 40 | // top list 41 | apiV1Router.get('analysis/getTopDatas', tokenRequired, analysis.getTopDatas); 42 | // 省总访问量排行 43 | apiV1Router.get('analysis/getProvinceAvgCount', tokenRequired, analysis.getProvinceAvgCount); 44 | 45 | // ----------------页面性能分析--------------- 46 | // 平均列表 47 | apiV1Router.get('pages/getAveragePageList', tokenRequired, pages.getAveragePageList); 48 | // 单个页面性能列表 49 | apiV1Router.get('pages/getOnePageList', tokenRequired, pages.getOnePageList); 50 | // 单个页面详情 51 | apiV1Router.get('pages/getPageDetails', tokenRequired, pages.getPageDetails); 52 | 53 | // ----------------用户系统位置ip等信息--------------- 54 | // 获得用户系统、地址位置、浏览器分类 55 | apiV1Router.get('environment/getDataGroupBy', tokenRequired, environment.getDataGroupBy); 56 | // 根据mark_page获得用户系统信息 57 | apiV1Router.get('environment/getEnvironmentForPage', tokenRequired, environment.getEnvironmentForPage); 58 | 59 | // -------------------ajax----------------------------- 60 | // 根据url获得ajax信息 61 | apiV1Router.get('ajax/getPageAjaxsAvg', tokenRequired, ajax.getPageAjaxsAvg); 62 | // 获得ajax平均性能列表 63 | apiV1Router.get('ajax/getAverageAjaxList', tokenRequired, ajax.getAverageAjaxList); 64 | // 获得单个api的平均性能数据 65 | apiV1Router.get('ajax/getOneAjaxAvg', tokenRequired, ajax.getOneAjaxAvg); 66 | // 获得单个api的性能列表数据 67 | apiV1Router.get('ajax/getOneAjaxList', tokenRequired, ajax.getOneAjaxList); 68 | // 获得单个ajax详情 69 | apiV1Router.get('ajax/getOneAjaxDetail', tokenRequired, ajax.getOneAjaxDetail); 70 | 71 | // -------------------resource资源----------------------------- 72 | // 根据资源类型获得数据 73 | apiV1Router.get('resource/getResourceForType', tokenRequired, resource.getResourceForType); 74 | // 获得resource平均性能列表 75 | apiV1Router.get('resource/getAverageResourceList', tokenRequired, resource.getAverageResourceList); 76 | // 获得单个resource的平均性能数据 77 | apiV1Router.get('resource/getOneResourceAvg', tokenRequired, resource.getOneResourceAvg); 78 | // 获得单个resource的性能列表数据 79 | apiV1Router.get('resource/getOneResourceList', tokenRequired, resource.getOneResourceList); 80 | // 获得单个resource的性能详细信息 81 | apiV1Router.get('resource/getOneResourceDetail', tokenRequired, resource.getOneResourceDetail); 82 | 83 | // -------------------resource资源----------------------------- 84 | // 获得错误分类信息 85 | apiV1Router.get('error/getAverageErrorList', tokenRequired, error.getAverageErrorList); 86 | // 获得单个错误详情列表 87 | apiV1Router.get('error/getOneErrorList', tokenRequired, error.getOneErrorList); 88 | // 单个错误详情 89 | apiV1Router.get('error/getErrorDetail', tokenRequired, error.getErrorDetail); 90 | 91 | }; 92 | -------------------------------------------------------------------------------- /app/router/web/web.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const { router, controller } = app; 5 | const { web } = controller.web; 6 | 7 | // ------------------------------ 浏览器 ------------------------------- 8 | // 首页pvuvip数据统计 9 | router.get('/web/home', web.webhome); 10 | 11 | // 用户访问轨迹 12 | router.get('/web/analysislist', web.analysislist); 13 | 14 | // 访问轨迹详情 15 | router.get('/web/analysisdetail', web.analysisdetail); 16 | 17 | // 访问页面平均性能 18 | router.get('/web/pagesavg', web.webpagesavg); 19 | 20 | // 单页面访问页面列表性能 21 | router.get('/web/pageslist', web.webpageslist); 22 | 23 | // 单个页面详情 24 | router.get('/web/pagesdetails', web.webpagedetails); 25 | 26 | // 单页面慢性能列表 27 | router.get('/web/slowpageslist', web.webslowpageslist); 28 | 29 | // ajax平均性能列表 30 | router.get('/web/ajaxavg', web.webajaxavg); 31 | 32 | // ajax详情列表 33 | router.get('/web/ajaxdetail', web.webajaxdetail); 34 | 35 | // ajax item详情信息 36 | router.get('/web/ajaxitemdetail', web.webajaxitemdetail); 37 | 38 | // 慢资源列表 39 | router.get('/web/resourcesavg', web.webresourceavg); 40 | 41 | // 慢资源详情 42 | router.get('/web/resourcesdetail', web.webresourcedetail); 43 | 44 | // 单个资源详情信息 45 | router.get('/web/resourcesitemdetail', web.webresourcesitemdetail); 46 | 47 | // 分类错误资源列表 48 | router.get('/web/erroravg', web.weberroravg); 49 | 50 | // 错误详情列表 51 | router.get('/web/errordetail', web.weberrordetail); 52 | 53 | // 错误item详情信息 54 | router.get('/web/erroritemdetail', web.weberroritemdetail); 55 | 56 | // web设置 57 | router.get('/web/setting', web.websetting); 58 | 59 | // web端新增系统 60 | router.get('/web/addsystem', web.webaddsystem); 61 | 62 | // top指标 63 | router.get('/web/top', web.webtop); 64 | 65 | // 城市热力图 66 | router.get('/web/diagram', web.webdiagram); 67 | 68 | // 告警 69 | router.get('/web/alarm', web.webalarm); 70 | 71 | }; 72 | -------------------------------------------------------------------------------- /app/router/wx/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const apiV1Router = app.router.namespace('/api/v1/wx/'); 5 | const { controller, middleware } = app; 6 | const { 7 | report, 8 | pvuvip, 9 | pages, 10 | ajax, 11 | error, 12 | analysis, 13 | } = controller.api.wx; 14 | 15 | // 校验用户是否登录中间件 16 | const tokenRequired = middleware.tokenRequired(); 17 | 18 | // 微信小程序数据上报 19 | apiV1Router.post('report/wx', report.wxReport); 20 | 21 | // ----------------pv uv ip--------------- 22 | // 获得实时统计概况 23 | apiV1Router.get('pvuvip/getPvUvIpSurvey', tokenRequired, pvuvip.getPvUvIpSurvey); 24 | // 获得某日统计概况 25 | apiV1Router.get('pvuvip/getPvUvIpSurveyOne', tokenRequired, pvuvip.getPvUvIpSurveyOne); 26 | // 实时获取pv uv ip信息 (多条数据) 27 | apiV1Router.post('pvuvip/getPvUvIpList', tokenRequired, pvuvip.getPvUvIpList); 28 | // 实时获取pv uv ip信息 (单条数据) 29 | apiV1Router.post('pvuvip/getPvUvIpOne', tokenRequired, pvuvip.getPvUvIpOne); 30 | // 获得历史pvuvip统计列表 31 | apiV1Router.get('pvuvip/getHistoryPvUvIplist', tokenRequired, pvuvip.getHistoryPvUvIplist); 32 | 33 | // ----------------- 用户漏斗分析 ---------------- 34 | // 用户行为轨迹列表 35 | apiV1Router.get('analysis/getAnalysislist', tokenRequired, analysis.getAnalysislist); 36 | // 单个用户行为轨迹列表 37 | apiV1Router.get('analysis/getAnalysisOneList', tokenRequired, analysis.getAnalysisOneList); 38 | // top list 39 | apiV1Router.get('analysis/getTopDatas', tokenRequired, analysis.getTopDatas); 40 | // 省总访问量排行 41 | apiV1Router.get('analysis/getProvinceAvgCount', tokenRequired, analysis.getProvinceAvgCount); 42 | 43 | // ----------------页面性能分析--------------- 44 | // 平均列表 45 | apiV1Router.get('pages/getAveragePageList', tokenRequired, pages.getAveragePageList); 46 | // 单个页面性能列表 47 | apiV1Router.get('pages/getOnePageList', tokenRequired, pages.getOnePageList); 48 | // 单个页面详情 49 | apiV1Router.get('pages/getPageDetails', tokenRequired, pages.getPageDetails); 50 | // 获得用户系统、地址位置、浏览器分类 51 | apiV1Router.get('pages/getDataGroupBy', tokenRequired, pages.getDataGroupBy); 52 | // 根据markpage获得页面详情信息 53 | apiV1Router.get('pages/getPageForMarkpage', tokenRequired, pages.getPageForMarkpage); 54 | 55 | // // -------------------ajax----------------------------- 56 | // 根据url获得ajax信息 57 | apiV1Router.get('ajax/getPageAjaxsAvg', tokenRequired, ajax.getPageAjaxsAvg); 58 | // 获得ajax平均性能列表 59 | apiV1Router.get('ajax/getAverageAjaxList', tokenRequired, ajax.getAverageAjaxList); 60 | // 获得单个api的平均性能数据 61 | apiV1Router.get('ajax/getOneAjaxAvg', tokenRequired, ajax.getOneAjaxAvg); 62 | // 获得单个api的性能列表数据 63 | apiV1Router.get('ajax/getOneAjaxList', tokenRequired, ajax.getOneAjaxList); 64 | // 获得单个ajax详情 65 | apiV1Router.get('ajax/getOneAjaxDetail', tokenRequired, ajax.getOneAjaxDetail); 66 | 67 | // -------------------error资源----------------------------- 68 | // 获得错误分类信息 69 | apiV1Router.get('error/getAverageErrorList', tokenRequired, error.getAverageErrorList); 70 | // 获得单个错误详情列表 71 | apiV1Router.get('error/getOneErrorList', tokenRequired, error.getOneErrorList); 72 | // 单个错误详情 73 | apiV1Router.get('error/getErrorDetail', tokenRequired, error.getErrorDetail); 74 | 75 | }; 76 | -------------------------------------------------------------------------------- /app/router/wx/web.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const { router, controller } = app; 5 | const { wx } = controller.web; 6 | 7 | // ------------------------------ 微信小程序 ------------------------------- 8 | 9 | // web端新增系统 10 | router.get('/wx/addsystem', wx.wxaddsystem); 11 | 12 | // 用户访问轨迹 13 | router.get('/wx/analysislist', wx.analysislist); 14 | 15 | // 访问轨迹详情 16 | router.get('/wx/analysisdetail', wx.analysisdetail); 17 | 18 | // 小程序首页 19 | router.get('/wx/home', wx.wxhome); 20 | 21 | // 小程序设置页面 22 | router.get('/wx/setting', wx.wxsetting); 23 | 24 | // 访问页面平均性能 25 | router.get('/wx/pagesavg', wx.wxpagesavg); 26 | 27 | // 单页面访问页面列表性能 28 | router.get('/wx/pageslist', wx.wxpageslist); 29 | 30 | // 单个页面详情 31 | router.get('/wx/pagesdetails', wx.wxpagedetails); 32 | 33 | // ajax平均性能列表 34 | router.get('/wx/ajaxavg', wx.wxajaxavg); 35 | 36 | // ajax详情列表 37 | router.get('/wx/ajaxdetail', wx.wxajaxdetail); 38 | 39 | // ajax item详情信息 40 | router.get('/wx/ajaxitemdetail', wx.wxajaxitemdetail); 41 | 42 | // 分类错误资源列表 43 | router.get('/wx/erroravg', wx.wxerroravg); 44 | 45 | // 错误详情列表 46 | router.get('/wx/errordetail', wx.wxerrordetail); 47 | 48 | // 错误item详情信息 49 | router.get('/wx/erroritemdetail', wx.wxerroritemdetail); 50 | 51 | // top指标 52 | router.get('/wx/top', wx.wxtop); 53 | 54 | // 城市热力图 55 | router.get('/wx/diagram', wx.wxdiagram); 56 | }; 57 | -------------------------------------------------------------------------------- /app/schedule/delete_report.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 处理数据定时任务 4 | module.exports = app => { 5 | return { 6 | schedule: { 7 | cron: app.config.pvuvip_task_day_time, 8 | type: 'worker', 9 | disable: app.config.report_data_type !== 'mongodb', 10 | }, 11 | // 每天执行一次,定时删除上报的原始数据 12 | async task(ctx) { 13 | if (app.config.is_web_task_run || app.config.is_wx_task_run) { 14 | // 保证集群servers task不冲突 15 | const preminute = await app.redis.get('delete_task_day_time') || ''; 16 | const value = app.config.cluster.listen.ip + ':' + app.config.cluster.listen.port; 17 | if (preminute && preminute !== value) return; 18 | if (!preminute) { 19 | await app.redis.set('delete_task_day_time', value, 'EX', 20000); 20 | const preminutetwo = await app.redis.get('delete_task_day_time'); 21 | if (preminutetwo !== value) return; 22 | } 23 | if (app.config.is_web_task_run) await ctx.service.remove.deleteDb1WebData(1); 24 | if (app.config.is_wx_task_run) await ctx.service.remove.deleteDb1WebData(2); 25 | } 26 | }, 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /app/schedule/ip_task.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 处理数据定时任务 4 | module.exports = app => { 5 | return { 6 | schedule: { 7 | cron: app.config.ip_task_time, 8 | type: 'worker', 9 | disable: !(app.config.is_web_task_run || app.config.is_wx_task_run), 10 | }, 11 | // 定时处理ip城市地理位置信息 12 | async task(ctx) { 13 | // 保证集群servers task不冲突 14 | const preminute = await app.redis.get('ip_task_time') || ''; 15 | const value = app.config.cluster.listen.ip + ':' + app.config.cluster.listen.port; 16 | if (preminute && preminute !== value) return; 17 | if (!preminute) { 18 | await app.redis.set('ip_task_time', value, 'EX', 20000); 19 | const preminutetwo = await app.redis.get('ip_task_time'); 20 | if (preminutetwo !== value) return; 21 | } 22 | if (app.config.is_web_task_run) await ctx.service.web.ipTask.saveWebGetIpDatas(); 23 | if (app.config.is_wx_task_run) await ctx.service.wx.ipTask.saveWxGetIpDatas(); 24 | }, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /app/schedule/pvuvip_pre_day.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 执行pvuvip定时任务的时间间隔 4 | module.exports = app => { 5 | return { 6 | schedule: { 7 | cron: app.config.pvuvip_task_day_time, 8 | type: 'worker', 9 | disable: !(app.config.is_web_task_run || app.config.is_wx_task_run), 10 | }, 11 | // 定时处pv,uv,ip统计信息 每天执行一次 12 | async task(ctx) { 13 | // 保证集群servers task不冲突 14 | const preminute = await app.redis.get('pvuvip_task_day_time') || ''; 15 | const value = app.config.cluster.listen.ip + ':' + app.config.cluster.listen.port; 16 | if (preminute && preminute !== value) return; 17 | if (!preminute) { 18 | await app.redis.set('pvuvip_task_day_time', value, 'EX', 200000); 19 | const preminutetwo = await app.redis.get('pvuvip_task_day_time'); 20 | if (preminutetwo !== value) return; 21 | } 22 | if (app.config.is_web_task_run) await ctx.service.web.pvuvipTask.getWebPvUvIpByDay(); 23 | if (app.config.is_wx_task_run) await ctx.service.wx.pvuvipTask.getWxPvUvIpByDay(); 24 | }, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /app/schedule/pvuvip_pre_minute.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 执行pvuvip定时任务的时间间隔 每天定时执行一次 4 | module.exports = app => { 5 | return { 6 | schedule: { 7 | cron: app.config.pvuvip_task_minute_time, 8 | type: 'worker', 9 | disable: !(app.config.is_web_task_run || app.config.is_wx_task_run), 10 | }, 11 | // 定时处pv,uv,ip统计信息 每分钟执行一次 12 | async task(ctx) { 13 | // 保证集群servers task不冲突 14 | const preminute = await app.redis.get('pvuvip_task_minute_time') || ''; 15 | const value = app.config.cluster.listen.ip + ':' + app.config.cluster.listen.port; 16 | if (preminute && preminute !== value) return; 17 | if (!preminute) { 18 | await app.redis.set('pvuvip_task_minute_time', value, 'EX', 20000); 19 | const preminutetwo = await app.redis.get('pvuvip_task_minute_time'); 20 | if (preminutetwo !== value) return; 21 | } 22 | if (app.config.is_web_task_run) await ctx.service.web.pvuvipTask.getWebPvUvIpByMinute(); 23 | if (app.config.is_wx_task_run) await ctx.service.wx.pvuvipTask.getWxPvUvIpByMinute(); 24 | }, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /app/schedule/report_task_mongodb.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 处理数据定时任务 4 | module.exports = app => { 5 | return { 6 | schedule: { 7 | cron: app.config.report_task_time, 8 | type: 'worker', 9 | disable: app.config.report_data_type !== 'mongodb', 10 | }, 11 | // 定时处理上报的数据 db1同步到db3数据 12 | async task(ctx) { 13 | if (app.config.is_web_task_run || app.config.is_wx_task_run) { 14 | // 保证集群servers task不冲突 15 | const preminute = await app.redis.get('report_task_for_mongodb') || ''; 16 | const value = app.config.cluster.listen.ip + ':' + app.config.cluster.listen.port; 17 | if (preminute && preminute !== value) return; 18 | if (!preminute) { 19 | await app.redis.set('report_task_for_mongodb', value, 'EX', 20000); 20 | const preminutetwo = await app.redis.get('report_task_for_mongodb'); 21 | if (preminutetwo !== value) return; 22 | } 23 | // 查询db3是否正常,不正常则重启 24 | try { 25 | const result = await ctx.model.System.count({}).exec(); 26 | app.logger.info(`-----------db3--查询db3数据库是否可用----${result}------`); 27 | 28 | if (app.config.is_web_task_run) ctx.service.web.reportTask.saveWebReportDatasForMongodb(); 29 | if (app.config.is_wx_task_run) ctx.service.wx.reportTask.saveWxReportDatasForMongodb(); 30 | } catch (err) { 31 | app.restartMongodbs('db3', ctx, err); 32 | } 33 | } 34 | }, 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /app/schedule/report_task_redis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 处理数据定时任务 4 | module.exports = app => { 5 | return { 6 | schedule: { 7 | cron: app.config.redis_consumption.task_time, 8 | type: 'worker', 9 | disable: !(app.config.report_data_type === 'redis'), 10 | }, 11 | // 定时处理上报的数据 redis同步到db3数据 12 | async task(ctx) { 13 | if (app.config.is_web_task_run || app.config.is_wx_task_run) { 14 | // 查询db3是否正常,不正常则重启 15 | try { 16 | const result = await ctx.model.System.count({}).exec(); 17 | app.logger.info(`-----------db3--查询db3数据库是否可用----${result}------`); 18 | 19 | if (app.config.is_web_task_run) ctx.service.web.reportTask.saveWebReportDatasForRedis(); 20 | if (app.config.is_wx_task_run) ctx.service.wx.reportTask.saveWxReportDatasForRedis(); 21 | } catch (err) { 22 | app.restartMongodbs('db3', ctx, err); 23 | } 24 | } 25 | }, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /app/service/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Service = require('egg').Service; 4 | 5 | class CacheService extends Service { 6 | /* 7 | * @param {*} key 8 | * @returns 9 | * @memberof CacheService 10 | */ 11 | async get(key) { 12 | const { redis, logger } = this.app; 13 | const t = Date.now(); 14 | let data = await redis.get(key); 15 | if (!data) return; 16 | data = JSON.parse(data); 17 | const duration = (Date.now() - t); 18 | logger.debug('Cache', 'get', key, (duration + 'ms').green); 19 | return data; 20 | } 21 | 22 | /* 23 | * @param {*} key 24 | * @param {*} value 25 | * @param {*} seconds 26 | * @memberof CacheService 27 | */ 28 | async setex(key, value, seconds) { 29 | const { redis, logger } = this.app; 30 | const t = Date.now(); 31 | value = JSON.stringify(value); 32 | await redis.set(key, value, 'EX', seconds); 33 | const duration = (Date.now() - t); 34 | logger.debug('Cache', 'set', key, (duration + 'ms').green); 35 | } 36 | 37 | /* 38 | * @param {*} key 39 | * @param {*} seconds 40 | * @returns 41 | * @memberof CacheService 42 | */ 43 | async incr(key, seconds) { 44 | const { redis, logger } = this.app; 45 | const t = Date.now(); 46 | const result = await redis.multi().incr(key).expire(key, seconds) 47 | .exec(); 48 | const duration = (Date.now() - t); 49 | logger.debug('Cache', 'set', key, (duration + 'ms').green); 50 | return result[0][1]; 51 | } 52 | } 53 | 54 | module.exports = CacheService; 55 | -------------------------------------------------------------------------------- /app/service/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Service = require('egg').Service; 3 | 4 | class ErrorsService extends Service { 5 | 6 | /* 7 | * 获得列表信息 8 | * 9 | * @returns 10 | * @memberof ErrorsService 11 | */ 12 | async getErrorList() { 13 | const result = await this.app.redis.lrange('db_servers_error_list', 0, -1); 14 | return result ? result : []; 15 | } 16 | 17 | /* 18 | * 保存db和服务重启错误信息 19 | * 20 | * @param {*} type 21 | * @param {*} item 22 | * @param {*} catcherr 23 | * @memberof ErrorsService 24 | */ 25 | async saveSysAndDbErrors(type, item, catcherr) { 26 | await this.app.redis.lpush('db_servers_error_list', JSON.stringify({ 27 | dbname: type, 28 | shell: item, 29 | catch_error: catcherr, 30 | create_time: new Date(), 31 | })); 32 | } 33 | 34 | } 35 | 36 | module.exports = ErrorsService; 37 | -------------------------------------------------------------------------------- /app/service/ldap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ldap = require('ldapjs'); 4 | const Service = require('egg').Service; 5 | 6 | class LdapService extends Service { 7 | constructor(ctx) { 8 | super(ctx); 9 | const { server, ou, dc } = ctx.app.config.ldap; 10 | this.config = { ou, dc }; 11 | this.client = ldap.createClient({ 12 | url: server, 13 | }); 14 | } 15 | 16 | /* 17 | * 18 | * 19 | * @param {*} userName 20 | * @returns 21 | * @memberof LdapService 22 | */ 23 | search(userName) { 24 | const { ou, dc } = this.config; 25 | return new Promise((resolve, reject) => { 26 | const str = `cn=${userName}, ou=${ou}, dc=${dc}, dc=com`; 27 | this.client.search(str, {}, (err, res) => { 28 | if (err) { 29 | this.ctx.logger.error(`ldap search error:${err.stack}`); 30 | reject(err); 31 | } 32 | res.on('searchEntry', entry => { 33 | resolve(entry.object); 34 | }); 35 | res.on('error', err => { 36 | this.ctx.logger.error(`ldap search searchEntry error:${err.stack}`); 37 | reject(err); 38 | }); 39 | res.on('end', err => { 40 | if (err) reject(err); 41 | }); 42 | }); 43 | }); 44 | } 45 | } 46 | 47 | module.exports = LdapService; 48 | -------------------------------------------------------------------------------- /app/service/remove.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const parser = require('cron-parser'); 3 | const Service = require('egg').Service; 4 | 5 | class RemoveService extends Service { 6 | 7 | /* 8 | * 定时删除原始上报数据 一天删一次 9 | * 10 | * @param {string} [type='web'] 11 | * @returns 12 | * @memberof RemoveService 13 | */ 14 | async deleteDb1WebData(type = 'web') { 15 | try { 16 | const interval = parser.parseExpression(this.app.config.pvuvip_task_day_time); 17 | interval.prev(); 18 | interval.prev(); 19 | const endTime = new Date(interval.prev().toString()); 20 | 21 | const query = { create_time: { $lt: endTime } }; 22 | let result = ''; 23 | if (type === 'web') { 24 | result = await this.ctx.model.Web.WebReport.remove(query).exec(); 25 | } else if (type === 'wx') { 26 | result = await this.ctx.model.Wx.WxReport.remove(query).exec(); 27 | } 28 | return result; 29 | } catch (err) { 30 | return {}; 31 | } 32 | } 33 | 34 | /* 35 | * 清空db2 number日之前所有性能数据 36 | * 37 | * @param {*} appId 38 | * @param {*} number 39 | * @param {string} [type='web'] 40 | * @returns 41 | * @memberof RemoveService 42 | */ 43 | async deleteDb2WebData(appId, number, type = 'web') { 44 | number = number * 1; 45 | const interval = parser.parseExpression(this.app.config.pvuvip_task_day_time); 46 | const endTime = new Date(new Date(interval.prev().toString()).getTime() - number * 86400000); 47 | const query = { create_time: { $lt: endTime } }; 48 | let result = null; 49 | 50 | if (type === 'web') { 51 | // Ajax 52 | const remove1 = Promise.resolve(this.app.models.WebAjaxs(appId).remove(query).exec()); 53 | // Pages 54 | const remove2 = Promise.resolve(this.app.models.WebPages(appId).remove(query).exec()); 55 | // Environment 56 | const remove3 = Promise.resolve(this.app.models.WebEnvironment(appId).remove(query).exec()); 57 | // Errors 58 | const remove4 = Promise.resolve(this.app.models.WebErrors(appId).remove(query).exec()); 59 | // Resource 60 | const remove5 = Promise.resolve(this.app.models.WebResource(appId).remove(query).exec()); 61 | result = await Promise.all([ remove1, remove2, remove3, remove4, remove5 ]); 62 | } else if (type === 'wx') { 63 | // Ajax 64 | const remove1 = Promise.resolve(this.ctx.model.Wx.WxAjaxs.remove(query).exec()); 65 | // Pages 66 | const remove2 = Promise.resolve(this.ctx.model.Wx.WxPages.remove(query).exec()); 67 | // Errors 68 | const remove3 = Promise.resolve(this.ctx.model.Wx.WxErrors.remove(query).exec()); 69 | result = await Promise.all([ remove1, remove2, remove3 ]); 70 | } 71 | return result; 72 | } 73 | } 74 | 75 | module.exports = RemoveService; 76 | -------------------------------------------------------------------------------- /app/service/web/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Service = require('egg').Service; 4 | 5 | class EnvironmentService extends Service { 6 | 7 | // 获得页面性能数据平均值 8 | async getDataGroupBy(type, url, appId, beginTime, endTime) { 9 | type = type * 1; 10 | 11 | const queryjson = { $match: { url } }; 12 | if (beginTime && endTime) queryjson.$match.create_time = { $gte: new Date(beginTime), $lte: new Date(endTime) }; 13 | const group_id = { 14 | url: '$url', 15 | city: `${type === 1 ? '$city' : ''}`, 16 | browser: `${type === 2 ? '$browser' : ''}`, 17 | system: `${type === 3 ? '$system' : ''}`, 18 | }; 19 | 20 | const datas = await this.app.models.WebEnvironment(appId).aggregate([ 21 | queryjson, 22 | { 23 | $group: { 24 | _id: group_id, 25 | count: { $sum: 1 }, 26 | }, 27 | }, 28 | { $sort: { count: -1 } }, 29 | { $limit: 10 }, 30 | ]).read('sp') 31 | .exec(); 32 | 33 | return datas; 34 | } 35 | 36 | // 根据mark_page获得用户系统信息 37 | async getEnvironmentForPage(appId, markPage) { 38 | return await this.app.models.WebEnvironment(appId).findOne({ mark_page: markPage }).read('sp') 39 | .exec(); 40 | } 41 | } 42 | 43 | module.exports = EnvironmentService; 44 | -------------------------------------------------------------------------------- /app/service/web/pvuvip_task.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const parser = require('cron-parser'); 3 | const Service = require('egg').Service; 4 | class PvuvipTaskService extends Service { 5 | 6 | // 获得web端 pvuvip 7 | async getWebPvUvIpByDay() { 8 | const interval = parser.parseExpression(this.app.config.pvuvip_task_day_time); 9 | const endTime = new Date(interval.prev().toString()); 10 | const beginTime = new Date(interval.prev().toString()); 11 | const query = { create_time: { $gte: beginTime, $lt: endTime } }; 12 | 13 | const datas = await this.ctx.model.System.distinct('app_id', { type: 'web' }).read('sp').exec(); 14 | this.groupData(datas, 2, query, beginTime, endTime); 15 | } 16 | // 定时执行每分钟的数据 17 | async getWebPvUvIpByMinute() { 18 | const interval = parser.parseExpression(this.app.config.pvuvip_task_minute_time); 19 | const endTime = new Date(interval.prev().toString()); 20 | const beginTime = new Date(interval.prev().toString()); 21 | const query = { create_time: { $gte: beginTime, $lt: endTime } }; 22 | 23 | const datas = await this.ctx.model.System.distinct('app_id', { type: 'web' }).read('sp').exec(); 24 | this.groupData(datas, 1, query, endTime); 25 | } 26 | // 对数据进行分组 27 | groupData(datas, type, query, beginTime, endTime) { 28 | if (!datas && !datas.length) return; 29 | datas.forEach(item => { 30 | // pvuvip 31 | this.savePvUvIpData(item, beginTime, type, query); 32 | // top排行 33 | this.ctx.service.web.analysis.saveRealTimeTopTask(item, type, beginTime, endTime); 34 | }); 35 | } 36 | 37 | // 获得pvuvip数据 38 | async savePvUvIpData(appId, endTime, type, query) { 39 | try { 40 | const pvpro = Promise.resolve(this.ctx.service.web.pvuvip.pv(appId, query)); 41 | const uvpro = Promise.resolve(this.ctx.service.web.pvuvip.uv(appId, query)); 42 | const ippro = Promise.resolve(this.ctx.service.web.pvuvip.ip(appId, query)); 43 | const ajpro = Promise.resolve(this.ctx.service.web.pvuvip.ajax(appId, query)); 44 | const flpro = Promise.resolve(this.ctx.service.web.pvuvip.flow(appId, query)); 45 | 46 | let data = []; 47 | if (type === 1) { 48 | data = await Promise.all([ pvpro, uvpro, ippro, ajpro, flpro ]); 49 | } else if (type === 2) { 50 | const user = Promise.resolve(this.ctx.service.web.pvuvip.user(appId, query)); 51 | const bounce = Promise.resolve(this.ctx.service.web.pvuvip.bounce(appId, query)); 52 | data = await Promise.all([ pvpro, uvpro, ippro, ajpro, flpro, user, bounce ]); 53 | } 54 | const pv = data[0] || 0; 55 | const uv = data[1].length ? data[1][0].count : 0; 56 | const ip = data[2].length ? data[2][0].count : 0; 57 | const ajax = data[3] || 0; 58 | const flow = data[4] || 0; 59 | const user = type === 2 ? (data[5].length ? data[5][0].count : 0) : 0; 60 | const bounce = type === 2 ? data[6] : 0; 61 | 62 | const pvuvip = this.ctx.model.Web.WebPvuvip(); 63 | pvuvip.app_id = appId; 64 | pvuvip.pv = pv; 65 | pvuvip.uv = uv; 66 | pvuvip.ip = ip; 67 | pvuvip.ajax = ajax; 68 | pvuvip.flow = flow; 69 | if (type === 2) pvuvip.bounce = bounce ? (bounce / pv * 100).toFixed(2) + '%' : 0; 70 | if (type === 2) pvuvip.depth = pv && user ? parseInt(pv / user) : 0; 71 | pvuvip.create_time = endTime; 72 | pvuvip.type = type; 73 | await pvuvip.save(); 74 | 75 | // 触发日报邮件 76 | if (type === 2) { 77 | this.ctx.service.web.sendEmail.getDaliyDatas({ 78 | appId, 79 | pv, 80 | uv, 81 | ip, 82 | ajax, 83 | flow, 84 | bounce: bounce ? (bounce / pv * 100).toFixed(2) + '%' : 0, 85 | depth: pv && user ? parseInt(pv / user) : 0, 86 | }, 'pvuvip'); 87 | } 88 | // 流量峰值 超过历史top邮件触达 89 | if (type === 1) { 90 | this.ctx.service.emails.highestPvTipsEmail({ appId, pv, uv, ip, ajax, flow }); 91 | } 92 | } catch (err) { console.log(err); } 93 | } 94 | 95 | } 96 | 97 | module.exports = PvuvipTaskService; 98 | -------------------------------------------------------------------------------- /app/service/web/report.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Service = require('egg').Service; 4 | 5 | class ReportService extends Service { 6 | 7 | // 保存用户上报的数据 8 | async saveWebReportData(ctx) { 9 | const query = ctx.request.body; 10 | const ip = ctx.get('X-Real-IP') || ctx.get('X-Forwarded-For') || ctx.ip; 11 | 12 | const system = await this.service.system.getSystemForAppId(query.appId); 13 | if (!system) return {}; 14 | if (system.is_use !== 0) return {}; 15 | 16 | const report = ctx.model.Web.WebReport(); 17 | report.app_id = query.appId; 18 | report.is_first_in = query.isFristIn ? 2 : 1; 19 | report.create_time = query.time; 20 | report.user_agent = ctx.headers['user-agent']; 21 | report.ip = ip; 22 | report.mark_page = this.app.randomString(); 23 | report.mark_user = query.markUser; 24 | report.mark_uv = query.markUv; 25 | report.url = query.url || ctx.headers.referer; 26 | report.pre_url = query.preUrl; 27 | report.performance = query.performance; 28 | report.error_list = query.errorList; 29 | report.resource_list = query.resourceList; 30 | report.screenwidth = query.screenwidth; 31 | report.screenheight = query.screenheight; 32 | report.type = query.type || 1; 33 | report.save(); 34 | return {}; 35 | } 36 | } 37 | 38 | module.exports = ReportService; 39 | -------------------------------------------------------------------------------- /app/service/wx/pvuvip_task.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const parser = require('cron-parser'); 3 | const Service = require('egg').Service; 4 | class PvuvipTaskService extends Service { 5 | 6 | // 获得web端 pvuvip 7 | async getWxPvUvIpByDay() { 8 | const interval = parser.parseExpression(this.app.config.pvuvip_task_day_time); 9 | const endTime = new Date(interval.prev().toString()); 10 | const beginTime = new Date(interval.prev().toString()); 11 | const query = { create_time: { $gte: beginTime, $lt: endTime } }; 12 | 13 | const datas = await this.ctx.model.System.distinct('app_id', { type: 'wx' }).read('sp').exec(); 14 | this.groupData(datas, 2, query, beginTime, endTime); 15 | } 16 | // 定时执行每分钟的数据 17 | async getWxPvUvIpByMinute() { 18 | const interval = parser.parseExpression(this.app.config.pvuvip_task_minute_time); 19 | const endTime = new Date(interval.prev().toString()); 20 | const beginTime = new Date(interval.prev().toString()); 21 | const query = { create_time: { $gte: beginTime, $lt: endTime } }; 22 | 23 | const datas = await this.ctx.model.System.distinct('app_id', { type: 'wx' }).read('sp').exec(); 24 | this.groupData(datas, 1, query, endTime); 25 | } 26 | // 对数据进行分组 27 | groupData(datas, type, query, beginTime, endTime) { 28 | if (!datas && !datas.length) return; 29 | datas.forEach(item => { 30 | // pvuvip 31 | this.savePvUvIpData(item, beginTime, type, query); 32 | // top排行 33 | this.ctx.service.wx.analysis.saveRealTimeTopTask(item, type, beginTime, endTime); 34 | }); 35 | } 36 | 37 | // 获得pvuvip数据 38 | async savePvUvIpData(appId, endTime, type, query) { 39 | try { 40 | const pvpro = Promise.resolve(this.ctx.service.wx.pvuvip.pv(appId, query)); 41 | const uvpro = Promise.resolve(this.ctx.service.wx.pvuvip.uv(appId, query)); 42 | const ippro = Promise.resolve(this.ctx.service.wx.pvuvip.ip(appId, query)); 43 | const ajpro = Promise.resolve(this.ctx.service.wx.pvuvip.ajax(appId, query)); 44 | const flpro = Promise.resolve(this.ctx.service.wx.pvuvip.flow(appId, query)); 45 | 46 | let data = []; 47 | if (type === 1) { 48 | data = await Promise.all([ pvpro, uvpro, ippro, ajpro, flpro ]); 49 | } else if (type === 2) { 50 | const user = Promise.resolve(this.ctx.service.wx.pvuvip.user(appId, query)); 51 | const bounce = Promise.resolve(this.ctx.service.wx.pvuvip.bounce(appId, query)); 52 | data = await Promise.all([ pvpro, uvpro, ippro, ajpro, flpro, user, bounce ]); 53 | } 54 | 55 | const pv = data[0] || 0; 56 | const uv = data[1].length ? data[1][0].count : 0; 57 | const ip = data[2].length ? data[2][0].count : 0; 58 | const ajax = data[3] || 0; 59 | const flow = data[4].length ? data[4][0].amount : 0; 60 | const user = type === 2 ? (data[5].length ? data[5][0].count : 0) : 0; 61 | const bounce = type === 2 ? data[6] : 0; 62 | 63 | const pvuvip = this.ctx.model.Wx.WxPvuvip(); 64 | pvuvip.app_id = appId; 65 | pvuvip.pv = pv; 66 | pvuvip.uv = uv; 67 | pvuvip.ip = ip; 68 | pvuvip.ajax = ajax; 69 | pvuvip.flow = flow; 70 | if (type === 2) pvuvip.bounce = bounce ? (bounce / pv * 100).toFixed(2) + '%' : 0; 71 | if (type === 2) pvuvip.depth = pv && user ? parseInt(pv / user) : 0; 72 | pvuvip.create_time = endTime; 73 | pvuvip.type = type; 74 | await pvuvip.save(); 75 | 76 | // 每日邮件触达 77 | if (type === 2) { 78 | this.ctx.service.wx.sendEmail.getDaliyDatas({ 79 | appId, 80 | pv, 81 | uv, 82 | ip, 83 | ajax, 84 | flow, 85 | bounce: bounce ? (bounce / pv * 100).toFixed(2) + '%' : 0, 86 | depth: pv && user ? parseInt(pv / user) : 0, 87 | }, 'pvuvip'); 88 | } 89 | // 流量峰值 超过历史top邮件触达 90 | if (type === 1) { 91 | this.ctx.service.emails.highestPvTipsEmail({ appId, pv, uv, ip, ajax, flow }); 92 | } 93 | } catch (err) { console.log(err); } 94 | } 95 | } 96 | 97 | module.exports = PvuvipTaskService; 98 | -------------------------------------------------------------------------------- /app/service/wx/report.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Service = require('egg').Service; 4 | 5 | class ReportService extends Service { 6 | 7 | // 保存用户上报的数据 8 | async saveWxReportData(ctx) { 9 | const query = ctx.request.body; 10 | const ip = ctx.get('X-Real-IP') || ctx.get('X-Forwarded-For') || ctx.ip; 11 | 12 | // 参数校验 13 | if (!query.appId) throw new Error('web端上报数据操作:app_id不能为空'); 14 | 15 | const system = await this.service.system.getSystemForAppId(query.appId); 16 | if (!system) return {}; 17 | if (system.is_use !== 0) return {}; 18 | 19 | const report = ctx.model.Wx.WxReport(); 20 | report.app_id = query.appId; 21 | report.create_time = query.time; 22 | report.errs = query.errs; 23 | report.ip = ip; 24 | report.mark_page = this.app.randomString(); 25 | report.mark_user = query.markuser; 26 | report.mark_uv = query.markuv; 27 | report.net = query.net; 28 | report.system = query.system; 29 | report.loc = query.loc; 30 | report.userInfo = query.userInfo; 31 | report.pages = query.pages; 32 | report.ajaxs = query.ajaxs; 33 | report.type = query.type || 1; 34 | report.save(); 35 | return {}; 36 | } 37 | } 38 | module.exports = ReportService; 39 | -------------------------------------------------------------------------------- /app/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getUserIp = ctx => { 4 | const res = ctx.req.headers['x-forwarded-for'] || 5 | ctx.req.headers['x-real-ip'] || 6 | ctx.req.headers.remote_addr || 7 | ctx.req.headers.client_ip || 8 | ctx.req.connection.remoteAddress || 9 | ctx.req.socket.remoteAddress || 10 | ctx.req.connection.socket.remoteAddress || 11 | ctx.ip; 12 | return res.match(/[.\d\w]+/g).join(''); 13 | }; 14 | 15 | 16 | module.exports = { 17 | getUserIp, 18 | }; 19 | -------------------------------------------------------------------------------- /app/view/errors.html: -------------------------------------------------------------------------------- 1 | 11 |
12 |
13 |
系统重启信息
14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
重启DB脚本链接导致重启错误信息重启时间
{{item.dbname}}{{item.shell}}{{item.catch_error}}{{item.create_time|date('/',true)}}
32 |
33 |
34 |
35 |
暂无数据!
36 |
37 | -------------------------------------------------------------------------------- /app/view/footer.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
-------------------------------------------------------------------------------- /app/view/github.html: -------------------------------------------------------------------------------- 1 | 2 | 30 |
31 |
32 |
33 |
34 | 35 | 36 | 37 | {{title}} 38 |
39 |
40 |
41 |
42 | -------------------------------------------------------------------------------- /app/view/header.html: -------------------------------------------------------------------------------- 1 | 101 |
102 |
103 | 107 | 130 |
131 | -------------------------------------------------------------------------------- /app/view/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= data.title %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | <% include ./header.html %> 30 | <%- body %> 31 | <% include ./footer.html %> 32 |
33 | 35 | 36 | -------------------------------------------------------------------------------- /app/view/selectype.html: -------------------------------------------------------------------------------- 1 | 67 |
68 |

选择应用类型

69 |
70 |
系统类型
71 |
72 |
73 |
74 |

选择系统类

75 |
76 |
77 | 78 |
WEB浏览器
79 |
统计项目的实时PV/UV/IP,每个页面、AJAX、各种资源的性能数据,上报错误信息,让您更全面的掌控自己的应用。
80 | 81 |
82 |
83 | 84 |
微信小程序
85 |
统计项目的实时PV/UV/IP,AJAX、各种资源的性能数据,上报错误信息,让您更全面的掌控自己的应用。
86 | 87 |
88 |
89 |
90 |
91 |
92 | -------------------------------------------------------------------------------- /app/view/web/alarm.html: -------------------------------------------------------------------------------- 1 | <% include ./side.html %> 2 | 3 | 123 |
124 |

应用告警

125 |
126 |
告警列表
127 |
告警规则
128 |
告警条件
129 | 130 |
131 |
132 |
133 |
开发中...
134 |
135 |
136 |
开发中...
137 |
138 |
139 |
开发中...
140 |
141 |
142 |
143 | x 144 | -------------------------------------------------------------------------------- /app/view/web/errordetail.html: -------------------------------------------------------------------------------- 1 | <% include ./side.html %> 2 | 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 | 57 | 58 | 59 |
错误资源错误信息错误类型所属URL错误资源错误类型状态码类型详情错误行错误列请求方式访问轨迹错误时间操作呢
{{item.resource_url}}{{item.msg}}{{item.category}}{{item.url}}{{item.target}}{{item.type}}{{item.status}}{{item.text}}{{item.line}}{{item.col}}{{item.method}}查看轨迹详情{{item.create_time|date('/',true)}}详情
60 |
61 |
62 |
63 | 64 |
65 |
66 |
67 | 68 |
暂无数据!
69 |
70 |
71 | 72 | -------------------------------------------------------------------------------- /app/view/web/resourcesitemdetail.html: -------------------------------------------------------------------------------- 1 | <% include ./side.html %> 2 | 51 |
52 |
53 |

慢资源详细信息

54 | 55 |
56 |
57 |
58 | 资源地址: 59 | {{datas.name}} 60 |
61 |
62 | 资源耗时: 63 | {{datas.duration|toFixed}} 64 |
65 |
66 | 请求方式: 67 | {{datas.method}} 68 |
69 |
70 | 速度类型: 71 | {{datas.speed_type == 1?'正常':'慢'}} 72 |
73 |
74 | body大小: 75 | {{datas.decoded_body_size|toSize}} 76 |
77 |
78 | 生成时间: 79 | {{datas.create_time|date('/',true)}} 80 |
81 |
82 | 所属URL: 83 | {{datas.url}} 84 |
85 |
86 | 来源城市: 87 | {{environment.city||'未知'}} 88 |
89 |
90 | 浏览器: 91 | {{environment.browser}} 92 |
93 |
94 | 浏览器版本: 95 | {{environment.borwser_version}} 96 |
97 |
98 | 操作系统: 99 | {{environment.system}} 100 |
101 |
102 | 操作系统版本: 103 | {{environment.system_version}} 104 |
105 |
106 | IP地址: 107 | {{environment.ip}} 108 |
109 |
110 | 完整URL: 111 | {{datas.full_url}} 112 |
113 |
114 | 访问轨迹: 115 | 查看用户访问轨迹详情 116 |
117 |
118 |
119 | 120 | -------------------------------------------------------------------------------- /app/view/web/side.html: -------------------------------------------------------------------------------- 1 | 57 |
58 | 104 |
105 | -------------------------------------------------------------------------------- /app/view/wx/errordetail.html: -------------------------------------------------------------------------------- 1 | <% include ./side.html %> 2 | 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 |
错误资源错误信息错误类型所属PATH状态码错误行错误列请求方式访问轨迹错误时间操作呢
{{item.name}}{{item.msg}}{{item.type}}{{item.path}}{{item.status}}{{item.line}}{{item.col}}{{item.method}}查看轨迹详情{{item.create_time|date('/',true)}}详情
54 |
55 |
56 |
57 | 58 |
59 |
60 |
61 | 62 |
暂无数据!
63 |
64 |
65 | 66 | -------------------------------------------------------------------------------- /app/view/wx/pagedetails.html: -------------------------------------------------------------------------------- 1 | <% include ./side.html %> 2 | 51 |
52 |
53 |

PATH 路径详情信息

54 | 55 |
56 |
57 |
58 | PATH路径: 59 | {{datas.path}} 60 |
61 |
62 | 网络类型: 63 | {{datas.net}} 64 |
65 |
66 | IP: 67 | {{datas.ip}} 68 |
69 |
70 | 所属城市: 71 | {{datas.city||'未知'}} 72 |
73 |
74 | 手机品牌: 75 | {{datas.brand}} 76 |
77 |
78 | 手机类型: 79 | {{datas.model}} 80 |
81 |
82 | 微信版本: 83 | {{datas.version}} 84 |
85 |
86 | 微信语言: 87 | {{datas.language}} 88 |
89 |
90 | 手机系统版本: 91 | {{datas.system}} 92 |
93 |
94 | SDK版本: 95 | {{datas.SDKVersion}} 96 |
97 |
98 | 屏幕宽度: 99 | {{datas.screenWidth}} 100 |
101 |
102 | 屏幕高度: 103 | {{datas.screenHeight}} 104 |
105 |
106 | 生成时间: 107 | {{datas.create_time|date('/',true)}} 108 |
109 |
110 | 访问轨迹: 111 | 查看用户访问轨迹详情 112 |
113 |
114 |
115 | 116 | -------------------------------------------------------------------------------- /app/view/wx/side.html: -------------------------------------------------------------------------------- 1 | 57 |
58 | 92 |
93 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '8' 4 | 5 | install: 6 | - ps: Install-Product node $env:nodejs_version 7 | - npm i npminstall && node_modules\.bin\npminstall 8 | 9 | test_script: 10 | - node --version 11 | - npm --version 12 | - npm run test 13 | 14 | build: off 15 | -------------------------------------------------------------------------------- /config/config.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = () => { 4 | const config = exports = {}; 5 | 6 | config.debug = false; 7 | 8 | // 用于安全校验和回调域名根路径 开发路径域名(必填) 9 | // 线上环境此处替换为项目根域名 例如:https://blog.seosiwei.com (这里需要填写http|https和斜杠等字符) 10 | config.origin = 'https://www.xxx.com'; 11 | 12 | // 百度地图api key 13 | config.BAIDUAK = 'xxxxxxxxxx'; 14 | 15 | // github login 16 | config.github = { 17 | client_id: 'xxxxxx', 18 | client_secret: 'xxxxxx', 19 | scope: [ 'user' ], 20 | }; 21 | 22 | // ldap 23 | config.ldap = { 24 | server: 'ldap://xxx', // ldap服务器地址 25 | ou: 'xx', // ou 26 | dc: 'xx', // dc, 非com的另外一层的dc,例如 dc=foobar,dc=com, 这里填 foobar 27 | isLdap: false, // 是否采用ldap; 28 | }; 29 | 30 | // 新浪微博 login 31 | config.weibo = { 32 | client_id: 'xxxxxx', // 微博的App Key 33 | client_secret: 'xxxxxx', // 微博的App Secret 34 | scope: [ 'all' ], 35 | }; 36 | 37 | // wechat login 38 | config.wechat = { 39 | client_id: 'xxxxxx', // 微信的AppId 40 | client_secret: 'xxxxxx', // 微信的App Secret 41 | }; 42 | 43 | // redis配置 44 | config.redis = { 45 | client: { 46 | port: 6379, // Redis port 47 | host: 'xx.xx.xx.xx', // Redis host 48 | password: 'xxxxxx', 49 | db: 0, 50 | }, 51 | }; 52 | 53 | // mongodb 服务 54 | const dbclients = { 55 | db3: { 56 | // 单机部署 57 | url: 'mongodb://127.0.0.1:27017/performance', 58 | // 副本集 读写分离 59 | // url: 'mongodb://127.0.0.1:28100,127.0.0.1:28101,127.0.0.1:28102/performance?replicaSet=rs1', 60 | // 集群分片 61 | // url: 'mongodb://127.0.0.1:30000/performance', 62 | options: { 63 | poolSize: 100, 64 | keepAlive: 10000, 65 | connectTimeoutMS: 10000, 66 | autoReconnect: true, 67 | reconnectTries: 100, 68 | reconnectInterval: 1000, 69 | }, 70 | }, 71 | }; 72 | if (config.report_data_type === 'mongodb') { 73 | dbclients.db1 = { 74 | // url: 'mongodb://127.0.0.1:27017,127.0.0.1:27018/performance?replicaSet=performance', 75 | url: 'mongodb://127.0.0.1:27019/performance', 76 | options: { 77 | poolSize: 100, 78 | keepAlive: 10000, 79 | connectTimeoutMS: 10000, 80 | autoReconnect: true, 81 | reconnectTries: 100, 82 | reconnectInterval: 1000, 83 | }, 84 | }; 85 | } 86 | 87 | // mongoose配置 88 | config.mongoose = { 89 | clients: dbclients, 90 | }; 91 | 92 | config.security = { 93 | domainWhiteList: [ 'https://xxx.xx.com' ], 94 | csrf: { 95 | enable: false, 96 | ignore: '/api/v1/report/**', 97 | }, 98 | }; 99 | 100 | return config; 101 | }; 102 | -------------------------------------------------------------------------------- /config/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | 4 | exports.ejs = { 5 | enable: true, 6 | package: 'egg-view-ejs', 7 | }; 8 | 9 | exports.redis = { 10 | enable: true, 11 | package: 'egg-redis', 12 | }; 13 | 14 | exports.mongoose = { 15 | enable: true, 16 | package: 'egg-mongoose', 17 | }; 18 | 19 | exports.routerPlus = { 20 | enable: true, 21 | package: 'egg-router-plus', 22 | }; 23 | 24 | exports.cors = { 25 | enable: true, 26 | package: 'egg-cors', 27 | }; 28 | 29 | exports.email = { 30 | enable: false, 31 | path: path.join(__dirname, '../lib/plugin/egg-email'), 32 | }; 33 | 34 | exports.kafka = { 35 | enable: false, 36 | path: path.join(__dirname, '../lib/plugin/egg-kafka'), 37 | }; 38 | 39 | exports.alinode = { 40 | enable: false, 41 | package: 'egg-alinode', 42 | }; 43 | 44 | -------------------------------------------------------------------------------- /demo/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/demo/01.png -------------------------------------------------------------------------------- /demo/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/demo/02.png -------------------------------------------------------------------------------- /demo/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/demo/03.png -------------------------------------------------------------------------------- /demo/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/demo/04.png -------------------------------------------------------------------------------- /demo/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/demo/05.png -------------------------------------------------------------------------------- /demo/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/demo/06.png -------------------------------------------------------------------------------- /demo/07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/demo/07.png -------------------------------------------------------------------------------- /demo/08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/demo/08.png -------------------------------------------------------------------------------- /demo/09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/demo/09.png -------------------------------------------------------------------------------- /demo/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/demo/10.png -------------------------------------------------------------------------------- /demo/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/demo/11.png -------------------------------------------------------------------------------- /demo/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/demo/12.png -------------------------------------------------------------------------------- /demo/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/demo/20.png -------------------------------------------------------------------------------- /demo/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/demo/21.png -------------------------------------------------------------------------------- /demo/ewm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/demo/ewm.jpg -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "0.1" 2 | services: 3 | redis: 4 | image: redis:3.2 5 | ports: 6 | - "6379:6379" 7 | mongo: 8 | image: mongo:3.4 9 | ports: 10 | - "27017:27017" 11 | # 源 https://raw.githubusercontent.com/wurstmeister/kafka-docker/master/docker-compose.yml 12 | zookeeper: 13 | image: wurstmeister/zookeeper 14 | ports: 15 | - "2181:2181" 16 | kafka: 17 | image: wurstmeister/kafka:2.12-2.0.1 18 | ports: 19 | - "9092:9092" 20 | environment: 21 | # your docker host IP (Note: Do not use localhost or 127.0.0.1 as the host ip if you want to run multiple brokers.) 22 | KAFKA_ADVERTISED_HOST_NAME: ${hostIP} 23 | # KAFKA_ZOOKEEPER_CONNECT: zookeeper:2180 24 | KAFKA_ZOOKEEPER_CONNECT: ${hostIP}:2181 25 | -------------------------------------------------------------------------------- /docs/configs.md: -------------------------------------------------------------------------------- 1 | # config重要配置说明 2 | 3 | ## 一:定时任务启动配置 4 | ```js 5 | // 浏览器端是否开启定时任务,如果不开启,所有的web相关的统计将不会执行(pv,uv,ip,top,省流量统计等) 6 | config.is_web_task_run = true; 7 | 8 | // 小程序端是否开启定时任务,如果不开启,所有的web相关的统计将不会执行(pv,uv,ip,top,省流量统计等) 9 | config.is_wx_task_run = true; 10 | ``` 11 | 12 | > 备注:此配置项可以把整个项目拆分成2个项目,针对于web的项目和针对于小程序的项目,可以按需配置 13 | 14 | ## 二:使用redis消息队列还是使用mongodb储存上报原始数据 15 | 16 | ```js 17 | // 此配置有两个值:redus || mongodb 18 | // 如果使用redis则上报的原始数据使用redis消息队列存储 19 | // 如果使用mongodb则上报的原始数据使用mongodb数据库储存 20 | config.report_data_type = 'redis'; 21 | 备注:此配置项对上报的方式有很大的影响,推荐使用 redis 消息队列(配置更简单,性能更强) 22 | 23 | // 如果 config.report_data_type = 'redis'; 则需要配置以下参数 24 | config.redis_consumption = { 25 | // 消费者消费生产数据的定时任务 26 | task_time: '*/20 * * * * *', 27 | 28 | // 每次定时任务消费线程数(web端) 29 | thread_web: 1000, // 此配置每次回消费 60/20 * 1000 = 3000 条数据 30 | 31 | // 每次定时任务消费线程数(wx端) (如果值为0则不执行消费任务) 32 | thread_wx: 0, 33 | 34 | // 消息队列池限制数, 0:不限制 number: 限制条数 高并发时服务优雅降级方案 35 | total_limit_web: 100000, 36 | total_limit_wx: 100000, 37 | }; 38 | ``` 39 | 40 | > 备注:(total_limit_web|total_limit_wx)只有消息队列时可用,若不想限制填为0即可 41 | 42 | ## 三:定时任务时间说明 43 | 44 | ```js 45 | // 此配置于 config.report_data_type = 'mongodb' 相关联,表示每分钟db3从db1同步数据的时间间隔,默认1s 46 | config.report_task_time = '0 */1 * * * *'; 47 | 48 | // 每分钟定时统计任务 (包括:pv,uv,ip,top排行) 49 | config.pvuvip_task_minute_time = '0 */1 * * * *'; 50 | 51 | // 每天定时统计任务 (包括:pv,uv,ip,top排行,省份流量统计排行) 52 | config.pvuvip_task_day_time = '0 0 0 */1 * *'; 53 | 54 | // ip解析为城市信息定时任务时间间隔 55 | config.ip_task_time = '0 */1 * * * *'; 56 | ``` 57 | 58 | ## 四:IP定时任务解析器使用redis还是mongodb 59 | 60 | ```js 61 | // 此配置有两个值 redus || mongodb,标识ip解析为城市地址是查询的数据方式 62 | config.ip_redis_or_mongodb = 'redis' 63 | ``` 64 | 65 | > 备注:ip解析为城市在使用数据库查询之前会优先去本地缓存文件查询一遍,如果没有才会去数据库匹配 66 | 67 | # 五:IP定时任务解析器使用本地缓存设置 68 | 69 | ```js 70 | // 缓存IP地址对应城市信息的本地json文件,优先级高于 config.ip_redis_or_mongodb 配置 71 | config.ip_city_cache_file = { 72 | web: 'web_ip_city_cache_file.txt', 73 | wx: 'wx_ip_city_cache_file.txt', 74 | }; 75 | ``` 76 | 77 | ## 六:IP定时任务解析器使用redis还是mongodb 78 | 79 | ```js 80 | // 此配置表示 db3同步db1的线程数 只有当 config.report_data_type = 'mongodb' 时才会生效 81 | config.report_thread = 10; // 此配置同步数据相当于10个线程在跑 82 | 83 | // ip地址解析为城市信息线程数 数字越大每次跑的数据越多 84 | config.ip_thread = 10; // 此配置默认每次跑 10 * 60 = 600 条 85 | ``` 86 | 87 | ## 七:数据库和servers出现意外时的重启脚本 88 | 89 | ```js 90 | config.shell_restart = { 91 | // mongodb重启shell,如果mongodb服务崩溃了,服务重启脚本(可选填) 92 | mongodb: [ '/data/performance/mongodb-restart.sh' ], 93 | 94 | // node.js服务重启shell,mongodb重启时,数据库连接池有可能会断,这时重启服务(可选填) 95 | servers: [ '/data/performance/servers-restart.sh' ], 96 | }; 97 | ``` 98 | 99 | > 备注:此配置一般用在单机模式下的保证服务正常方案,集群模式下可不填写 100 | > 程序中有一套检测数据库是否正常机制,如果检测不正常则会重启db,和servers,一直重复直到重启成功,以此保证单机的高可用方案 -------------------------------------------------------------------------------- /docs/github.md: -------------------------------------------------------------------------------- 1 | # github登录授权 2 | 3 | ## 一:登录github 4 | 5 | ## 二:依次进入: Settings -> Developer settings -> New OAuth App -> Register a new OAuth application 6 | 7 | 你会看到如下界面: 8 | 9 | ![](https://github.com/wangweianger/zanePerfor/blob/master/demo/20.png) 10 | 11 | > Homepage URL: 你的根域名,也就是location.origin 12 | > Authorization callback URL: github授权成功之后的回调域名,这里的地址后缀必须是 /api/v1/github/callback 13 | 14 | ## 三:注册成功之后进入OAuth Apps,进入创建好的应用获取Client ID 和 Client Secret。 15 | 16 | 17 | ### 项目中配置授权参数 18 | 19 | - 进入项目的`config/config.default.js` 和 `config/config.prod.js` 配置相应的参数 20 | 21 | ```js 22 | // github login 23 | config.github = { 24 | // github Client ID 25 | client_id: 'xxxxxx', 26 | 27 | // github Client Secret 28 | client_secret: 'xxxxxx', 29 | 30 | // 此参数表示只获取用户信息 31 | scope: [ 'user' ], 32 | }; 33 | ``` 34 | 35 | END! -------------------------------------------------------------------------------- /docs/img/github-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/docs/img/github-login.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # zanePerfor是什么? 2 | 3 | ## zanePerfor是一个服务于性能监控的业务平台项目,目前实现了浏览器,微信小程序的相关基础性能统计。 4 | 5 | > 备注:zanePerfor目前还不完善,处于开发初期,我会长期的维护和升级 6 | 7 | > 展望:zanePerfor的目标是解决中小应用的性能统计,支持通用的数据统计和定制化的统计开发,解决高并发下的应用高可用 8 | 9 | > 性能:目前架构理论上来说可支持每日(百万、千万)级PV,未来会持续开发和优化 10 | 11 | ## zanePerfor目前实现了哪些功能? 12 | 13 | ### 浏览器端(WEB) 14 | - 页面级的性能上报(多页面 || 单页面应用程序通用) 15 | - 页面AJAX性能上报 16 | - 页面所有加载资源性能上报(图片,js,css) 17 | - 页面所有错误信息上报(js,css,ajax) 18 | - 微信小程序端 19 | - path路径对应的AJAX性能上报 20 | - 小程序错误信息上报(js,ajax,img) 21 | - 用户设备信息及其网络信息上报 22 | - 后端界面展示功能(web,小程序通用) 23 | - 统计每分钟应用的PV,UV,IP信息,统计每天的PV,UV,IP,跳出率,用户访问平均深度 24 | - 统计实时和每天的应用top最高访问排行,跳出率最高排行 25 | - 统计实时和每天的全国省份流量热力图 26 | - 统计每个用户每次访问的行为轨迹 27 | - 下面用图来展示实现的大致功能 28 | - 实时PV,UV,IP统计 29 | 30 | ### 单页面性能详情列表 31 | ![](https://github.com/wangweianger/zanePerfor/blob/master/demo/05.png) 32 | 33 | ### 单个AJAX性能详情 34 | ![](https://github.com/wangweianger/zanePerfor/blob/master/demo/06.png) 35 | 36 | ### 用户轨迹跟踪 37 | ![](https://github.com/wangweianger/zanePerfor/blob/master/demo/09.png) 38 | 39 | ### 省份实时流量统计 40 | ![](https://github.com/wangweianger/zanePerfor/blob/master/demo/12.png) 41 | -------------------------------------------------------------------------------- /docs/simple_deployment.md: -------------------------------------------------------------------------------- 1 | # 单机部署入门体验 2 | 3 | ## 一:安装Mongodb数据库 4 | 5 | > 提示:安装Mongodb的教程网上非常之多,我这里也不重复的编写,直接推荐几篇文章 6 | 7 | - 菜鸟教程:http://www.runoob.com/mongodb/mongodb-osx-install.html 8 | 9 | - Mongodb官网:https://docs.mongodb.com/manual/administration/install-enterprise/ 10 | 11 | - Mongo中文社区:http://www.mongoing.com/docs/administration/install-community.html 12 | 13 | Mongodb可视化工具推荐Robomongo,非常好用! 14 | 15 | - Robomongo:https://robomongo.org/download 16 | 17 | ## 参考:一份最基础的Mongodb配置 18 | ```js 19 | processManagement: 20 | fork: true 21 | systemLog: 22 | destination: file 23 | path: /usr/local/var/log/mongodb/mongo27017.log 24 | logAppend: true 25 | storage: 26 | dbPath: /Users/MacBook/mongodb/mongodb27017 27 | net: 28 | bindIp: 127.0.0.1 29 | port: 27017 30 | ``` 31 | 32 | ### Mongodb各项配置含义参考链接: 33 | 34 | - 官网Config:https://docs.mongodb.com/manual/reference/configuration-options/index.html 35 | 36 | - 会煮咖啡的猫咪:https://www.jianshu.com/p/f9f1454f251f 37 | 38 | 39 | ## 二:安装Redis数据库 40 | 41 | > 提示:安装Redis的教程网上也非常之多,以下链接可参考 42 | 43 | - 菜鸟教程:http://www.runoob.com/redis/redis-install.html 44 | 45 | - Redis中文网:http://www.redis.net.cn/tutorial/3503.html 46 | 47 | Redis可视化工具推荐Redis Desktop Manager,也很好用! 48 | 49 | 官网下载比较困难,对于windows和Mac收费,这里推荐使用我收藏的安装文件安装 50 | 51 | - Mac端链接:链接: https://pan.baidu.com/s/1Z1i22OhfSMh3e4-3E5Q5WA 提取码: 8pd6 52 | 53 | - Windows端链接:链接: https://pan.baidu.com/s/1CbHIXdmPuYuEBfk7rbCjRw 提取码: xkak 54 | 55 | ### 参考:Redis Config参考配置含义,推荐链接 56 | 57 | - 官网Config:https://redis.io/topics/config 58 | 59 | - 菜鸟教程:http://www.runoob.com/redis/redis-conf.html 60 | 61 | ## 三:Node.js服务部署 62 | 63 | ### 一:下载项目文件 64 | 65 | ```js 66 | git clone https://github.com/wangweianger/zanePerfor.git 67 | ``` 68 | 69 | ### 二:安装依赖 70 | 71 | ```js 72 | // 查看node版本 node版本需要 >=8.0.0 73 | node -v 74 | // 安装依赖 75 | npm install 76 | ``` 77 | 78 | > 如果你的node版本低于8.0.0,请升级 79 | 80 | > 推荐nvm安装node,Linux安装可参考 LINUX系统安装nvm 快速搭建Nodejs开发环境 81 | 82 | ### 三:配置项目Config 83 | 84 | 配置项目端口和Host (本地开发默认即可) 85 | 86 | ```js 87 | config.cluster = { 88 | listen: { 89 | port: 7001, 90 | hostname: '127.0.0.1', 91 | ip: address.ip(), 92 | }, 93 | }; 94 | ``` 95 | 96 | 配置Redis Config(本地开发默认即可) 97 | 98 | ```js 99 | config.redis = { 100 | client: { 101 | port: 6379, // Redis port 102 | host: '127.0.0.1', // Redis host 103 | password: '', 104 | db: 0, 105 | }, 106 | }; 107 | ``` 108 | 109 | 配置Mongodb服务(本地开发默认即可) 110 | 111 | ```js 112 | const dbclients = { 113 | db3: { 114 | url: 'mongodb://127.0.0.1:27019/performance', 115 | options: { 116 | poolSize: 20, 117 | }, 118 | }, 119 | }; 120 | ``` 121 | 项目各配置项含义参考说明 122 | 123 | ## 四:启动服务 124 | 125 | ```js 126 | // 本地开发 127 | npm run dev 128 | 129 | // 生产环境 130 | npm run build 131 | ``` 132 | 133 | ## 四:登录后台创建应用 134 | 135 | ### 一:浏览器打开启动好的应用链接:http://127.0.0.1:7001 136 | 如果你部署成功会看到如下登录界面 137 | 138 | 注册账号(系统默认admin为管理员,其他账号全是普通用户) 139 | 注册号之后会自动登录系统,点击添加应用,请选择你的应用类型(这里分为WEB 和 微信小程序两种类型应用) 140 | 141 | 填写应用名称和应用域名 142 | 143 | ## 五:下载SDK并进行数据的上报 144 | 145 | 以上步骤完成你会看到如下的部署配置 146 | 147 | SDK使用文档 148 | - WEB端SDK及其使用文档:https://github.com/wangweianger/web-report-sdk 149 | - 小程序端SDK及其使用文档:https://github.com/wangweianger/wx-report-sdk 150 | 151 | ### 多页面部署案例: 152 | ```js 153 | // 头部引入sdk脚本 154 | <\/script src="/js/performance-report-default.min.js"><\/script> 155 | <\script> 156 | Performance({ 157 | domain: 'http://127.0.0.1:7001/api/v1/report/web', 158 | add: { 159 | // 建好应用的appId 160 | appId: 'xxxxxxxxxxxxxx' 161 | } 162 | }) 163 | <\/script> 164 | ``` 165 | 166 | ### 单页面部署案例: 167 | 168 | - 一:项目下新建 performance.js 文件,文件内容如下 169 | 170 | ```js 171 | const Performance = require('common/js/performance-report.min') 172 | try{ 173 | Performance({ 174 | domain:'https://127.0.0.1:7001/api/v1/report/web', 175 | add:{ 176 | // 建好应用的appId 177 | appId: 'xxxxxxxxxxxxxx' 178 | } 179 | }) 180 | } catch (e) {} 181 | ``` 182 | 183 | - 二:项目main.js入口文件顶部引入SDK插件 184 | 185 | ```js 186 | // main.js 单页面入口文件 187 | // 引入jdk 188 | require('./performance') 189 | // 其他配置项 190 | import Vue from 'vue' 191 | import App from './app' 192 | import VueRouter from 'vue-router' 193 | import routes from './router' 194 | import filter from './filter' 195 | ...... 196 | ``` 197 | 198 | - 三:webpack配置加入性能SDK 199 | 200 | ```js 201 | // 入口配置 202 | entry: { 203 | performance:path.resolve(__dirname, '../src/performance.js'), 204 | // 其他配置项 205 | ... 206 | }, 207 | ``` 208 | 209 | ## 六:大功告成,开始第一次的试用吧。 210 | 其他说明:新建应用后,第一次需要给用户分配权限才会显示应用 211 | -------------------------------------------------------------------------------- /docs/tasks.md: -------------------------------------------------------------------------------- 1 | # 项目定时任务功能说明 2 | 3 | ## 一:消息队列消费者Task (建议上报使用此Task) 4 | 5 | ```js 6 | // 是否执行由 config.report_data_type = 'redis' 配置决定 7 | // 此定时任务执行频次由 config.redis_consumption.task_time 决定 8 | schedule/report_task_redis.js 9 | ``` 10 | 11 | > 开启此定时任务,消费者端会定时消费用户上报的原始数据并储存到db中,是非常重要的配置,是后续逻辑处理数据的来源。 12 | 13 | > 配置优势:性能强,使用消息队列,支持秒杀形式的高并发数据上报,并且能实现限流的功能,实现servers集群很方便 14 | 15 | ## 二:db3同步db1数据Task(不建议使用此Task) 16 | 17 | ```js 18 | // 是否执行由 config.report_data_type = 'mongodb' 配置决定 19 | // 此定时任务执行频次由 config.redis_consumption.task_time 决定 20 | schedule/report_task_mongodb.js 21 | ``` 22 | 23 | > 开启此定时任务,用户上报的原始数据会先储存到db1,然后再经过定时任务把需要的数据同步到db3 24 | 25 | > 配置劣势:性能比消息队列差很多,配置也稍复杂,没有做限流逻辑,实现了servers集群Task的不重复执行 26 | 27 | ## 三 :每分钟PV,UV,IP统计Task 28 | 29 | ```js 30 | // 此定时任务执行频次由 config.pvuvip_task_minute_time 决定 31 | schedule/pvuvip_pre_minute.js 32 | ``` 33 | 34 | > 备注:此Task实现了servers集群模式下的Task不重复执行 35 | 36 | 37 | ## 四:每天凌晨执行一次的Task 38 | 39 | ```js 40 | // 此定时任务执行频次由 config.pvuvip_task_day_time 决定 41 | // 此定时任务主要负责 每日PV,UV,IP,TOP排行,城市流量排行等每日统计功能的Task 42 | schedule/pvuvip_pre_day.js 43 | ``` 44 | 45 | > 备注:此Task实现了servers集群模式下的Task不重复执行 46 | 47 | ## 五:定时解析用户IP地理位置Task 48 | 49 | ```js 50 | // 此定时任务执行频次由 config.ip_task_time 决定 51 | // 此定时任务主要是根据用户的IP地址得到城市位置信息,方便后期的省,城市的相关统计 52 | schedule/ip_task.js 53 | ``` 54 | 55 | > 备注:此Task实现了servers集群模式下的Task不重复执行 56 | 57 | > 性能说明:此Task会先从本地文件查询,若无再从redis或者mongodb数据库查询 58 | 59 | ## 六:定时删除用户原始上报数据Task 60 | 61 | ```js 62 | // 此定时任务 需要 config.report_data_type = 'mongodb' 时才会执行 63 | // 定时删除用户前2天之前上报的原始数据 64 | schedule/delete_report.js 65 | ``` -------------------------------------------------------------------------------- /docs/vue.md: -------------------------------------------------------------------------------- 1 | ## Vue 项目使用SDK: 2 | 3 | - 进去app目录下public文件夹,解压web-report-vue-min.zip。将里面的文件复制到自己项目下,找到public文件夹或者根目录文件夹下的index.html。添加以下代码: 4 | 5 | ```js 6 | 7 | 15 | ``` 16 | 17 | 这里向大家分享一下不同环境下使用的方法: 18 | 19 | - Vue2: 可以在 **.env.build** 等环境配置文件配置环境变量,例如: `VUE_APP_BASE_URL=/abc/index` 20 | 21 | ```js 22 | 23 | 31 | ``` 32 | 33 | - Vue3: 可以在 **.env.build** 等环境配置文件配置环境变量,例如: `VITE_BUILD_HOST = 'http://xxxxx:7001'` 34 | 35 | - 然后在Vite.config.ts中新增插件:createHtmlPlugin -> npm install createHtmlPlugin -D。然后在plugins中配置以下代码 36 | 37 | ```js 38 | createHtmlPlugin({ 39 | minify: true, 40 | pages: [ 41 | { 42 | // entry: "src/main.ts", 43 | filename: "index.html", 44 | template: "index.html", 45 | injectOptions: { 46 | data: { 47 | // title: "index", 48 | injectScript: ``, 49 | }, 50 | }, 51 | }, 52 | ], 53 | }), 54 | ``` 55 | 56 | 在src下的utils文件夹新增文件inject.ts 57 | 58 | ```js 59 | window.Performance({ 60 | domain: `${window._process_env.VITE_BUILD_HOST}/api/v1/report/web`, 61 | add: { 62 | appId: "ZzdSi3P1XXXX2161087", 63 | }, 64 | }); 65 | ``` 66 | 67 | 这样就可以动态的引入各个环境的域名前缀,部署到各个环境啦。 68 | 69 | -------------------------------------------------------------------------------- /dump.rdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangweianger/zanePerfor/444fd3cc1aaa77abd6a2924a55955987d74a351e/dump.rdb -------------------------------------------------------------------------------- /lib/plugin/egg-email/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Alibaba Group Holding Limited and other contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/plugin/egg-email/README.md: -------------------------------------------------------------------------------- 1 | # egg-email 2 | 3 | -------------------------------------------------------------------------------- /lib/plugin/egg-email/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const email = require('./lib/email'); 4 | 5 | module.exports = app => { 6 | email(app); 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /lib/plugin/egg-email/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.email = { 4 | }; 5 | -------------------------------------------------------------------------------- /lib/plugin/egg-email/lib/email.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const nodemailer = require('nodemailer'); 4 | // const assert = require('assert'); 5 | 6 | module.exports = app => { 7 | app.addSingleton('email', createOneClient); 8 | }; 9 | 10 | function createOneClient(config, app) { 11 | const isHostProt = config.host && config.port && typeof (config.secure) === 'boolean'; 12 | const service = config.service; 13 | 14 | if (!(isHostProt || service) || !config.auth) return {}; 15 | 16 | // assert((isHostProt || service) && config.auth, '[egg-email] host and prot or service are require on config'); 17 | 18 | app.coreLogger.info('[egg-email] connecting success!'); 19 | 20 | const smtpTransport = nodemailer.createTransport(config); 21 | 22 | return smtpTransport; 23 | } 24 | -------------------------------------------------------------------------------- /lib/plugin/egg-email/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egg-email", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "nodemailer": "^5.0.0" 13 | }, 14 | "eggPlugin": { 15 | "name": "egg-email" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/plugin/egg-kafka/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Alibaba Group Holding Limited and other contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/plugin/egg-kafka/README.md: -------------------------------------------------------------------------------- 1 | # egg-kafka 2 | 3 | -------------------------------------------------------------------------------- /lib/plugin/egg-kafka/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const kafka = require('./lib/kafka'); 4 | 5 | module.exports = app => { 6 | if (app.config.report_data_type === 'kafka') kafka(app); 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /lib/plugin/egg-kafka/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.kafka = { 4 | client: { // kafkaClient 5 | kafkaHost: 'localhost:9092', 6 | }, 7 | producer: { 8 | web: { 9 | topic: 'zane_perfor_web', 10 | partition: 0, // default 0 11 | attributes: 0, // default: 0 12 | // timestamp: Date.now(), 13 | }, 14 | wx: { 15 | topic: 'zane_perfor_wx', 16 | }, 17 | }, 18 | consumer: { 19 | web: { 20 | topic: 'zane_perfor_web', 21 | offset: 0, // default 0 22 | partition: 0, // default 0 23 | isone: false, // 此参数默认不可更改 24 | }, 25 | wx: { 26 | topic: 'zane_perfor_wx', 27 | isone: false, 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /lib/plugin/egg-kafka/lib/kafka.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const kafka = require('kafka-node'); 5 | 6 | module.exports = app => { 7 | app.addSingleton('kafka', createClient); 8 | }; 9 | 10 | class Kafka { 11 | constructor(config, app) { 12 | this.config = config || {}; 13 | this.app = app; 14 | this.client = null; 15 | this.producer = null; 16 | this.init(); 17 | } 18 | 19 | init() { 20 | assert(this.config.kafkaHost, '[egg-kafka] kafkaHost is required on config'); 21 | this.client = new kafka.KafkaClient(this.config); 22 | this.producers(); 23 | } 24 | 25 | producers() { 26 | const Producer = kafka.Producer; 27 | this.producer = new Producer(this.client); 28 | this.producer.on('ready', () => { 29 | this.app.coreLogger.info('[egg-kafka] the producer is ready.'); 30 | }); 31 | this.producer.on('error', err => { 32 | this.app.coreLogger.error(`[egg-kafka] have error ${err}`); 33 | }); 34 | } 35 | 36 | consumer(type = 'web', fn) { 37 | assert(type, '[egg-kafka] consumers type argument must be required'); 38 | const kafkaConfig = this.app.config.kafka; 39 | const consumer = kafkaConfig.consumer[type] || {}; 40 | const consumers = Array.isArray(consumer) ? consumer : [ consumer ]; 41 | const Consumer = kafka.Consumer; 42 | const _consumer = new Consumer( 43 | this.client, 44 | consumers, 45 | { 46 | autoCommit: true, 47 | } 48 | ); 49 | _consumer.on('error', err => { 50 | this.app.coreLogger.error(`[egg-kafka] consumer have error ${err}`); 51 | }); 52 | _consumer.on('message', message => { 53 | fn && fn(message); 54 | }); 55 | } 56 | 57 | consumerGroup(type = 'web', fn) { 58 | assert(type, '[egg-kafka] consumers type argument must be required'); 59 | const kafkaConfig = this.app.config.kafka; 60 | const kafkaHost = kafkaConfig.client.kafkaHost; 61 | const consumerOption = kafkaConfig.consumerGroup[type] || {}; 62 | const topic = consumerOption.topic; 63 | consumerOption.kafkaHost = kafkaHost; 64 | const ConsumerGroup = kafka.ConsumerGroup; 65 | const _consumer = new ConsumerGroup(consumerOption, topic); 66 | _consumer.on('error', err => { 67 | this.app.coreLogger.error(`[egg-kafka] consumer have error ${err}`); 68 | }); 69 | _consumer.on('message', message => { 70 | fn && fn(message); 71 | }); 72 | } 73 | 74 | createTopics(topics) { 75 | assert(Array.isArray(topics), '[egg-kafka] createTopics opction must be Array.'); 76 | this.client.createTopics(topics, error => { 77 | if (error) this.app.coreLogger.error(`[egg-kafka] createTopics have error ${error}`); 78 | }); 79 | } 80 | 81 | send(type, data) { 82 | assert(type, '[egg-kafka] type is must required.'); 83 | if (!data) return; 84 | let producer = this.app.config.kafka.producer[type] || {}; 85 | let producers = []; 86 | if (typeof (data) === 'string') { 87 | producer.messages = data; 88 | producers = [ producer ]; 89 | } else if (Object.prototype.toString.call(data) === '[object Object]') { 90 | producer = Object.assign({}, producer, data); 91 | producers = [ producer ]; 92 | } else if (Object.prototype.toString.call(data) === '[object Array]') { 93 | for (let i = 0; i < data.length; i++) { 94 | data[i] = Object.assign({}, producer, data[i]); 95 | } 96 | producers = data; 97 | } 98 | this.producer.send(producers, (err, data) => { 99 | if (err) assert(err, '[egg-kafka] err. errmsg ${err}'); 100 | console.log(data); 101 | }); 102 | } 103 | } 104 | 105 | function createClient(config, app) { 106 | const kafka = new Kafka(config, app); 107 | kafka.init(); 108 | return kafka; 109 | } 110 | -------------------------------------------------------------------------------- /lib/plugin/egg-kafka/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egg-kafka", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "kafka-node": "^4.0.0" 13 | }, 14 | "eggPlugin": { 15 | "name": "egg-kafka" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /mongodb-restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | #本地 5 | /usr/local/mongodb/bin/mongod -f /usr/local/etc/mongod27017.conf 6 | 7 | /usr/local/redis/src/redis-server /usr/local/redis/redis.conf 8 | 9 | #/usr/src/mongodb/bin/mongod -f /data/mongodb/mongod27017.conf 10 | #/usr/src/mongodb/bin/mongod -f /data/mongodb/mongod27019.conf 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zane-performance", 3 | "version": "1.0.0", 4 | "description": "前端性能监控系统", 5 | "private": true, 6 | "dependencies": { 7 | "address": "^1.0.3", 8 | "egg": "^2.2.1", 9 | "egg-alinode": "^2.0.1", 10 | "egg-cors": "^2.1.0", 11 | "egg-mongoose": "^3.1.0", 12 | "egg-redis": "^2.4.0", 13 | "egg-router-plus": "^1.3.0", 14 | "egg-scripts": "^2.10.0", 15 | "egg-socket.io": "^4.1.4", 16 | "ldapjs": "^1.0.2", 17 | "nodemailer": "^6.9.1", 18 | "kafka-node": "^4.0.0", 19 | "md5": "^2.2.1", 20 | "ua-parser-js": "^0.7.18", 21 | "egg-view-ejs": "^2.0.0", 22 | "cron-parser": "^2.6.0" 23 | }, 24 | "devDependencies": { 25 | "autod": "^3.0.1", 26 | "autod-egg": "^1.0.0", 27 | "egg-bin": "^4.3.5", 28 | "egg-ci": "^1.8.0", 29 | "egg-email": "^1.0.2", 30 | "egg-mock": "^3.14.0", 31 | "eslint": "^4.11.0", 32 | "eslint-config-egg": "^6.0.0", 33 | "eslint-plugin-html": "^5.0.0", 34 | "webstorm-disable-index": "^1.2.0" 35 | }, 36 | "engines": { 37 | "node": ">=8.9.0" 38 | }, 39 | "scripts": { 40 | "start": "egg-scripts start --daemon --workers=2 --title=performance", 41 | "stop": "egg-scripts stop --title=performance", 42 | "dev": "egg-bin dev", 43 | "debug": "egg-bin debug", 44 | "test": "npm run lint -- --fix && npm run test-local", 45 | "test-local": "egg-bin test", 46 | "cov": "egg-bin cov", 47 | "lint": "eslint .", 48 | "ci": "npm run lint && npm run cov", 49 | "autod": "autod" 50 | }, 51 | "ci": { 52 | "version": "8" 53 | }, 54 | "repository": { 55 | "type": "git", 56 | "url": "https://github.com/wangweianger/zanePerfor" 57 | }, 58 | "author": "wangweianger", 59 | "license": "MIT" 60 | } -------------------------------------------------------------------------------- /servers-restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /data/performance 4 | npm stop 5 | ps -ef | grep performance | grep -v grep | grep -v PPID | awk '{ print $2}' | xargs kill -9 6 | # sleep 10s 7 | npm start 8 | result=$? 9 | 10 | while [ $result -ne 0 ] 11 | do 12 | npm start 13 | result=$? 14 | if [ $result -eq 0 ] 15 | then 16 | echo "执行成功退出循环" 17 | else 18 | echo "执行失败重新执行" 19 | fi 20 | done 21 | 22 | -------------------------------------------------------------------------------- /start-docker-compose.sh: -------------------------------------------------------------------------------- 1 | export hostIP='自己的外网IP' 2 | docker-compose up -d --build -------------------------------------------------------------------------------- /test/app/controller/home.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { app, assert } = require('egg-mock/bootstrap'); 4 | 5 | describe('test/app/controller/home.test.js', () => { 6 | 7 | it('should assert', function* () { 8 | const pkg = require('../../../package.json'); 9 | assert(app.config.keys.startsWith(pkg.name)); 10 | 11 | // const ctx = app.mockContext({}); 12 | // yield ctx.service.xx(); 13 | }); 14 | 15 | it('should GET /', () => { 16 | return app.httpRequest() 17 | .get('/') 18 | .expect('hi, egg') 19 | .expect(200); 20 | }); 21 | }); 22 | --------------------------------------------------------------------------------