Feb 12

一种无痕埋点痛点解决思路

Lrdcq , 2018/02/12 22:28 , 程序 , 閱讀(8874) , Via 本站原創
在前端工程中,埋点和统计早在几年前就进入了无痕埋点时代,但是在实际工程中,我们还是觉得,常见的无痕埋点方案,写起来形式是简单了,但是从实现需求的角度并没有帮啥大忙。

痛点

因此在实际工程中,我们看看我们的痛点。无痕埋点,在实际工程中的写法,往往是这样的:
myBtn.sta_bid = xxx;
myBtn.sta_click_val = @{
    @"a" : a,
    @"b" : b
};
//.....

本质上做的是,把将上报的数据作为view属性赋予,并且打开开关,就自动进行上报了。本质上,是节约掉了手动实现事件触发上报的工程。——不过这个过程真的繁琐么。事实上我们写埋点的时候很自然的就会把这种重复吃力不讨好的代码抽象化,所以问题不大。
因此回到剩余的部分,我们依然觉得麻烦的,就是我们需要给view设置的这么些属性,的数据的来源了。具体来说,我们每一个埋点往往需要一些什么呢:

- 页面id,cid,埋点框架设计上往往单抽出来全局设置的,因此问题不大。

- 事件类型,action,比如PageView,Click(点击),View(曝光),Edit(编辑)等,这些大部分无痕埋点框架可以给出,但是还是有指定的余地。

- 事件id,bid,事件的唯一表示。会出现的问题是事件可能会以业务或页面为单位,但控件或程序实现可能是复用的,如何进行分割标记是一个问题。

- 业务数据,val。

其中,业务输出是最难处理的一块,比如我们有这么一个页面:

點擊在新視窗中瀏覽此圖片

我们希望在二级分类的一个列表中一个按钮的埋点上,上传包括一级分类id,二级分类id,item的id。显然,item的id是最好获得的,但是一级分类id,二级分类id显然不会直接暴露到当前cell中,就需要一层一层的传输进来了。

因此,我们需要专门为埋点这事儿设计多余的组件属性,并且进行绑定和传递,如果页面层次更复杂,或者还带弹窗,或者交互变化的关系的话,。这显然违背了无痕埋点的初衷——不为埋点做多余的业务逻辑。

综上所诉,我们现在最大的痛点是,为了将埋点依赖的数据耦合到事件发生的view上,还是需要编写复杂的逻辑才能完成。

目标

那么,从程序抽象的角度,到底想怎么抽象这些数据呢:

- 我们注意到我们的数据分为很多类型,有些必填有些可填,也分别对应了不同界面的不同属性。我们是否可以把他们用相同的规则进行抽象,并消除差异。

- 页面id就存在只需要在controller上设置,就可以代表当前页面下所有页面id的机制,那么其他的属性是否也可以这么处理来避免多余的数据传递。

- 页面中也存在子页面,比如viewPager那样的页面结构的场景,因此抽象时不需要区别页面和view,作为同一个概念处理。

- 通过拦截器实现对必选可选数据的筛选来方便拓展,并与实现层分离。

另外,从事件埋点设计上,需要的业务数据应与界面逻辑一致,即有什么报什么,而不是要什么我们再去造,这样就失去前端埋点的初衷了——毕竟前端埋点的核心是描述用户行为,而不是描述业务数据。

解决思路

因此总结出来,我们解决这个问题的核心抽象是:

- view/responder:触发事件的目标
  - sta_val:属性,当前页面携带的数据的map
  - trigger:方法,触发事件

一句话:每一次触发事件,遍历当前view以上的所有父view直到尽头,把所有sta_val放在一起,即这次埋点所需要的全部数据。

多说无用,下面通过几个例子来优化和改进这个解决方案:

事例一

就是上面说到的一二级分类的场景,在现有框架下,我们只需要:

- 在当前页面的根view上设置cid
- 在一级分类的每一个pageview上设置一级分类的id
- 在二级分类的每一个pageview上设置二级分类的id
- 在item的view上设置item的id
- 在item的对应按钮上设置action=click和bid来开启点击埋点

这样在item的点击上,就可以很自然,不做多余逻辑的完成这个埋点了。

事例二

點擊在新視窗中瀏覽此圖片

我们来看知乎的话题的首页,这是一个典型的嵌套viewPager结构,外层pager和里层三个pager有各自的cid。现在我们需要做埋点:

- 对外层的“关注话题”,需要话题id
- 内层的“关注问题”,需要话题id和问题id

因此,在现有框架下,我们需要:

- 在当前页面的根view上设置cid,为每个pager分别设置cid
- 在当前页面的根view设置话题id。因为当前页面是单个话题首页,所以这显然是合理的
- 在讨论pager的每个item上设置问题id
- 在“关注话题”和“关注问题”按钮上设置action=click和分别的bid来开启点击埋点

这里出现了一个问题,内外层page都设置了cid,那么这种数据重复的场景怎么处理?考虑一下其他可能出现的场景:

- 淘宝商品详情页,根页面注册商品id,下面推荐商品列表,item注册商品id。item点击操作,需要的是内部的那个id。
- 一个块状按钮,点击有对应bid,其子view中有一个小按钮,有对应bid。此时小按钮的点击事件,应认的自己的bid

可以想象在绝大部分场景下,如果我们对一个更内部的view设置了一些属性,我们当然是想最终埋点使用它啦。因此,补充规则:

如果遍历到重复属性,以最内侧属性为准。

事例三

比如这么一个页面:

點擊在新視窗中瀏覽此圖片

在一个列表中item的一个按钮,点击出现一个菜单,需对菜单上的东西进行埋点,需要列表item的id。

这个场景的问题是,往往菜单等通过动效或者别的方式出现的界面,布局上并不是一个subview的构造,因此用上面的规则从爬界面树的角度,是无法实现这个场景的埋点的。因此我们继续拓展规则,添加属性:

- view/responder:触发事件的目标
  - sta_parent:属性,统计功能上的父级view,默认为null

这样的话,我们可以定义爬树的时候的规则:如果这个view上有sta_parent,则从它往上爬,否则从实际界面树上爬。

因此,这个场景的实现会是:

- 在当前页面的根view上设置cid
- 为每个item设置业务id
- 在item打开弹窗的时候,将弹窗的sta_parent指向this
- 在弹窗按钮上设置action=click和分别的bid来开启点击埋点

事例四

最复杂的情况,是跨页面的埋点数据传输,假设是事例一的一二级分类场景,item点击打开详情页,详情页的操作埋点需要列表页的一二级分类id,那么如何实现呢。

如果是ios或者web伪路由,可以直接将新页面的根view的sta_parent指向列表页面的控件,但是在android中,页面之间不宜相互持有,因此并不可行。另外,还需要考虑的是,打开新页面后,上一个页面被销毁的情况。在不同的页面切换和路由实现中,这也是很常见的。

因此为了页面切换的case,我们准备工具类:

- view/responder(sta_val_utils):统计数据工具拓展
  - pack:打包,把当前view下的统计信息打包为一个map
  - unpack:拆包,把一个map塞入当前view中

然后通过各端页面间数据传输的方式,手动把统计数据传输到下一个页面即可,其他设置照常完成便能实现这个埋点。虽然看起来繁琐,但是这应该是罕见场景因此只需支持即可。

其他

设计完成后,我们在来看这一套埋点方案下,最佳实践的核心设计原则:

- 埋点以页面数据为基础进行设计:界面上有什么,就报什么;而不是埋点需要什么,我们再从页面上找出来。

- 埋点以业务逻辑为基础进行设计:业务逻辑携带了什么,就报什么;不要为埋点编写业务逻辑。


在这两条原则下,这一套无痕埋点方案便可以很简单流畅优雅的编写并实现绝大部分需求了。

另外作为无痕埋点框架的组成部分,以上痛点主要集中在数据来源上,具体trigger上报事件,又是一通复杂的操作。只不过这个就很常见了:

- 对抽象的trigger进行具体实现:
  - pageview实现:注入controller/activity/组件生命周期触发
  - click实现:监听按起事件进行触发
  - view实现:这又是一个巨大的工作了,调研过后市面上几乎没有现成的曝光触发方案,可能会单独讨论
  - 其他:不行,还是得手动触发

不过这一块由于有比较简易的需求实现方式,因此暂时还不成为break级的痛点,但是也免不了一顿吐槽和腥风血雨了。
关键词:埋点
logo