埋点自动收集方案-概述
引言
埋点自动收集方案,由于涉及内容较多,考虑到篇幅原因,将分为三篇文章分别阐述
《埋点自动收集方案-概述》
《埋点自动收集方案-路由依赖分析》
《埋点自动收集方案-埋点提取》
其余两篇会在随后相继发布
本篇文章篇幅较长,对最终效果好奇的同学,可以优先查看“后台预览”部分的截图
痛点
埋点,作为跟踪业务上线效果的核心手段
其说明文档也是重中之重,但在很多公司或团队都在以最原始的方式维护
有的团队即便有统一的埋点平台,但整体运营成本也较高
1)维护成本
每次提需求,pm需要花费大量时间维护埋点文档
新增埋点还好,直接加上就可以了。
如果有涉及老埋点删除或修改,还要先找开发同学确认,然后再更新文档
如果怕麻烦,文档只增不删。。时间长了,你懂的~
2)到底谁来维护埋点文档
前面提到,新增还好,主要是删除或者更新埋点,谁来推动埋点文档更新?
开发同学推动pm?
pm找开发要埋点更新list?
开发直接更新埋点文档?
开发同学在开发同时还要记录埋点更新list?
开发和pm会不会相互扯皮?
3)无阻断性流程
整个流程,单靠一方角色确实难以达成目标
最终无论谁来维护,但凡没有阻断性流程,就变成全靠自觉了。
靠自觉,那要是能持久,就见鬼了~
4)埋点文档不准确
由于前面的原因,也就造成了埋点文档和实际上报代码越来越远
时间长了自然没法看,尤其碰到人员变动或者业务调整
新同学接手后,脑门直接三道黑线。。
5)开发同学抵触情绪强
PM:“帮我查下xx项目的埋点吧,我急用”
PM:“之前做的xx活动的埋点能帮我查一下么”
PM:“上次需求迭代,埋点更新list给我一份”
...
尤其赶上倒排期的项目。。
作为FE的你,是否有同感。。
引发的思考
无论怎样,线上数据通过埋点反馈,而且业务调整的依据就是数据
这是客观事实。
所以埋点文档这个痛点必须解决。
可能有同学会问,为什么不用第三方的埋点方案(比如:growingIO、神策等)
因为,无论采用哪种方案,目前主流上报埋点的方式就两种:
全埋点上报(含区域上报)
sdk主动上报(单埋点、多买点上报)
全埋点上报需要pm根据页面结构进行配置(主要维护方在pm),一旦页面结构变更,需要重新配置。
另外公司的现状就是有自己的数据平台,但如果采用全埋点上报,在数据分析层面会有非常大的负担,而且各业务线本身采用的是代码主动上报方式。
sdk主动上报,就会遇到前面提到的问题。
可能还有同学说:“这本来就是pm该干的事,我只负责我这部分就好。”
这本身也没毛病,但我们本质上还是想提升整体团队的效率
在做这个方案的时候,我们也和很多团队的pm沟通过,维护一个可用的埋点文档确实会消耗相当大的精力
这也让我更加坚定了决心!
方案思路
前面所有的问题,核心就两个:
没有统一埋点平台
没有科学可用的埋点协作模式
经过组内多次讨论,又将问题细分,
以及对应解决方案如下:
问题 | 解决方案 |
---|---|
维护角色模糊 | pm只提供需要统计的埋点,更新由代码层面自动维护 |
维护成本高 | 通过jsdoc代码注释,利用代码在编译时,从文件中抽离埋点及解释 |
文档可用性 | 埋点抽离完成,直接上报埋点后台 |
老项目接入 | 提供统一自动注释添加工具 |
新项目接入 | 将该方案集成到脚手架中,同时提供检查loader,硬性检查完整性 |
业务提效 | 建立埋点后台,提供埋点及数据预览 |
看表格可能还是一脸懵,我们先看看整体思路图
解释一下流程:
pm编写埋点文档(仅开发用)
fe依据文档编写上报逻辑
代码在上线时要执行build, 此时触发webpack 埋点抽离插件
埋点抽离插件按页面,收集其所有埋点
收集完后,上报埋点服务
pm可以通过埋点平台查看埋点及对应的数据了
看流程很清晰,但肯定会有一些问题,那么我们分别解释下:
问题1:如何收集埋点
我们以vue项目为例,在Vue.prototype下挂了公共上报埋点方法:
Vue.prototype.$log = function (actionType, backup = null) {...}
actionType 必选,行为埋点参数
backup 可选,埋点的附加参数
如上报某个页面pv,附加参数为渠道号channel,即:
this.$log('PAGE_SHOW',{channel:'xxx'})
解决方案:利用自定义jsdoc插件和自定义标签收集注释
/**
* @log 页面展现
* @backup channel: 渠道号
*/
this.$log('PAGE_SHOW',{channel:'xxx'})
其中,@log和@backup 是jsdoc的自定义标签,让jsdoc只监控$log方法也是需要明确指定的
所以,为了让整个插件行为保持一致,需要引入一个公共的配置文件 js_doc.conf.js
js_doc.conf.js
module.exports = {
// 让jsdoc识别 @log
tag: 'log',
// 告知插件this.$log是发送埋点的方法,定义成数组是因为有的项目可能存在多个
method: ['$log'],
// pageType前缀,即会给项目pageType加前缀来区分不同项目
prefix: 'H5BOOK',
// pageType具体生成规则,不同项目可能规则不同
pageTypeGen: function({ prefix, routeName, dir, path, fileName }) {
return (prefix + routeName).toUpperCase();
},
// 公共的backup说明,最终单个埋点backup由公共的和私有的拼接而成
backup: 'uid:用户id',
// 项目信息,埋点上报用
projectInfo: {
projectId: '工程id,一般用项目名称',
projectName: '中文名称',
projectDesc: '项目描述',
projectIsShowOldMark: false // 是否展示老数据
}
}
该配置文件,是js_doc插件,连同后文提到的文件依赖分析插件、自动添加埋点注释工具所共用的
具体js_doc插件的实现方式 ,在随后的《埋点自动收集方案-埋点提取》中会专门讲解,本文主要讲解整体方案原理
通过这一步,就可以顺利收集到每个行为埋点,以及相应的注释和参数说明了,即actionType和对应的注释
当然,确定一个埋点,需要有2个核心因素
actionType:行为埋点
pageType:页面标识(有的团队叫pageId),用来定位具体是哪个页面
pageType通常是在页面运行时以当前页面路由为参照得来的,那问题来了
问题2:如何在编译时获取pageType
而且还有一种常见情况,公共组件,如下图
A组件被多个页面引用
或者A组件被二级组件B引用,然后又被直接或间接引入页面。
那么组件A中的埋点,肯定要被收集到对应的页面1和页面2当中。
这怎么办?
这问题等于就变成了:
如何确定页面路由(即获取pageType)
如何分析文件依赖(即收集公共组件中的埋点)
解决方案:
通过编写文件依赖分析插件,分析每个文件的相互引用关系
利用ast语法分析路由文件,拿到所有页面路由,即拿到页面的根组件
利用webpack的编译分析出整个项目的文件依赖关系,最终,你拿到每个页面引用了哪些组件
该部分具体实现方案会在 《埋点自动收集方案-路由依赖分析》中会专门阐述
样例:
// 附近的人
export default {
routes: [
// 首页
{
path: '/page1',
name: 'page1',
meta: {
title: '转转活动页',
desc: 'xx首页'
},
component: () => import('../views/page1/index.vue')
},
...
]
}
当然实际情况会很复杂:
路由入口文件包含多个子路由文件。
路由引入组件的方式有多种(静态引入、动态引入)
引入组件的书写方式多样
...
写法复杂的情况,通过ast语法分析,都可以进行兼容,不过确实需要耐心~
到此,核心信息收集完成。
前面jsdoc部分:拿到了每个文件所有的actionType及对应的注释
而这一步又拿到了:项目所有的页面路由引用及对应的组件文件依赖关系
对于页面说明的取值方式:meta.desc || meta.title || name || path
因为页面的描述和真实title不一定相同,所以单独加了个desc字段
想一下,拿到这些是不是可以以页面为单位,组织行为埋点数据了!
在接入过程中,我们还遇到了一个问题,就是
各业务pageType生成规则不统一
考虑到这个问题,我们在配置文件中定义了下面内容:
module.exports = {
...
// pageType前缀,即会给项目pageType加前缀来区分不同项目
prefix: 'H5BOOK',
// pageType具体生成规则,不同项目可能规则不同
pageTypeGen: function({ prefix, routeName, dir, path, fileName }) {
return (prefix + routeName).toUpperCase();
},
...
}
prefix:作为项目个性化前缀(主要用于区分不同项目,因为不同项目可能会定义同样的路由)
pageTypeGen:该方法用于业务自己定义生成pageType的规则,我们把prefix(项目前缀),routeName(路由名称),dir(文件目录),path(路由),fileName(文件名)通通返回给用户,有了这些参数,基本上已经可以覆盖全部的路由生成方式了。
OK,最终数据结构形式:
{
// 项目所有页面
pageList: [
// 单个页面
{
// 该页面的路由信息
routeInfo: {
routeName: 'page1',
description: 'xx首页'
},
// 该页面对应所有的行为埋点
actionList: [
{
actionType: 'PAGE_SHOW',
pageType: 'H5BOOK_page1',
backup: 'channel: 渠道号',
description: '页面展现'
},
...
]
},
...
],
// 项目信息
projectInfo: {
projectId: '项目id',
projectName: '项目名称',
projectDesc: '项目描述信息',
projectMark: '项目标记',
projectLogsMark: '埋点标记',
projectType: '项目类型',
projectIsShowOldMark: false, // 是否展示老的埋点数据,默认false
projectDefBackup: '默认参数说明'
}
}
埋点收集完成,
再加入项目相关信息, 然后通过接口批量上报到埋点后台保存。
埋点后台
我们用eggjs + mongoose搭建的后台服务
用react + antd + bizChart(图表库)搭建的后台系统
后台预览
显示所有自动埋点收集项目列表
上面是方案的核心部分,但毕竟整个方案解题思路是技术驱动
所以我们肯定还要关心:如何让整个方案可持续性的运转起来?
问题3:如何保证埋点及时更新?
这个问题就比较简单了,既然做成了webpack插件,那么
解决方案:更新build命令,在上线时执行收集和上报插件
埋点提取插件,在webpack编译后,收集完埋点自动上报。
保证每次上线时都对埋点文档进行更新。让文档和代码实时同步
问题4:老项目如何快速接入埋点方案?
老项目接入埋点是每个开发最头疼的事情,尤其补埋点注释
为此,我们也提出了一套便捷方案
解决方案:通过命令行工具自动补齐注释
我们编写了一个命令行工具,叫做autocomment,全局安装
在项目根目录执行该命令,工具会自动将src目录下全部的.vue、.js、.ts文件进行自动添加
该命令工具和前面插件使用共同的配置文件js_doc.conf.js
module.exports = {
// 让jsdoc识别 @log
tag: 'log',
// 告知插件this.$log是发送埋点的方法,定义成数组是因为有的项目可能存在多个
method: ['$log'],
...
}
读取里面tag和method属性。
对形如this.$log('xxxx', {...}),且没有注释方法进行自动注释添加
添加前:
this.$log('PAGE_CLOSE')
添加后
/**
* @log autocomment-PAGE_CLOSE
*/
this.$log('PAGE_CLOSE')
添加注释的逻辑很简单:
依旧通过ast分析,提取this.$log()方法中的actionType参数
加上固定前缀 'autocomment-' 作为最终默认注释
为什么加固定前缀?
因为可以便让开发同学快速找出,哪些注释是通过工具自动添加的。
全局搜索 'autocomment-'
同时,为了消除开发同学的心理障碍(毕竟工具直接改源码,心里还是不踏实的)
为此,我们还做了添加汇总页面,工具添加完注释,会自动弹出
用仿git-history的样式,告知开发同学,我们改了这些地方
问题5:开发时,如何保证遵循埋点添加注释的规范
因为是注释形式收集,开发同学很容易忘记写,那么
解决方案:提供一个监控loader,对开发实时检测
开发时,一旦发现有this.$log()方法调用,且前面没有注释,直接控制台报错
实现方式同样是利用ast语法分析,复用前面的算法就可以了,大同小异。
注意:埋点规范
细心的同学可能也发现了,这个方案确实有几个限制:
actionType必须是字符串
如果是变量或者表达式,ast就无法正常收集。
所以如果需要上报接口返回的内容,可以把值放到backup里。通过参数说明上报接口
不支持场景:
// 错误演示1:
const resp = {...}
this.$log(resp.actionType)
// 错误演示2
this.$log(type === 1 ? 'actionType1' : 'actionTyp2')
如果遇到这种场景,需要变更书写方式
// 错误演示1改进:
const resp = {...}
/**
* @log 内容上报
* @backup type: 数据类型
*/
this.$log('respData', { type: resp.actionType })
// 错误演示2改进:
if (type === 1) {
/**
* @log 行为埋点1
*/
this.$log('actionType1')
} else {
/**
* @log 行为埋点2
*/
this.$log('actionType2')
}
PS:从代码美观上讲,确实不好看。。。但,毕竟功能最重要嘛
另外该方案也 不支持动态添加路由
当然也不是不能支持,有必要的话,可以单独处理。
不过,通常ToC的项目,一般的路由定义方式已经足够了。
ok, 这就是埋点自动收集方案的核心内容。
结语
当初这个方案也和很多业务方的开发同学深入讨论过
其中关注度最高的几个问题:
埋点文档本本就该由pm维护,这等于把pm的工作转嫁给开发了
因为要写注释,导致开发效率降低,业务能否接受
如果出了问题,责任该由谁承担
大家所面临的处境与合作模式千差万别,确实没办法一概而论
但我们的初衷还是希望能够提升整体的协作效率。
最起码可以把团队维护文档的精力节省出来,去思考更有价值的事,不是么~
这个方案目前在已经接入的团队中,广受好评,也确实解决了核心痛点。
我们相信:技术就是效率,技术与产品本来就该相互促进!