bobkent 2019-09-07
数据埋点是监控用户在应用中的表现行为,对于TO C的产品迭代来说越来越重要。
数据埋点是产品需求分析的来源,检验功能是否达到预期。前端是更贴近用户,我来说说数据埋点在系统开发中的方案。
不同的产品对于数据的关注的角度不同,根据需求来采集和设计不同的方案。比如信息流的产品抖音,关注用户的停留时间更高。比如商品类的注重的是“复购率”,统计新老用户。
数据埋点统计通常分为:
(1)页面访问量统计
(2)功能点击量统计
我们今天只讨论页面访问量统计,在开发系统中自己定义页面访问量相关数据的统计。
通常我们接入的是第三方的服务,比如百度统计,就有相关页面访问统计,以及用户的画像等等。但是作为开发人员来说,如果在自己做的系统中,按照自己的需求定制化数据埋点是不是很cool。
1、页面访问量相关统计
页面访问量通常分为:PV和UV。
(1)PV:页面访问人次。
(2)UV:页面访问人数。
页面访问量,并非仅仅是内容吸引力决定的,影响因素有:入口,页面位置,到主页面深度等等。入口主要是UI视觉相关人员设计。入口位置可以通过数据分析后进行调整,到主页面深度可以分析用户的访问路径进行调整。
采集页面加载 from、to 以获知用户访问路径:
分析可以知道用户普遍访问深度,每一层和每一个页面的流失率可以很直观看出来,从而调整核心页面入口源和深度。
还有一些特殊情况出现:比如PV稳定的页面访问量突然暴跌,可能是加载失败或者报错。
接下来我通过自己的系统接入数据埋点:https://chat.chengxinsong.cn
技术架构:vue+vuex+koa2+mysql+websocketIO+redis等。
在main.js中全局定义
/*全局PV统计*/ router.beforeEach((to, from, next) => { let flag = localStorage.getItem("HappyChatUserInfo") !== null ? true: false; let data = { type: 'visit', user_id: flag ? JSON.parse(localStorage.getItem("HappyChatUserInfo")).user_id: '获取不到userId', time: (new Date()).getTime(), params: { from: { name: from.name || '', path: from.path || '', query: from.query || '' }, to: { name: to.name || '', path: to.path || '', query: to.query || '' } } } App.methods.logEvent(data); next() })
停留时间可以通过from和to页面的时间(跳转页time - 当前页time)。关闭应用的时候如何统计?可以考虑window.onunload方法
window.onunload = function unloadPage() { let data = { type: 'close', user_id: localStorage.getItem("HappyChatUserInfo") !== null ? JSON.parse(localStorage.getItem("HappyChatUserInfo")).user_id: '获取不到userId', time: (new Date()).getTime(), params: { from: { name: '关闭前', path: router.currentRoute.path || '', query: router.currentRoute.params || '' }, to: { name: '关闭', path: '', query: '' } } } App.methods.logEvent(data); }
这时候我们需要去定义接口传参,接口方法是logEvent。
我们自定义Vue插件App的method,用于埋点数据向服务器发送。
我们在App.vue中定义
具体方法
/* 数据埋点 */ logEvent(opts) { let data = { type: opts.type, user_id: opts.user_id, time: opts.time, params: opts.params || {} } return Api.pvLog(data).then(res => { console.log(res) }).catch(function (error) { console.log(error); }); }
其中Api是axios的接口统一封装的方法。
数据到了后端怎么保存,保存哪些参数,根据需求来定义,比如常见的:客户端IP,数据类型type,用户id,访问时间,from中的页面名字name,路径path,查询茶树query等等。
后端接口的控制层。(接口需不需要验证,根据需求来设计。)
let pvLog = async (ctx, next) => { const data = ctx.request.body; let req = ctx.req; let clientIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.socket.remoteAddress || req.connection.socket.remoteAddress; userModel.logPV([clientIP, data.type, data.user_id, data.time, data.params.from.name || '', data.params.from.path || '', JSON.stringify(data.params.from.query) || '', data.params.to.name || '', data.params.to.path || '', JSON.stringify(data.params.to.query) || '']); ctx.body = { success: true } };
接下来就是数据入库操作,代码就不放了,源码地址:
1、后端代码:https://github.com/saucxs/hap...
2、前端代码:https://github.com/saucxs/hap...
欢迎fork和start。
虽然官方说,慎用全局混入对象。
放一下示例代码
import Vue from 'vue' Vue.mixin({ beforeRouteEnter (to, from, next) { next(vm => { vm.$app.logEvent({ type: 'visit', name: to.name, time: new Date().valueOf(), params: { from: { name: from.name, path: from.path, query: from.query }, to: { name: to.name, path: to.path, query: to.query } } }) }) }, beforeRouteLeave (to, from, next) { this.$app.logEvent({ type: 'visit', name: to.name, time: new Date().valueOf(), params: { from: { name: from.name, path: from.path, query: from.query }, to: { name: to.name, path: to.path, query: to.query } } }) next() } })
我们需要考虑是否在应用关闭的时候触发beforeRouteLeave方法?
还有两个问题:
(1)每一个页面都有钩子函数beforeRouteEnter,beforeRouteLeave方法,如何进行合并。
(2)有时候涉及到子路由问题,子路由的页面会调用2次beforeRouteEnter和beforeRouteLeave方法,PV会翻倍。
所以觉得方案二仅供参考,慎用。
其中,this.$app.logEvent(vm.$app.logEvent)等同于方案一的App.logEvent。
根据实际需求和评估使用不同的方案:
(1)SPA应用,单入口,在入口文件全局定义Router.beforeEach就可以,就是方案1。
(2)MPA应用,多入口,可以封装公用的逻辑,免去重复构造entry成本。
(3)SPA+MPA混合应用:采用MPA方案就行。
(4)SSR应用:追求SEO采用服务端渲染(SSR),采用不同的模板渲染页面,直接统计调用模板的次数就可以知道PV相关数据。
至于 UV,统计 PV 时采集 userId 去重即可。若应用无用户管理体系,采集 IP、deviceId 也可粗略得知 UV(不精准)。
作者简介
昵称:saucxs | songEagle | 松宝写代码
github:https://github.com/saucxs
一、技术产品
二、开源作品:
background-color: blue;background-color: yellow;<input type="button" value="变蓝" @click="changeColorT