背景
手动埋点一直以来都是一个比较麻烦的行为,埋点不属于业务逻辑,而我们必须插入业务逻辑中,有可能就导致了我们不得不修改设计以满足埋点的需求。
同时数据方很多时候会要求带上额外的数据,而这些额外的数据甚至和当前模块毫无关系,也就是破坏了我们的独立性和解耦原则。比如在事件点击中加入当前页面的pageId,当前cell的index等等。
简介
目前我们手动埋点的埋点信息可能包含的内容比较多,但也可以进行归类。
不同类型的埋点信息都是在不同的层上收集的,比如我们需要pageId,那么这个必然是在当前VC上才有,如果我们要收集上个页面的pageId,那么只有navigationController上会有该信息,而如果我们需要收集cell的位置信息,那么我们需要在dataSource上进行收集。
这给我们的埋点收集一个思路,我们手动埋点的时候不要马上收集完所有数据上报,而是收集当前层存在的信息,并把这个埋点任务抛向上层。每一层都会往埋点任务里面写入自己层的信息,直到结束,这样我们就可以收集一个非常完整的埋点信息。
当然这样做的缺点是埋点里面的信息比较多,比较冗余,但作为埋点信息来说,信息冗余并不是太大的问题。
要建立这样一个结构,如果是一些新业务或者组织架构设计较好的业务来说,会比较简单,但是对于一个已经成型的,有很多复杂的业务却比较麻烦。这里需要去寻找一种方法,使改动量尽可能的小,并且组织灵活。
数据流
数据方向
首先我们来看看一般应用的页面结构。
绿色:Application
蓝色:View
橘色:ViewController
我们更新页面的数据方向是这样的:
但是我们埋点触发,需要收集的数据却是这样的:
我们更新数据和收集数据的方向是完全相反的,这也就是导致我们手动埋点总是会感觉很别扭的原因。
而我们一般做埋点的方式,是将这个事件通过回调等方式一层层传递到ViewController上,然后进行统一埋点。
但是对于一个复杂的VC,我们往往会继续拆分,成为多个独立组件的组合(可以看我之前的DDComponent),这样不仅能避免VC的无限膨胀,也能解耦业务。但是遇到埋点需求时,我们却不得不打破这个组织方式,因为有很多信息必须要在VC上才能获取到。
数据流
我们在上面了解到的埋点现状,可以抽象为一条数据流,在这个数据流上有多个节点,不同的节点可以收集必要的埋点信息。
而每一个节点,可以抽象为一个Stream。
这里我们可以看到每一个节点同时也是响应链的一部分,那么我们是不是就可以依靠响应链来连接整个数据流呢?这样我们就不需要大量改动代码,从而达到建立这套系统了。
1 |
|
这里我们还需要为这条数据流创建一个终点,也就是最终输出端,可以选择Application上的节点为输出节点。
1 | @implementation UIApplication (TracePipeline) |
使用
这里我们举一个列表中点赞埋点的列子:
在cell中的某个view中,触发了点赞:
1 | - (void)onPriase:(id)sender { |
但是我们还需要知道这个cell是位于列表中的位置,那么我们可以cell中加入该信息:
1 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { |
然后我们还需要知道是在哪个页面中触发的,那么我们需要在vc中加入:
1 | - (void)viewDidLoad { |
那么最后我们收集到的埋点信息就会包括了praise
, index
, page
信息。
自定义
在开发过程中,我们可能还会有很多业务分割的部分,比如ViewModel,这些内容并不一定在响应链中,那么我们需要能够将这个数据流通过该组件来收集数据。
这里我们可以在默认的数据流中插入一个自定义节点来解决。本身这个数据流和响应链其实并没有什么关系,我们只是利用了响应链的结构而已。
1 | viewModel.view.trace.outStream = viewModel.trace; |
这样我们就可以自定义数据节点了。
过滤器
由于各种原因,我们的埋点并不能完全的通用,比如有时候,同样是点赞,有时候是praise
,有时候是like
。有时候我们又不希望触发某些埋点。
我们需要在某些确定的节点要求能够过滤或者转换某些埋点,那么我们需要在每个节点上添加过滤器:
1 | [self.trace.filter setFilterBlock:^BOOL(NMTraceInfo * _Nonnull traceInfo) { |
虽然这么做并不是特别好,但在一些无法抗拒的情况下还是能够给我们一个修改的机会。
逻辑型埋点
另一种让我们所有人都头疼的就是逻辑型埋点。
一般来说我们的埋点都是事件埋点,也就是触发一个方法,我们埋一次。但是有些需求要统计时长,比如用户点播放,和点暂停之前的时间间隔。按照原有方法埋点,那么我们可能需要在VC里增加一个成员,专门为了记录这个时间间隔。而这些成员和业务毫无关系,只服务于埋点信息,导致了业务的耦合增加。
对于我们程序来说,事件的点是比较简单的,而这种逻辑型埋点则非常复杂。如果我们把逻辑型埋点都转化为事件埋点,那么就会减少很多复杂度。
1 | __block NSDate *playTime = nil; |
总结
这种方案虽然能够解决很大一部分的埋点痛点,但也依然存在一些缺陷。
- 开发者必须要了解这种方案的原理,才能知道在什么节点收集埋点,以及如何插入自定义节点。
- 埋点信息会比较冗余,会带上一些无用信息。
- 埋点信息必须保证大部分是统一的,不然每个页面全部采用不同的key和埋点方式,也会让这个系统失去复用的能力。