概述
在项目开发初期,我为产品设计了一套简易的埋点框架,主要解决了如何定义埋点,以及如何管理埋点的配置。随着项目能力不断的扩展,简易的AOP采集以及手工埋点方式在迭代速度上逐渐无法满足。以及依赖发版才能新增埋点数据,能支撑的业务模式太少,比如短期的线上活动分析就非常不友好,活动初期需要实时的用户使用数据,而活动结束后,这批数据又无需采集。
那么如何才能在不更新埋点规则的情况下,也能让产品同学能有可供分析的数据呢?我的答案是提供更细颗粒度的埋点数据。
方案1.0
以我现在的IM项目为例,使用定制埋点代码的方式来上报埋点数据,比如1个小时内用户点击创建群组按钮10次。这类数据可以理解为结果数据,它直观的提供了一个无需分析的数据。
优点是服务端只需对数据进行合并分组,即可呈现到可视化后台,减少了服务端的开发成本,以及分析计算压力。缺点也很明显,由于客户端已经将数据进行了二次处理,造成了数据失真,通过结果无法反推原先用户的操作。比如用户是从首页创建的群组,还是从通讯录创建的群组,此时就需要再增加两个相关入口的埋点,或者修改“创建群组”埋点规则,增加关于来源的数据字段,并且需要等待下次发版后才能生效
方案2.0
如果把上报数据从设计具体定制埋点,改变为统计用户的行为,则可以更灵活全面的为产品提供数据。比如从上报1小时内用户点击创建群组的次数,改为分别上报用户进入首页,用户点击创建群组按钮,用户创建了群组等多个埋点。此时产品再想分析用户创建群组的行为,可以让后端开发从埋点数据库中获取数据,再通过分析规则,1个小时内某个用户点击创建群组的次数,同时来源分布比例也可同时分析得出。
这个方案的优点如上述,即增加了数据的完整性,提供了灵活分析数据的可能。但也有些不足,如客户端需要在更短的时间周期内上报数据,势必对网络/系统资源的占用更多。分析埋点数据的计算从客户端集中到了服务端,相当于从分布式计算变成了单机计算,增加对服务端资源使用的压力。
所以目前我选择集合使用两种模式,对需要分析,但规则更新频率较低埋点,仍然采用客户端聚合计算后上传,比如页面停留时长的计算。
埋点与采集点
为了更好的规范埋点的使用,先做几个抽象的定义。
埋点类型:
- 事件类型埋点,表示某个事件的发生,也可以通过相同的事件ID来表示一个事件流程。比如用户发送了一张图片,用户从启动到IM登录的过程。
- 页面类型埋点,表示页面的生命周期。比如用户打开通讯录,用户进入聊天会话。
- 状态类型埋点,表示某些配置项的状态。比如用户是否开启推送通知,用户是否启用人脸识别。
- 日常类型埋点,表示在一个时间段内只上报一次的数据。比如用户当日登录。
- 计次类型埋点,由事件类型衍生而来,将时间段内的埋点聚合起来表示时间段内的发生次数。比如一小时内用户进入聊天会话的次数,一小时内用户发送图片消息的次数。
采集点类型:
- 通用型采集点,采集点可以产出多个同类型的埋点数据。比如页面跳转,按钮点击,App生命周期。
- 定制型采集点,采集点对应特定功能的逻辑。比如在创建群组成功后API的异步回调埋点,用户登录的完整流程埋点。
用户行为统计
设计目的
为通用型采集点提供上下文环境,对用户的行为进行更细致的全量采集,同时采集点的设计尽可能不涉及原有逻辑代码的修改,即目前广泛应用的无埋点模式。
用户行为链
每当用户行为触发时,创建用户行为模型加入用户行为链,最终可以形成一条记录用户行为的链表。
用户行为类型:
- 页面跳转类型,比如页面初始化,页面展现,页面消失。
- 按键点击类型,比如功能按钮点击(发送表情),复用列表点击(从通讯录进入个人详情)。
- 系统事件类型,比如系统推送,系统截图,系统静音。
采集用户行为
以iOS为例,使用AOP去Hook页面的生命周期,手势的delegate,复用列表的Delegate,UIApplication的事件处理等,来完成采集点的初步搭建。
这里利用了Objc的Runtime特性,实现进行方法实现替换,如果语言不支持,使用基类继承的方式也能实现,只是成本会大很多。关于delegate的hook需要用到动态类创建,类似于系统框架实现KVO的机制,实现方案不太赘述,推荐七步实现列表点击事件的采集
基于采集点,提供插件协议,将实现协议的插件注册到用户行为链管理器中,即可在事件触发式得到回调,再根据不同的筛选规则,生成不同的定制埋点。
)
事件标识
为了通过用户行为链来分析当前用户的行为,需要保证每个事件都有标识符。通过标识符来预设规则,为触发规则的行为创建埋点数据。
页面跳转类型标识符
以iOS的UIKit框架提供NavigationController,可以维护页面栈,可以得到从跟页面至当前页面的所有页面数据。
比如从首页(HomePage)先跳转会话详情界面(SessionPage),再跳转联系人详情界面(UserInfoPage),组成了导航路径HomePage -> SessionPage -> UserInfoPage。此时,即可将不同页面的Class作为单页面标识符组合成页面路径使用,如:“HomePage/SessionPage/UserInfoPage”。
如果当前系统框架并没提供类似功能,则需要自己设置一个标识符,或者将当前页面的Class名作为标识符,如"UserInfoPage"。
每当触发页面跳转时,创建一个用户行为模型加入到用户行为链中,如此只需要查询用户行为链即可知道用户的历史页面操作。
按键点击类型标识符
按键类型分为两种,一种为单独使用按键,一种为复用列表按键。
单独使用按键可能存在UI控件被复用,按键的响应函数被复用的情况。可以组合按键的Class名、按键的响应函数名、按键所在的页面Class名来描述。
比如在会话详情(SessionPage)点击消息气泡的头像(ImageView),触发了联系人详情界面跳转动作(pushUserInfo)。根据标识符生成规则,可以组成“SessionPage/ImageView/pushUserInfo”的标识符。
复用列表按键用于动态列表中,会同时出现多个相同类型的按钮,在用户刷新或滚动的途中,相同按键的关联数据源可能发生改变,复用列表的响应函数几乎也都是同一个函数。此时在单独使用按键的标识符规则上,去除响应函数名,加入按键的位置信息来重新组成标识符。
比如在首页(HomePage)点击会话列表中第3个会话(SessionListCell),此时可以收集到的信息就只有按键的Class名称,按键所在页面的Class名以及按键的相对位置信息(第0个session,第3个row),可以组合成“HomePage/SessionListCell/0:3”。
如果发生数据源变化的情况,则还需要解析数据源模型,进行定制的埋点操作。
系统事件类型标识符
这些事件是通过用户使用了系统功能后触发形成的,比如用户截屏,用户点击推送。可以直接设置默认字符串进行标识。
用户行为分析
复杂的埋点规则,通常很难单凭一个用户行为数据做出单独的判断。
比如从首页(HomePage)使用全局搜索界面(GlobalSearchPage)搜索联系人,点击联系人跳转联系人详情界面(UserInfoPage),在联系人详情界面(UserInfoPage)中点击发送消息,进入会话详情界面(SessionPage)。需求是统计会话详情的来源是否为首页的全局搜索界面。
当采集点被触发时,我们需要进行一系列的判断
- 判断距离当前用户行为是否为会话详情界面(SessionPage)的页面跳转。
- 判断距离当前最近的一次页面跳转事件是否全局搜索界面(GlobalSearchPage)。
- 判断全局搜索界面(GlobalSearchPage)之前是否来源为首页(HomePage)。
注意,用户行为链会记录全量的用户行为,可能会存在重复的用户操作,以及已经被统计过的用户行为并不会从链中移除。
查询深度
我们可以根据页面跳转行为作为分界线来给用户行为链分层,设计命中埋点时需要查询的最小深度来规避重复统计。
比如当前用户从首页(HomePage)跳转到全局搜索界面(GlobalSearchPage),又从全局搜索界面(GlobalSearchPage)退回到了首页(HomePage),接着用户去通讯录(ContactPage)等其他页面进行了十几个页面的跳转,最后通过联系人详情界面(UserInfoPage)的路径进入了会话详情界面(SessionPage)。
如果不设计查询深度,则此时仍然能查到链中存在跳转全局搜索界面(GlobalSearchPage)的用户行为记录。单纯判断是否全局搜索界面(GlobalSearchPage)的跳转记录会误判会话详情界面(SessionPage)的来源。如果将查询深度设置为2,则只查询会话详情界面(SessionPage)之前的两次页面跳转(用户详情与全局搜索界面),之前的用户行为记录则可以忽略。
埋点验证
为了验证埋点数据的有效性,在开发模式下,每个埋点触发时,都应该打印相关信息到控制台和写入日志文件,再通过脚本验证日志的方式来确保每一步操作的埋点能正确记录。