共计 14003 个字符,预计需要花费 36 分钟才能阅读完成。
本文作者:dl
一、背景
在以后挪动互联网时代,一个产品想疾速、精确的抢占市场,无疑是须要产品疾速迭代更新,如何帮助产品经理对产品以后的数据做出最优判断是要害,这就须要客户端侧提供 高精度 、 稳固 、 全链路 的埋点数据;做客户端开发的同学都粗浅晓得,想要在开发过程中满足上述三点,开发过程都是头大的;
针对这个问题,咱们自研了一套全链路埋点计划,从埋点设计、到客户端三端 (iOS、Android、H5) 开发、以及埋点校验 & 稽查、再到埋点数据应用,目前曾经广泛应用于云音乐各个次要 APP。
二、先聊聊传统埋点计划的弊病
传统埋点,就是 BI 数据人员依据策动想要的数据,设计出一个个的 单点 的坑位埋点,而后客户端人员一一埋进来,这些埋点常常都存在以下特点:
- 坑位的事件埋点很简略:点击 / 双击 / 滑动等明确的事件类埋点,很简略,依据需要一个一个埋下来即可
- 资源位曝光埋点是噩梦:在列表 / 非列表资源的曝光埋点场景,想做到 高精度 (埋点精度提到 99.99%) 难度很大,你有可能每一个曝光埋点都须要思考如下大部分场景:
- 每个坑位都是独立的:坑位之间的埋点没有关系,须要给每一个坑位 起名字(比方通过随机字符串,或者组合参数来标识),页面、列表、元素之间,存在大量的反复参数,以达到数据分析要求
- 漏斗 / 归因剖析难:因为每一个坑位埋点都是独立的,APP 应用过程中先后产生的埋点是无关联的,想要做到漏斗 / 归因剖析,须要客户端做 魔鬼参数 传递,而后数据分析时再一一场景的做参数关联剖析
- 坑位黑盒:想晓得一个 app 有多少坑位埋点,以后页面下曾经显现出了多少坑位,坑位之间是什么关系,治理老本高
三、咱们已经做过的一些尝试
3.1 无痕埋点
市面上有很多人介绍 无痕埋点,咱们已经也做过相似的尝试;这种无痕,次要是针对一些坑位事件(比方点击、双击、滑动等事件)埋点做主动生成埋点,同时附带上生成的xpath(依据 view 层级生成),而后把埋点上报到数据平台后,再将 xpath 赋予实在的业务意义,从而能够进行数据分析;
然而这个计划的问题是只能解决一些简略事件场景,并且数据平台做 xpath 关联是一件噩梦,工作量大,最次要的是 不稳固,对于埋点数据高精度场景,这个计划不可行(没有哪个客户端开发人员天天破费大量工夫查找 xpath 是什么意义,以及随着迭代业务的开发,xpath 因为不受管制的变动带来的数据问题带来的排查工作量是微小的)。
特地对于资源位的曝光上,想要做到真正的无痕,主动埋点,是不太可行的;比方列表场景,底层是不意识一个 cell 是什么资源的,甚至都也不晓得是不是一个资源。
四、咱们的计划
4.1 对象
对象是咱们计划埋点治理和开发的根本单位,给一个 UIView 设置 _oid(对象 Id: Object Id),该 view 就是一个对象; 对象分为两大类,page & element;
- page 对象: 比方 UIViewController.view, WebView, 或者一个半屏浮层的 view,再或者一个业务弹窗
- element 对象: 比方 UIButton, UICollectionViewCell, 或者一个自定义 view
- 对象参数: 对象是埋点具体信息的承载体,承载着对象维度的具体埋点参数
- 对象的复用: 对象的存在,其中一个很大的起因,就是须要做复用,对于一些通用 UI 组件,尤为适合
4.2 虚构树(VTree)
对象不是孤立存在的,而是以 虚构树 (VTree) 的形式组合在一起的, 上面是一个示例:
虚构树 VTree 有如下特点:
- View 树子集: 原始 view 树层级很简单,被标识成对象的称为节点,所有节点就组合成了 VTree,是原始 view 树的子集
- 上下文: 虚构树中的对象,是存在高低关系的,一个节点的所有先人节点,就是该对象 (节点) 的上下文
- 对象参数: 有了节点的高低层级,不同维度的对象,只关怀本人维度的参数,比方歌单详情页中歌曲 cell 不关怀页面申请级别的歌单 id
- SPM: 节点及其所有先人结点的 oid 组成了 SPM 值(其实还有 position 参数的参加,稍后再详解),该 SPM 能够惟一定位该节点
- 继续生成: VTree 是源源不断的构建的,每一个 view 产生了变动,View 的增加 / 删除 / 层级变动 / 位移 / 大小变动 /hidden/alpha,等等,都会引起从新构建一颗新的 VTree
五、埋点的产生
下面的计划介绍完之后,你肯定存在很多纳闷,有了对象,有了虚构树,对象有了参数,埋点在哪儿?
5.1 先来看下埋点格局
一个埋点除了有事件类型(action), 埋点工夫等一些根本信息之外,还得有业务埋点参数,以及能体现出对象上下级的构造
先来看下一个一般埋点的格局:
{
"_elist": [
{
"_oid": "【必选】元素的 oid",
"_pos": "【可选】,业务方配置的地位信息",
"biz_param": "【按需】业务参数"
}
],
"_plist": [
{
"_oid": "【必选】page 的 oid",
"_pos": "【可选】,业务方配置的地位信息",
"_pgstep": "【必选】, 该 page/ 子 page 曝光时的页面深度"
}
],
"_spm": "【必选】这里形容的是节点的“地位”信息,用来定位节点",
"_scm": "【必选】这里形容的是节点的“内容”信息,用来形容节点的内容",
"_sessid": "【必选】冷启动生成,会话 id",
"_eventcode": "【必选】事件: _ec/_ev/_ed/_pv/_pd",
"_duration": "数字,毫秒单位"
}
- _eventcode: 埋点的类型,比方元素点击(_ec), 元素曝光开始(_ev), 元素曝光完结(_ed), 页面曝光开始(_pv), 页面曝光完结(_pd) 等等
- _elist: 从以后元素节点开始,向上所有元素节点的汇合,是一个数组,顺叙
- _plist: 从以后节点开始,向上所有页面结点的即可,是一个数组,顺叙
- _spm: 下面曾经介绍(SPM),能够惟一定位该坑位
从下面的数据结构能够看出,数据结构是结构化的,坑位不是独立的,存在层级关系的
5.2 点击事件
大部分的点击事件,都产生在如下四个场景上:
- UIView 上增加的 TapGesture 单击手势
- UIControl 的子类增加的 TouchUpInside 单击事件
- UITableViewCell 的 didSelectedRowAtIndexPath 单击事件
- UICollectionViewCell 的 didSelectedItemAtIndexPath 单击事件
对于上述四种场景,咱们采纳了 AOP 的形式来外部承接掉,这里简略阐明下如何做的;
-
UIView: 通过 Method Swizzling 形式来进行对要害办法进行 hock,当须要给 view 增加 TapGesture 时,顺便增加一个咱们本人的 TapGesture, 这样咱们就能够在点击事件触发的时候减少点击埋点,要害办法如下:
- initWithTarget:action:
- addTarget:action:
- removeTarget:action:
- 对 UIView 点击事件的 hock 留神须要做到随着业务侧事件的减少 / 删除而一起减少 / 删除
- 同时,咱们做到了在 所有业务侧点击事件触发之前(pre) & 所有业务侧点击事件触发之后(after) 两个维度的 hock
要害代码如下:
@interface UIViewEventTracingAOPTapGesHandler : NSObject
@property(nonatomic, assign) BOOL isPre;
- (void)view_action_gestureRecognizerEvent:(UITapGestureRecognizer *)gestureRecognizer;
@end
@implementation UIViewEventTracingAOPTapGesHandler
- (void)view_action_gestureRecognizerEvent:(UITapGestureRecognizer *)gestureRecognizer {if (![gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]
|| gestureRecognizer.ne_et_validTargetActions.count == 0) {return;}
UIView *view = gestureRecognizer.view;
// for: pre
if (self.isPre) {
/// MARK: 这里是 Pre 代码地位
return;
}
// for: after
/// MARK: 这里是 After 代码地位
}
@interface UITapGestureRecognizer (AOP)
@property(nonatomic, strong, setter=ne_et_setPreGesHandler:) UIViewEventTracingAOPTapGesHandler *ne_et_preGesHandler; /// MARK: Add Category Property
@property(nonatomic, strong, setter=ne_et_setAfterGesHandler:) UIViewEventTracingAOPTapGesHandler *ne_et_afterGesHandler; /// MARK: Add Category Property
@property(nonatomic, strong, readonly) NSMapTable<id, NSMutableSet<NSString *> *> *ne_et_validTargetActions; /// MARK: Add Category Property
@end
@implementation UITapGestureRecognizer (AOP)
- (instancetype)ne_et_tap_initWithTarget:(id)target action:(SEL)action {if ([self _ne_et_needsAOP]) {[self _ne_et_initPreAndAfterGesHanderIfNeeded];
}
if (target && action) {UITapGestureRecognizer *ges = [self init];
[self addTarget:target action:action];
return ges;
}
return [self ne_et_tap_initWithTarget:target action:action];
}
- (void)ne_et_tap_addTarget:(id)target action:(SEL)action {
if (!target || !action
|| ![self _ne_et_needsAOP]
|| [[self.ne_et_validTargetActions objectForKey:target] containsObject:NSStringFromSelector(action)]) {[self ne_et_tap_addTarget:target action:action];
return;
}
SEL handlerAction = @selector(view_action_gestureRecognizerEvent:);
// 1. pre
[self _ne_et_initPreAndAfterGesHanderIfNeeded];
if (self.ne_et_validTargetActions.count == 0) { // 第一个 target+action 被增加的时候,才增加 pre
[self ne_et_tap_addTarget:self.ne_et_preGesHandler action:handlerAction];
}
[self ne_et_tap_removeTarget:self.ne_et_afterGesHandler action:handlerAction]; // 保障 after 是最初一个,所以后行尝试删除一次
// 2. original
[self ne_et_tap_addTarget:target action:action];
NSMutableSet *actions = [self.ne_et_validTargetActions objectForKey:target] ?: [NSMutableSet set];
[actions addObject:NSStringFromSelector(action)];
[self.ne_et_validTargetActions setObject:actions forKey:target];
// 3. after
[self ne_et_tap_addTarget:self.ne_et_afterGesHandler action:handlerAction];
}
- (void)ne_et_tap_removeTarget:(id)target action:(SEL)action {[self ne_et_tap_removeTarget:target action:action];
NSMutableSet *actions = [self.ne_et_validTargetActions objectForKey:target];
[actions removeObject:NSStringFromSelector(action)];
if (actions.count == 0) {[self.ne_et_validTargetActions removeObjectForKey:target];
}
if (self.ne_et_validTargetActions.count > 0) { // 删除以后 target+action 之后,还有其余的,则不需做任何解决,否则清理掉 pre+after
return;
}
SEL handlerAction = @selector(view_action_gestureRecognizerEvent:);
[self ne_et_tap_removeTarget:self.ne_et_preGesHandler action:handlerAction];
[self ne_et_tap_removeTarget:self.ne_et_afterGesHandler action:handlerAction];
}
- (BOOL)_ne_et_needsAOP {return self.numberOfTapsRequired == 1 && self.numberOfTouchesRequired == 1;}
- (void)_ne_et_initPreAndAfterGesHanderIfNeeded {if (!self.ne_et_preGesHandler) {UIViewEventTracingAOPTapGesHandler *preGesHandler = [[UIViewEventTracingAOPTapGesHandler alloc] init];
preGesHandler.isPre = YES;
self.ne_et_preGesHandler = preGesHandler;
}
if (!self.ne_et_afterGesHandler) {self.ne_et_afterGesHandler = [[UIViewEventTracingAOPTapGesHandler alloc] init];
}
}
@end
- UIControl: 通过 Method Swizzling 形式对要害办法进行 hock,要害办法: sendAction:to:forEvent:
对 UIcontrol 点击事件的 hock 须要留神业务侧增加了多个 Target-Action 事件,不能埋点埋了屡次
同样,也反对 pre & after 两个维度的 hock
要害代码如下:
@interface UIControl (AOP)
@property(nonatomic, copy, readonly) NSMutableArray *ne_et_lastClickActions; /// MARK: Add Category Property
@end
@implementation UIControl (AOP)
- (void)ne_et_Control_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {NSString *selStr = NSStringFromSelector(action);
NSMutableArray<NSString *> *actions = @[].mutableCopy;
[self.allTargets enumerateObjectsUsingBlock:^(id _Nonnull obj, BOOL * _Nonnull stop) {NSArray<NSString *> *actionsForTarget = [self actionsForTarget:obj forControlEvent:UIControlEventTouchUpInside];
if (actionsForTarget.count) {[actions addObjectsFromArray:actionsForTarget];
}
}];
BOOL valid = [actions containsObject:selStr];
if (!valid) {[self ne_et_Control_sendAction:action to:target forEvent:event];
return;
}
// pre
if ([self.ne_et_lastClickActions count] == 0) {/// MAKR: 这里是 Pre 代码地位}
[self.ne_et_lastClickActions addObject:[NSString stringWithFormat:@"%@-%@", [target class], NSStringFromSelector(action)]];
// original
[self ne_et_Control_sendAction:action to:target forEvent:event];
// after
if (self.ne_et_lastClickActions.count == actions.count) {
/// MARK: 这里是 After 代码地位
[self.ne_et_lastClickActions removeAllObjects];
}
}
@end
- UITableViewCell: 先对 setDelegate: 进行 hock,而后以 NSProxy 的模式将 Original Delegate 进行 封装,组成 Delegate Chain 的模式,而后在 DelegateProxy 外部做音讯散发,从而能够齐全掌控点击事件
- 该 Delegate Chain 的形式能够 hock 的不反对 点击事件,能够 hock 所有 Delegate 的办法
- 同样,也反对 pre & after 两个维度的 hock
- 特地留神: 须要做到真正的 DelegateChain,不然会跟不少三方库抵触,比方 RXSwift,RAC,BlocksKit,IGListKit 等
要害示例代码几个重要的相干办法 (代码较多不再展现,三方有多个库均能够借鉴):
- (id)forwardingTargetForSelector:(SEL)selector;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector;
- (void)forwardInvocation:(NSInvocation *)invocation;
- (BOOL)respondsToSelector:(SEL)selector;
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
5.3 曝光埋点
曝光埋点在传统埋点场景下是最辣手的,很难做到 高精度 埋点,埋点机会总是穷举不完,即便有了欠缺的标准,开发人员还总是会脱漏场景
咱们这里的计划让开发者齐全疏忽曝光埋点的机会,开发者只把精力放在构建对象(或者说构建 VTree),以及给对象增加参数上,上面看下是如何基于 VTree 做曝光的:
- 继续构建 VTree: 后面提到,VTree 是源源不断的构建的,每一个 view 产生了变动,View 的增加 / 删除 / 层级变动 / 位移 / 大小变动 /hidden/alpha,等等(这里均是 AOP 形式 hock),都会引起从新构建一颗新的 VTree
- VTree Diff: 先后两个 VTree 的 diff,就是咱们曝光埋点的后果
随着工夫,会源源不断的生成新的 VTree:
比方 T1 时刻生成的 VTree:
T2 时刻生成的 VTree:
先后两颗 VTree 的 diff:
- T1 存在 T2 不存在的节点: 3, 4, 6, 7, 8, 11
- T1 不存在 T2 存在的节点: 20, 21, 22, 23
下面的 diff 后果,就是曝光埋点的论断
- 曝光完结: 3, 4, 6, 7, 8, 11
- 曝光开始: 20, 21, 22, 23
从下面以及 VTree Diff 的曝光策略,得出如下:
- 这种策略,齐全抹平了列表和非列表
- 曝光机会问题,转而变成了何时构建 VTree 问题上
- 资源是否曝光的问题, 转而变成了 VTree 中节点的可见性问题上
5.4 埋点开发步骤
基于 VTree 的埋点,不论是点击、滑动等事件埋点,还是元素、页面的曝光埋点,转化成了如下两个开发步骤:
- 给 View 设置 oid => 成为对象 (构建 VTree)
- 给对象设置埋点参数
六、VTree 的构建
6.1 VTree 构建过程
构建一个 VTree,是须要遍历原始 view 树的,构建过程中有如下特点:
- 一个节点是否可见,跟 view 的 hidden, alpha 无关,并且必须增加到 window 上
- 子节点的可见区域小于等于父节点的可见区域
- 节点的可见区域,能够自定义的 扩充 或者 放大, 就像 UIButton 的 contentEdgeInsets 那样
- 节点是能够被遮挡的: 一个 page 节点能够遮挡父节点名下增加程序早于本人的其余节点
从虚构树上来看,被遮挡的后果:
-
可突破原有 view 层级关系: 能够手工干涉高低层级关系,以做到逻辑挂载的能力
事实上,目前提供了三种逻辑挂载能力,这里简略提下,不做具体开展
- 手动逻辑挂载: 指定将 A 挂载到 B 名下
- 主动逻辑挂载: 将 A 挂载到以后 rootPage(以后 VTree 最上层最右侧的 page 节点)名下
- spm 形式逻辑挂载: 指定将 A 挂载到
spm
名下(对于解耦特地有用)
- 虚构父节点: 能够给多个节点虚构出一个父节点,对于双端 UI 差别时,然而要求同一套埋点构造时,很有用
一个常见的例子,拿云音乐首页列表举例子,每一个模块的 title 和资源容器 (外部可横向滑动),别离是一个 cell;图中的浅红色(模块) 其实没有一个 UIView 与之对应,业务侧埋点须要咱们提供 模块 维度的曝光数据(然而 Android 开发过程中,通常都有 UI 与之对应)
精细化埋点:
- 自定义可见区域 & 遮挡 & 节点的递归可见性 联合起来,能够做到精细化埋点成果
- 针对 tabbar, navbar, 再或者云音乐 app 底部的 mini 播放条等场景引起的列表 cell 是否曝光的问题,可做到精细化管制
- 以及配合遮挡能力,真正做到了节点所见及曝光,不可见即曝光完结的成果
6.2 构建过程的性能思考
view 的任何变动,都会引起 VTree 构建,看上去这是一件很恐怖的事件,因为每一次构建 VTree 都须要遍历整颗原始 view 树,咱们做了如下优化来保障性能:
- 主线程 runloop 闲暇的时候构建 VTree(而且须要该 runloop 曾经运行的工夫,小于等于 16.7ms/3,这是拿固定帧率 60 帧举例)
- runloop 构建限流器
要害代码如下:
/// MARK: 增加最小时长限流器
_throtte = [[NEEventTracingTraversalRunnerDurationThrottle alloc] init];
/// 至多距离 0.1s 才做一次
_throtte.tolerentDuration = 0.1f;
_throtte.callback = self;
/// MAKR: runloop observer
CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL};
const CFIndex CFIndexMax = LONG_MAX;
_runloopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, CFIndexMax, &ETRunloopObserverCallback, &context);
/// MAKR: Observer Func
void ETRunloopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {NEEventTracingTraversalRunner *runner = (__bridge NEEventTracingTraversalRunner *)info;
switch (activity) {
case kCFRunLoopEntry:
[runner _runloopDidEntry];
break;
case kCFRunLoopBeforeWaiting:
[runner.throtte pushValue:nil];
break;
case kCFRunLoopAfterWaiting:
[runner _runloopDidEntry];
break;
default:
break;
}
}
- (void)_runloopDidEntry {_currentLoopEntryTime = CACurrentMediaTime() * 1000.f;
}
- (void)_needRunTask {CFTimeInterval now = CACurrentMediaTime() * 1000.f;
// 如果本次主线程的 runloop 曾经应用了了超过 16.7/2.f 毫秒,则本次 runloop 不再遍历,放在下个 runloop 的 beforWaiting 中
// 依照目前手机一秒 60 帧的场景,一帧须要 1 /60 也就是 16.7ms 的工夫来执行代码,主线程不能被卡住超过 16.7ms
// 特地是针对 iOS 15 之后,iPhone 13 Pro Max 帧率能够设置到 120hz
static CFTimeInterval frameMaxAvaibleTime = 0.f;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSInteger maximumFramesPerSecond = 60;
if (@available(iOS 10.3, *)) {maximumFramesPerSecond = [UIScreen mainScreen].maximumFramesPerSecond;
}
frameMaxAvaibleTime = 1.f / maximumFramesPerSecond * 1000.f / 3.f;
});
if (now - _currentLoopEntryTime > frameMaxAvaibleTime) {return;}
BOOL runModeMatched = [[NSRunLoop mainRunLoop].currentMode isEqualToString:(NSString *) self.currentRunMode];
/// MARK: 这里回调,开始构建 VTree
}
- 列表滑动中部分虚构树 VTree
- 部分构建 VTree,能够大大减少构建一次 VTree 的工作量
- 部分构建的前提时,间隔上次构建虚构树,产生变动的 view 都是 ScrollView 或者是 ScrollView 的子 view
- 列表滑动中限流器
6.3 性能相干数据
- 适当的曝光延后,满足数据要求,比方提早 1、2 帧(取决于手机的性能以及以后 CPU 的工作量)
- runloop 最小时长限流器的作用,还保障了延后不会太大,目前应用的 0.1s
- 用 iPhone12 手机,以云音乐首页简单场景举例子,不停地上下滑动,全量 / 部分构建 VTree 别离大略须要 3 -8ms/1-2ms 的样子,CPU 占用 2 -3% 左右(云音乐原来的列表曝光组件占用 10% 左右的 CPU)
- 不会因为 SDK 的存在,引起显著的主线程卡顿或者手机发烫
七、链路追踪
这个是 SDK 的重中之重的性能,指标是将 app 产生的所有埋点 链起来,以帮助数据侧对立一套模型即可剖析漏斗 / 归因数据
7.1 链路追踪 refer 的含意
refer 是一段格式化的字符串,能够通过该字符串,在整个数仓中惟一定位到一个埋点,这就是链路追踪
7.2 如何定义一个埋点
- _sessid: 每次 app 冷启动时生成,格局:
[timestap]#[rand]#[appver]#[buildver]
- _pgstep: 该 app 启动范畴内,每一个 page 曝光,
_pgstep
+1 - _actseq: 该
rootPage
曝光周期内,每一次交互
事件(_pv 也算一次事件),_actseq
+1
通过上述三个参数,即可定位某一次 app 启动 & 一次页面曝光 周期内,哪一次的
交互
事件
7.3 先来看看如何意识一个埋点坑位
- _spm: 埋点的坑位信息,该字符串形容该坑位是什么
-
_scm: 埋点坑位的内容信息,该字符串形容该资源的内容是什么
- 格局:
[cid:ctype:ctraceid:ctrp]
- cid: content id, 该资源的惟一 id
- ctype: content type, 该资源的类型
- ctraceid: content traceid, 接口达到网关时生成,服务端 / 算法 / 举荐应用该字符串做数据逻辑,在后续埋点时关联起来,用来联结剖析举荐 / 算法的成果
- ctrp: 透传的扩大字段,用来在资源维度透传服务端 / 算法 / 举荐的自定义参数
- 格局:
7.3 refer 格局解析
格局:
[_dkey:${keys}][F:${option}][sessid][e/p/xxx][_actseq][_pgstep][spm][scm]
- option: 是一个
位
运算的值,用以形容该 refer 字符串蕴含什么内容 - _dkey: 是对 option 的字符串模式,可读性强(目前仅开发期间才有,不便人工辨认)
- undefine-xpath: 用以标识该 refer 指向的内容是被
降级
了的,随着埋点笼罩越来越全,有该标识的 refer 会越来越少
7.4 refer 的应用
先举一个典型的应用场景
过程解读:
- 点击歌曲 cell,触发了歌曲播放列表的更新,这些歌曲的播放归因(
_addrefer
),就归结到该 cell 的点击埋点 - 同时又跳转了歌曲播放页,该歌曲播放的归因(
_pgrefer
),也归结到了该 cell 的点击
refer 的查找:
- 主动向前查找: 这是绝大部分应用的策略,主动向前在 refer 队列中找到适合的 refer
- undefine-xpath 降级: 如果找到的 refer 生成的工夫,早于最初一次 AOP 捕捉到的
点击事件
工夫,则表明该地位没有埋点,阐明 refer 不可信,则被降级到最初一次rootPage 曝光
所对应的 refer 上 - 准确 refer 查找: 也有多个策略的准确 refer 查找机制,不过应用起来不不便,没有被大范畴应用
7.5 refer 的对立解析
依据下面 refer 的格局,数仓侧梳理出 refer 的格局对立解析,配合埋点治理平台,让规范化的漏斗 / 归因剖析变为可能
7.6 其余 refer 应用场景
- multirefers: 在实时剖析场景,对一些要害埋点,带上了五级 (甚至更多级) 的 refer 数组,间接形容该操作的前五步做了什么(实时剖析要求高,不能做离线数据关联)
- _hsrefer: 一键归因,能够一次性归因到该生产操作来源于 app 级别的哪个场景,比方首页、搜寻页、我的页面等
- _rqrefer: 让客户端埋点跟服务端埋点桥接了起来
7.7 refer 对开发人员通明
- refer 的复杂性: refer 的复杂度很高,实在的 refer 解决比上述形容的还要简单很多,对于一般客户端开发人员,想要残缺了解,老本过于高
-
开发时通明: 对于开发人员来说,就是在对应的节点上减少相应的参数即可
对象维度的三个规范私参(组成了_scm): cid, ctype, ctraceid, ctrp
- 可平台校验: 对象的事件是否参加链路追踪, 参数完整性,等等,都能够在平台做合法性校验,进一步保障了 refer 的正确性
八、H5、RN
- RN: 做了一层桥接,能够在 RN 维度给 view 设置节点,同时设置参数
- 站内 H5: 采纳了半白盒计划,H5 外部部分虚构树,所有埋点通过客户端 SDK 产生,H5 埋点达到 SDK 后,在 native 侧做虚构树交融,从而将站内 H5 跟 native 无缝地连接了起来
九、可视化工具
客户端上传统的埋点都是看不见摸不着的,基于 VTree 的计划是结构化的,能够做到可视化查看埋点的数据,以及如何埋点的,上面是几个工具的截图
十、埋点校验 & 稽查
- 埋点是结构化的,虚构树是在埋点平台治理起来的,埋点的校验,能够做到准确校验,校验出客户端的埋点虚构树是否正确
- 以及每一个对象上埋点的参数是否正确
稽查:
- 在测试包、灰度包中,对产生的所有埋点在平台侧做稽查,并输入稽查报告,在版本公布前,对有问题的埋点问题进行及时的修复,防止上线带来数据问题
十一、落地
该全链路埋点计划,曾经全面在云音乐各个 app 铺开,并且 P0 场景曾经实现数据侧切割,失去了充沛的验证。
十二、将来布局
基于 VTree 能够做十分多的事件,比方:
- 自动化测试: 关键点是对 view 做标识,同时能够应用该标识查问到该 view(基于 VTree 的 UI 自动化测试,曾经落地,前面思考再独自跟大家聊)
- 页面标识: 跨端的对立页面标识能力,用来做各种维度的场景标识
- 基于 VTree 的数据可视化能力: 能够在手机上看整个 app 级别的数据趋势
- 站内 H5 的可视化埋点: 进一步升高 H5 场景的埋点工作量
- refer 能力的主动校验和数据稽查: refer 能力很强,然而出了问题后排查问题,有了相干工具来配合,会让原本对开发人员通明的 refer 能力也能轻松排查
本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!