前期做了埋点后可进行丰富的可视化实现,用来监控日常的运营情况。网上所谓的自动化埋点也并非全自动化的,而且要引入一套第三方的代友,你的用户数据都被发送到了第三方的服务器,非常的不放心,所以自建了一套埋点系统,数据库采用MySQL关系统型数据库,前端VUE+Elements UI,后台采用Spring Boot+MyBatis实现,并非针对Web应用封装了埋点的API,供别人调用,之所以没有采用自动化的埋点方式,是因为不想保存太多乱七八糟的数据,影响分析。
步骤 | 标题 |
---|---|
1 | 指标设计 |
2 | 数据库设计 |
3 | Java后台开发 |
4 | Vue+Webpack前端开发 |
5 | 客户端埋点API开发 |
首先进行的是指标设计,就是将来要出什么样的统计分析图。常见的图表有下面这些。
1、APP平均访问量对比
所有应用平均访问量对比(周期:累计、近三月,近一月,近一周,当天)———柱状图
说明:因为每个应用对应的用户数是不同的,统计绝对的PV值做对比没有多大的意义,所以定义平均访问量=总访问量/用户人数
1、PV:
A、某应用PV趋势 (按周、按天)———折线图
B、某应用中各页面访问量对比(周期:近一月、近一周)———柱状图
C、某应用中各页面访问量趋势(按周、按天)
2、、UV访问量趋势
某用户访问了一次应用,计数+1,同一天重复访问此应用,仍记为访问1次。(按天,按周,折线趋势图)
3、用户存留率
留存率=新增用户中登录用户数/新增用户数*100%,用户留存率对于互联网应用很重要,但是对于BI系统来说,用户不需要注册(全员可用),所以不存在新增用户一说,但是用户流失这种现象依然是存在的,比如某应用上线后用户逐渐不再使用了,我们就要及时采取措施,为此可把用户留存率指标进行改造,以某个周期(如周)的开始为起点,记录此时访问的用户,统计这些用户后续时间的访问流失情况,这样也是有意义的。
A、按周:以周(7天)为单位,假设第一天有100人访问,第二天这100中只有80人访问,则第二天的留存率为80%,第三天这80人中只有20再次访问,则留存率降为20%,以此类推。
B、按月:上月新增用户数在本月访问了应用的个数 / 上个月新增用户数。改造为:上个月访问的用户数在本月依然访问的用户数 / 上个月访问的用户数
4、单应用用户活跃度
A、日活跃用户数(DAU-Daily Activated Users):某APP日活跃用户数 (此处等于日UV)
B、月活跃用户数(MAU-Month Activated Users):某APP月活跃用户数
C、用户活跃度(UAUser activity)=本周期内使用APP的用户数/本APP用户总量
周期:天、月、年
各APP日活、周活、月活度对比;(折线图)
5、页面停留时长对比
A、针对某个APP,用户停留时长的页面TOP 5 (按周、按月),可针对某个页面出趋势图
B、可对所有APP统计用户合计时长/用户数(平均用户访问时长)做对比,分析哪个APP用户粘性更大
6、跳出率分析
A、所有APP跳出率对比:用户进入应用就退出的量/总访问量 (按周、按月)
B、某应用中各页面的跳出率:用户进入此页面就退出的量/此页面的总访问量 (按周、按月)
7、页面用户操作统计
某页面中用户操作Top 5 (按周、按月)
8、单用户分析(针对某应用)
A、某用户活跃性分析(按天、按周、按月,此用户PV趋势折线图)
B、某用户关注点分析(用户常用操作,常看页面,停留时间最长的几个页面)(周期:累计)
9、用户设备统计
A、统计出用户手机机型持有情况(帕累托图:80%访问量的机型)
B、统计IOS和android机型占比
10、各分公司使用情况分析
A、针对某APP,分析33家分公司是否有用户覆盖,各分公司用户量(柱状图),对无用户覆盖的分公司重点关注
B、某分公司使用(对某APP的访问量)趋势图(按天、按周、按月),对访问量整体偏小或下降趋势的分公司重点关注
11、消息推送转化率
A、转化率=点击数/推送数 。实现方式:在推送时增加URL参数ATN,埋点load时带上ATN参数,记录在页面加载表里,每推送一条消息就在消息推送日志表中记录下此APP的消息,(统计页面加载表中带ATN参数的记录(去重-同用户同ATN值多次访问记为一次)/消息推送日志表中的记录数)就得到转化率了
假如你有很多个项目或应用,配置表保存各个应用,名称,所在平台,只有在配置表里的应用才会被记录访问数据,将来也可以统计不同应用的访问情况。
页面访问日志记录什么人在什么时间通过什么设备访问了哪个页面,并且停留了多长时间。也就是说用户访问过哪些页面。
页面操作日志表记录用户在什么时间在页面中做了什么操作,比如点击了一个按钮,或者设置了什么条件等,这种数据是对用户行为和路径进行分析的基础。
使用Spring Boot搭建项目,全注解的方式,除了MyBatis的xml文件,没有多余的配置文件,通过IDea配置项目环境,设置启动器本地调试,前后端分离,Java层只负责Rest接口的输出。
使用Maven进行打包,把构建好的项目部署到Tomcat下,前端构建的代码发到tomcat里,就可以运行了,不存在跨域问题。但在开发阶段,因为Java和Vue项目是分开启动的,端口不一样,存在跨域,需要配置前端的代理访问。
这样前端就不存在跨域的问题了,当然代理也可以通过Nginx做,如果发布时Java项目和前端项目不在一起,就比较适合用Nginx做代理,开发时用webpack的代理就行了。
为了减小项目压力,采用部分引入的方式,用不到的组件不引入。
import Vue from 'vue'
import App from './App'
import router from './router'
// import 'bootstrap/dist/js/bootstrap.js'
import 'font-awesome/css/font-awesome.min.css'
import 'bootstrap/dist/css/bootstrap.min.css'
import axios from 'axios'
Vue.prototype.$http = axios
import echarts from 'echarts'
Vue.prototype.$echarts = echarts //引入组件
import { DatePicker, TimePicker,Select,OptionGroup, Option,Menu, Submenu, MenuItem, MenuItemGroup,Switch,Dialog} from 'element-ui'; Vue.use(DatePicker)
Vue.use(TimePicker)
Vue.use(Menu)
Vue.use(Submenu)
Vue.use(MenuItem)
Vue.use(MenuItemGroup)
Vue.use(TimePicker)
Vue.use(Switch)
Vue.use(Dialog)
Vue.use(Select)
Vue.use(OptionGroup)
Vue.use(Option)
Vue.config.productionTip = false
Date.prototype.toLocaleString=function(b){
var m=this.getMonth()+1;
if(m<10){m="0"+m;}
if(b){
return this.getFullYear()+"-"+m+"-"+this.getDate();
}
return this.getFullYear()+"-"+m+"-"+this.getDate()+" "+this.getHours()+":"+this.getMinutes()+":"+this.getSeconds();
}
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: ' '
})
在App.vue中处理一些路由相关的信息
>
>
-view/>
>
>
新建Home.vue用来做目录:
<template>
<div class="home">
<div class="banner">
<img class="logo" src="./assets/logo.png">
元数据管理系统
<div class="pull-right banner_tools">
{{user.user_name}} <a href="javascript:void(0)" @click="logout()" class="fa fa-sign-out" title="退出"></a>
<a href="/help" title="帮助" target="_blank"><i class="fa fa-question-circle-o"></i></a>
<a href="/#/users/" title="用户管理" class="users"><i class="fa fa-users"></i></a>
</div>
</div>
<div class="south">
<div class="west">
<el-menu
:default-active="activeIndex"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose">
<el-submenu index="7">
<template slot="title">
<i class="fa fa-history blue"></i> 埋点管理
</template>
<el-menu-item-group>
<template slot="title">日志</template>
<el-menu-item index="7-1">
<a href="/#/bury/pagevisits"><i class="fa fa-history"></i> 页面访问日志</a>
</el-menu-item>
<el-menu-item index="7-2">
<a href="/#/bury/useroplogs"><i class="fa fa-history"></i> 用户操作日志</a>
</el-menu-item>
</el-menu-item-group>
<el-menu-item-group>
<template slot="title">报表</template>
<el-menu-item index="7-3">
<a href="/#/bury/appvisits"><i class="fa fa-bar-chart"></i> APP访问量</a>
</el-menu-item>
<el-menu-item index="7-4">
<a href="/#/bury/pvuv"><i class="fa fa-bar-chart"></i> PV-UV趋势</a>
</el-menu-item>
<el-menu-item index="7-6">
<a href="/#/bury/retention"><i class="fa fa-industry"></i> 用户存留率</a>
</el-menu-item>
<el-menu-item index="7-7">
<a href="/#/bury/activity"><i class="fa fa-bar-chart"></i> 用户活跃度</a>
</el-menu-item>
<el-menu-item index="7-8">
<a href="/#/bury/pagestats"><i class="fa fa-bar-chart"></i> 页面分析</a>
</el-menu-item>
<!-- <el-menu-item index="7-11">
<a href=""><i class="fa fa-bar-chart"></i> 单用户行为分析</a>
</el-menu-item>
<el-menu-item index="7-12">
<a href=""><i class="fa fa-bar-chart"></i> 用户设备统计</a>
</el-menu-item>
<el-menu-item index="7-13">
<a href=""><i class="fa fa-bar-chart"></i> 各分公司使用情况</a>
</el-menu-item>
<el-menu-item index="7-14">
<a href=""><i class="fa fa-bar-chart"></i> 消息推送转化率</a>
</el-menu-item> -->
</el-menu-item-group>
</el-submenu>
<el-menu-item index="8">
<a href="/#/analysis/explore"><i class="fa fa-shield purple"></i> 数据探索</a>
</el-menu-item>
</el-menu>
</div>
<div class="east">
<router-view/>
</div>
</div>
</div>
</template>
<template>
<div class="criterions">
<div class="bread">
<ul><li>元数据管理</li><li>埋点</li><li>页面访问日志</li></ul>
<div class="btn-tools">
应用:
<el-select v-model="app" placeholder="请选择APP" size="mini">
<el-option-group v-for="group in apps" :key="group.groupId" :label="group.groupName">
<el-option v-for="item in group.apps" :key="item.appId"
:label="item.appName" :value="item.appId">
</el-option>
</el-option-group>
</el-select>
页面:<input type="text" class="bread-input" v-model="pageId">
用户:<input type="text" class="bread-sm-input" v-model="userId">
<el-date-picker
v-model="sedate"
type="daterange"
range-separator="至"
size="small"
class="el-range-editor-bread"
:editable="false"
start-placeholder="开始日期"
end-placeholder="结束日期">
</el-date-picker>
<a href="javascript:void(0);" class="btn btn-sm btn-primary" @click="doSearch"><i class="fa fa-search"></i> 查询</a>
</div>
</div>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th class="text-left">page_ID</th>
<th>页面名称</th>
<th class="text-left">app_ID</th>
<th>应用名称</th>
<th class="text-left">platform_ID</th>
<th>系统名称</th>
<th>访问时间</th>
<th>用户</th>
<th>停留时长</th>
<th>客户端</th>
</tr>
</thead>
<tbody>
<tr v-for="row in logs">
<td>{{row.pageId}}</td>
<td>{{row.pageName}}</td>
<td>{{row.appId}}</td>
<td>{{row.appName}}</td>
<td>{{row.platformId}}</td>
<td>{{row.platformName}}</td>
<td>{{new Date(row.createTime).toLocaleString()}}</td>
<td class="text-center">{{row.userId}}</td>
<td class="text-center">{{row.stayTime}}</td>
<td>{{formatter(row.userAgent)}}</td>
</tr>
</tbody>
</table>
<template>
<v-Pagination :total="total" :current-page='current' @pagechange="pagechange"></v-Pagination>
</template>
</div>
</template>
<script>
import Pagination from '@/components/Pagination'
export default {
data () {
return {
logs: [],
app:null,
apps:[],
sedate:[],
pageId:null,
userId:null,
total: 0, // 记录总条数
current: 1 // 当前的页数
}
},
mounted(){
this.getAPPs();
this.fetchData();
},
components: {
'v-Pagination': Pagination,
},
methods:{
getAPPs(){
var self=this;
return this.$http.get('/api/buryData/getapps')
.then(function (response) {
if(response.data.rstCode==200){
var groups=[];var list=response.data.data;
for(var i=0;i<list.length;i++){
if(groups.some(item=>item.groupId==list[i].platformId)){
//添加进去
for(var k=0;k<groups.length;k++){
if(groups[k].groupId==list[i].platformId){
groups[k].apps.push(list[i]);//加到第组里
}
}
}else{
groups.push({groupId:list[i].platformId, groupName:list[i].platformName, apps:[list[i]] });
}
}
self.apps=groups;
}else{
console.error(response.data);
}
}).catch(function (error) {console.log(error); });
},
fetchData: function (page=1) {
var self = this
var params={page:page,rows:20};
if(self.userId){
params.userId=self.userId;
}
if(self.pageId){
params.pageId=self.pageId;
}
if(self.app){
params.appId=self.app;
}
if(self.sedate && self.sedate.length==2){
params.sDate=self.sedate[0].getTime();
params.eDate=self.sedate[1].getTime();
}
return this.$http.get('/api/buryData/pagelogs',{params:params})
.then(function (response) {
if(response.data.rstCode==200){
self.logs=response.data.data.list;
self.total=response.data.data.total;
}else{
console.error(response.data);
}
}).catch(function (error) {
console.log(error);
});
},
pagechange:function(page){
this.fetchData(page);
},
doSearch:function(){
console.log(this.sedate);
this.fetchData(1);
},
formatter:function(UA){
var ua = UA.toLowerCase();
var Sys = {};
var s;
(s = ua.match(/msie ([\d.]+)/)) ? Sys.ie = "IE:"+s[1] : 0;
(s = ua.match(/wow64; trident\/([\d.]+)/)) ? Sys.Edge = "Edge Trident:"+s[1] : 0;
(s = ua.match(/firefox\/([\d.]+)/)) ? Sys.firefox = "Firefox:"+s[1] : 0;
(s = ua.match(/chrome\/([\d.]+)/)) ? Sys.chrome = "Chrome:"+s[1] : 0;
(s = ua.match(/opera.([\d.]+)/)) ? Sys.opera = "Opera:"+s[1] : 0;
(s = ua.match(/version\/([\d.]+).*safari/)) ? Sys.safari = "Safari:"+s[1] : 0;
(s = ua.match(/iphone os\s([\d+_]+).*mobile/)) ? Sys.iPhone = "iPhone OS:"+s[1] : 0;
(s = ua.match(/android\s([\d+.]+).*;(.*)\sbuild\/.*mobile/)) ? Sys.Android = "Android:"+s[1]+" "+(s[2]?s[2].toUpperCase():"") : 0;
return Sys.ie ||Sys.Edge||Sys.iPhone ||Sys.Android|| Sys.firefox ||Sys.chrome ||Sys.opera ||Sys.safari || UA;
}
}
}
</script>
<style scoped>
.bread .btn-tools{flex:8;}
</style>
基中v-Pagination是自己开发的一个基于Bootstrap分页组件,因为ElementUI的分页组件太不好用了。实际上它的表格组件也不好用,我也自己做了一个,功能很强大,只是此处表格太简单,没有引入。
完成的效果截图(部分):
客户端项目想要埋点总不能都去自己写调接口的方法呀,所以在客户端我做了两套API,一个是纯JS版,不依赖任何组件库,另一个是VUE,为了篇幅简化,这里给出VUE版的部分代码供参考。
思路是:在页面加载时调用load接口,记录这次访问的信息,包括:页面、用户、终端、时间等,load接口会返回一个ID,把这个ID保存在前端,当用户退出此页面时,再调用一个stay接口,记录用户在此页面停留了多长时间,用来做跳出率分析。这里的难点在于,用户退出的事件捕获是很难的,因为用户可能是正常的关闭浏览器的标签页或浏览器窗口,也可能直接杀浏览器的进程,甚至直接关机走人,这时候你根本没机会调用stay接口,何况在Android和ios设备中更复杂,杀进程也更频繁,所以为了更可靠的记录用户在页面的停留时间,不得已采用定时器timer来做,每隔一秒调下stay接口,当然,更优的方法可以采用websocket方式,这个要Java端配置做相关的服务,而且要做心跳检测,自动重联,本项目暂时不这么搞。
至于用户行为,做个track方法记录一下就行了,客户端只用引入组件并调用相应的方法就可以了,对于VUE项目,不用在每个调用的VIEW里都去引入webbury组件,只用在路由处理的地方记录就可以了,在路由切换时可以根据load接口返回的id,记录下from页面的停留时间,这样一来timer甚至可以不用,但是尽量还是要用timer,以防记录不到停留时间的情况。
下面是埋点API的代码:
function param(obj) {
var query = '';
var name, value, fullSubName, subName, subValue, innerObj, i;
for (name in obj) {
value = obj[name];
if (value instanceof Array) {
for (i = 0; i < value.length; ++i) {
subValue = value[i];
fullSubName = name + '[' + i + ']';
innerObj = {};
innerObj[fullSubName] = subValue;
query += param(innerObj) + '&';
}
} else if (value instanceof Object) {
for (subName in value) {
subValue = value[subName];
fullSubName = name + '[' + subName + ']';
innerObj = {};
innerObj[fullSubName] = subValue;
query += param(innerObj) + '&';
}
} else if (value !== undefined && value !== null) {
query += encodeURIComponent(name) + '='
+ encodeURIComponent(value) + '&';
}
}
return query.length ? query.substr(0, query.length - 1) : query;
}
function _extend(src,target){
for(var k in target){
src[k]=target[k];
}
return src;
}
function WebBury(config={}){
var store={};//记录每个页面的loadId和loadTime
// var loadId=0;
// var TSTART=0;
var DEFO=_extend({
platformId:'BI',
userAgent:window.navigator.userAgent,
},config);
return {
auth(userId){
DEFO.userId=userId;
},
load(appId,pageId,pageName,mId){
//加载一个Page
var p=_extend(DEFO,{appId,pageId,pageName,mId:mId});
if(p.userId==null){console.error('userId is not ready for bury.');return;}
fetch(url+"PL?"+param(p)).then(res=>{
res.json().then(ret=>{
if(ret.data && ret.data.length>0){
store[pageId]={loadId:ret.data[0].id,loadTime:Date.now()};
}else{
console.error("fail on web bury.")
}
});
// {"data":[{"id":"f84d39f26987403b809b08fe3501aaec"}],"status":true}
});
},
track(appId,pageId,operation){
//记录一个操作
fetch(url+"UO?"+param(_extend(DEFO,{appId,pageId,operation}))).then(res=>{
res.json().then(ret=>{
// if(ret && ret.ret){}else{console.log(ret);}
});
});
},
stay(appId,pageId){
//找到store中的记录
if(store[pageId]){
var diff =parseInt((Date.now()-store[pageId].loadTime)/1000);
fetch(url+"UST?"+param(_extend(DEFO,{appId,pageId,id:store[pageId].loadId,stayTime:diff}))).then(res=>{
res.json().then(ret=>{
if(ret && ret.ret){}else{console.log(ret);}
});
});
}
}
}
}
export default WebBury;
以上项目从设计,到数据库,Java,再到前端一气呵成,所用技术也都是主流,项目不难,但是要做好也挺麻烦,编辑器的配置、项目的配置,每一项都需要花时间,不过得益于SpringBoot的强大能力和Vue组件代开发的魅力,调试起来却很轻松。