本文作者: dl

一、布景

  在当时移动互联网年代,一个产品想快速、准确的抢占市场,无疑是需求产品快速迭代更新,怎么协助产品经理对产品当时的数据做出最优判断是要害,这就需求客户端侧供给高精度稳定全链路的埋点数据;做客户端开发的同学都深入知道,想要在开发过程中满足上述三点,开发过程都是头大的;

  针对这个问题,咱们自研了一套全链路埋点计划,从埋点规划、到客户端三端(iOSAndroidH5)开发、以及埋点校验&稽察、再到埋点数据运用,现在现已广泛应用于云音乐各个主要APP。

二、先聊聊传统埋点计划的弊端

  传统埋点,便是BI数据人员依据策划想要的数据,规划出一个个的单点的坑位埋点,然后客户端人员逐个埋进来,这些埋点经常都存在以下特色:

  1. 坑位的事情埋点很简略:点击/双击/滑动等明确的事情类埋点,很简略,依据需求一个一个埋上去即可
  2. 资源位曝光埋点是噩梦:在列表/非列表资源的曝光埋点场景,想做到高精度(埋点精度提到 99.99%)难度很大,你有或许每一个曝光埋点都需求考虑如下大部分场景:
    基于自建 VTree 的全链路埋点方案
  3. 每个坑位都是独立的:坑位之间的埋点没有联系,需求给每一个坑位起名字(比方经过随机字符串,或许组合参数来标识),页面、列表、元素之间,存在许多的重复参数,以到达数据剖析要求
  4. 漏斗/归因剖析难:由于每一个坑位埋点都是独立的,APP运用过程中先后发生的埋点是无相关的,想要做到漏斗/归因剖析,需求客户端做魔鬼参数传递,然后数据剖析时再逐个场景的做参数相关剖析
  5. 坑位黑盒:想知道一个app有多少坑位埋点,当时页面下现已闪现出了多少坑位,坑位之间是什么联系,办理本钱高

三、咱们从前做过的一些尝试

3.1 无痕埋点

  市道上有许多人介绍无痕埋点,咱们从前也做过相似的尝试;这种无痕,主要是针对一些坑位事情(比方点击、双击、滑动等事情)埋点做主动生成埋点,一同附带上生成的xpath(依据view层级生成),然后把埋点上报到数据渠道后,再将xpath赋予实在的事务意义,然后能够进行数据剖析;

  可是这个计划的问题是只能处理一些简略事情场景,而且数据渠道做xpath相关是一件噩梦,作业量大,最主要的是不稳定,关于埋点数据高精度场景,这个计划不行行(没有哪个客户端开发人员天天花费许多时刻查找 xpath 是什么意义,以及跟着迭代事务的开发,xpath由于不受操控的改变带来的数据问题带来的排查作业量是巨大的)。

  特别关于资源位的曝光上,想要做到实在的无痕,主动埋点,是不太可行的;比方列表场景,底层是不知道一个cell是什么资源的,乃至都也不知道是不是一个资源。

四、咱们的计划

4.1 目标

目标是咱们计划埋点办理和开发的基本单位,给一个UIView设置 _oid(目标Id: Object Id),该view便是一个目标; 目标分为两大类,page & element;

基于自建 VTree 的全链路埋点方案

  • page目标: 比方 UIViewController.view, WebView, 或许一个半屏浮层的view,再或许一个事务弹窗
  • element目标: 比方 UIButton, UICollectionViewCell, 或许一个自界说view
  • 目标参数: 目标是埋点具体信息的承载体,承载着目标维度的具体埋点参数
  • 目标的复用: 目标的存在,其间一个很大的原因,便是需求做复用,关于一些通用UI组件,尤为适宜

4.2 虚拟树(VTree)

目标不是孤立存在的,而是以**虚拟树(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": "数字,毫秒单位"
}
  1. _eventcode: 埋点的类型,比方元素点击(_ec), 元素曝光开端(_ev), 元素曝光完毕(_ed), 页面曝光开端(_pv), 页面曝光完毕(_pd) 等等
  2. _elist: 从当时元素节点开端,向上一切元素节点的集合,是一个数组,倒叙
  3. _plist: 从当时节点开端,向上一切页面结点的即可,是一个数组,倒叙
  4. _spm: 上面现已介绍(SPM),能够仅有定位该坑位

从上面的数据结构能够看出,数据结构是结构化的,坑位不是独立的,存在层级联系的

5.2 点击事情

大部分的点击事情,都发生在如下四个场景上:

  1. UIView上添加的TapGesture单击手势
  2. UIControl的子类添加的TouchUpInside单击事情
  3. UITableViewCell的 didSelectedRowAtIndexPath 单击事情
  4. UICollectionViewCell的 didSelectedItemAtIndexPath 单击事情

关于上述四种场景,咱们采用了AOP的办法来内部接受掉,这儿简略阐明下怎么做的;

  1. UIView: 经过 Method Swizzling 办法来进行对要害办法进行hock,当需求给view添加TapGesture时,顺便添加一个咱们自己的 TapGesture, 这样咱们就能够在点击事情触发的时分添加点击埋点,要害办法如下:
    1. initWithTarget:action:
    2. addTarget:action:
    3. removeTarget:action:
  1. 对UIView点击事情的hock留意需求做到跟着事务侧事情的添加/删去而一同添加/删去
  2. 一同,咱们做到了在 一切事务侧点击事情触发之前(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
  1. 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
  1. UITableViewCell: 先对 setDelegate: 进行hock,然后以 NSProxy 的办法将 Original Delegate 进行 封装,组成 Delegate Chain 的办法,然后在 DelegateProxy 内部做消息分发,然后能够彻底掌控点击事情
  1. 该 Delegate Chain 的办法能够hock的不支撑 点击事情,能够hock一切 Delegate 的办法
  2. 相同,也支撑 pre & after 两个维度的hock
  3. 特别留意: 需求做到实在的 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做曝光的:

  1. 继续构建VTree: 前面提到,VTree是连绵不断的构建的,每一个view发生了改变,View的添加/删去/层级改变/位移/巨细改变/hidden/alpha,等等(这儿均是AOP办法hock),都会引起从头构建一颗新的VTree
  2. VTree Diff: 先后两个VTree的diff,便是咱们曝光埋点的成果

跟着时刻,会连绵不断的生成新的VTree:

基于自建 VTree 的全链路埋点方案

比方T1时刻生成的VTree:

基于自建 VTree 的全链路埋点方案

T2时刻生成的VTree:

基于自建 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的曝光战略,得出如下:

  1. 这种战略,彻底抹平了列表和非列表
  2. 曝光机遇问题,转而变成了何时构建VTree问题上
  3. 资源是否曝光的问题, 转而变成了VTree中节点的可见性问题上

5.4 埋点开发过程

  根据VTree的埋点,不管是点击、滑动等事情埋点,还是元素、页面的曝光埋点,转化成了如下两个开发过程:

  1. 给View设置oid => 成为目标 (构建VTree)

基于自建 VTree 的全链路埋点方案

  1. 给目标设置埋点参数

基于自建 VTree 的全链路埋点方案

六、VTree的构建

6.1 VTree构建过程

  构建一个VTree,是需求遍历原始view树的,构建过程中有如下特色:

  1. 一个节点是否可见,跟 view 的 hidden, alpha 有关,而且必须添加到window上
  2. 子节点的可见区域小于等于父节点的可见区域
  3. 节点的可见区域,能够自界说的 扩大 或许 缩小, 就像 UIButton 的 contentEdgeInsets 那样

基于自建 VTree 的全链路埋点方案

  1. 节点是能够被遮挡的: 一个page节点能够遮挡父节点名下添加次序早于自己的其他节点

基于自建 VTree 的全链路埋点方案

从虚拟树上来看,被遮挡的成果:

基于自建 VTree 的全链路埋点方案

  1. 可打破原有view层级联系: 能够手工干预上下层级联系,以做到逻辑挂载的才能

    事实上,现在供给了三种逻辑挂载才能,这儿简略提下,不做具体展开

    1. 手动逻辑挂载: 指定将 A 挂载到 B 名下
    2. 主动逻辑挂载: 将 A 挂载到当时 rootPage(当时VTree最下层最右侧的page节点) 名下
    3. spm办法逻辑挂载: 指定将 A 挂载到 spm 名下(关于解耦特别有用)
  2. 虚拟父节点: 能够给多个节点虚拟出一个父节点,关于双端UI差异时,可是要求同一套埋点结构时,很有用

一个常见的比方,拿云音乐主页列表举比方,每一个模块的title和资源容器(内部可横向滑动),别离是一个cell;图中的浅赤色(模块)其实没有一个UIView与之对应,事务侧埋点需求咱们供给 模块 维度的曝光数据(可是Android开发过程中,通常都有UI与之对应)

基于自建 VTree 的全链路埋点方案

精细化埋点:

  1. 自界说可见区域 & 遮挡 & 节点的递归可见性 结合起来,能够做到精细化埋点效果
  2. 针对 tabbar, navbar, 再或许云音乐app底部的mini播映条等场景引起的列表cell是否曝光的问题,可做到精细化操控
  3. 以及合作遮挡才能,实在做到了节点所见及曝光,不行见即曝光完毕的效果

6.2 构建过程的功能考虑

view的任何改变,都会引起VTree构建,看上去这是一件很恐惧的事情,由于每一次构建VTree都需求遍历整颗原始view树,咱们做了如下优化来保证功能:

  1. 主线程runloop空闲的时分构建VTree(而且需求该runloop现已运转的时刻,小于等于16.7ms/3,这是拿固定帧率60帧举例)
  2. runloop构建限流器

基于自建 VTree 的全链路埋点方案

要害代码如下:

    /// 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
}
  1. 列表滑动中部分虚拟树VTree
  1. 部分构建VTree,能够大大削减构建一次VTree的作业量
  2. 部分构建的前提时,距离上次构建虚拟树,发生改变的view都是ScrollView或许是ScrollView的子view
  1. 列表滑动中限流器

基于自建 VTree 的全链路埋点方案

6.3 功能相关数据

  1. 适当的曝光拖延,满足数据要求,比方推迟1、2帧(取决于手机的功能以及当时CPU的作业量)
  2. runloop最小时长限流器的效果,还保证了拖延不会太大,现在运用的0.1s
  3. 用iPhone12手机,以云音乐主页杂乱场景举比方,不停地上下滑动,全量/部分构建VTree别离大约需求3-8ms/1-2ms的样子,CPU占用2-3%左右(云音乐原来的列表曝光组件占用10%左右的CPU)
  4. 不会由于SDK的存在,引起明显的主线程卡顿或许手机发烫

七、链路追寻

这个是SDK的重中之重的功能,目标是将app发生的一切埋点起来,以协助数据侧一致一套模型即可剖析漏斗/归因数据

7.1 链路追寻 refer 的含义

refer是一段格局化的字符串,能够经过该字符串,在整个数仓中仅有定位到一个埋点,这便是链路追寻

7.2 怎么界说一个埋点

  1. _sessid: 每次app冷发动时生成,格局: [timestap]#[rand]#[appver]#[buildver]
  2. _pgstep: 该app发动范围内,每一个page曝光,_pgstep +1
  3. _actseq: 该 rootPage 曝光周期内,每一次 交互 事情(_pv也算一次事情),_actseq +1

经过上述三个参数,即可定位某一次app发动 & 一次页面曝光 周期内,哪一次的 交互 事情

7.3 先来看看怎么知道一个埋点坑位

  1. _spm: 埋点的坑位信息,该字符串描绘该坑位是什么
  2. _scm: 埋点坑位的内容信息,该字符串描绘该资源的内容是什么
    1. 格局: [cid:ctype:ctraceid:ctrp]
    2. cid: content id, 该资源的仅有id
    3. ctype: content type, 该资源的类型
    4. ctraceid: content traceid, 接口到达网关时生成,服务端/算法/引荐运用该字符串做数据逻辑,在后续埋点时相关起来,用来联合剖析引荐/算法的效果
    5. ctrp: 透传的扩展字段,用来在资源维度透传服务端/算法/引荐的自界说参数

7.3 refer格局解析

格局: [_dkey:${keys}][F:${option}][sessid][e/p/xxx][_actseq][_pgstep][spm][scm]

  1. option: 是一个运算的值,用以描绘该refer字符串包含什么内容
  2. _dkey: 是对option的字符串办法,可读性强(现在仅开发期间才有,便利人工辨认)

基于自建 VTree 的全链路埋点方案

  1. undefine-xpath: 用以标识该refer指向的内容是被 降级 了的,跟着埋点覆盖越来越全,有该标识的refer会越来越少

7.4 refer的运用

先举一个典型的运用场景

基于自建 VTree 的全链路埋点方案

过程解读:

  1. 点击歌曲cell,触发了歌曲播映列表的更新,这些歌曲的播映归因(_addrefer),就归结到该cell的点击埋点
  2. 一同又跳转了歌曲播映页,该歌曲播映的归因(_pgrefer),也归结到了该cell的点击

refer的查找:

  1. 主动向前查找: 这是绝大部分运用的战略,主动向前在refer行列中找到适宜的refer
  2. undefine-xpath降级: 如果找到的refer生成的时刻,早于最终一次AOP捕获到的点击事情时刻,则标明该方位没有埋点,阐明refer不行信,则被降级到最终一次 rootPage曝光 所对应的refer上
  3. 准确refer查找: 也有多个战略的准确refer查找机制,不过运用起来不便利,没有被大范围运用

7.5 refer的一致解析

依据上面refer的格局,数仓侧梳理出refer的格局一致解析,合作埋点办理渠道,让标准化的漏斗/归因剖析变为或许

7.6 其他refer运用场景

  1. multirefers: 在实时剖析场景,对一些要害埋点,带上了五级(乃至更多级)的refer数组,直接描绘该操作的前五步做了什么(实时剖析要求高,不能做离线数据相关)
  2. _hsrefer: 一键归因,能够一次性归因到该消费操作来源于app级别的哪个场景,比方主页、查找页、我的页面等
  3. _rqrefer: 让客户端埋点跟服务端埋点桥接了起来

7.7 refer对开发人员通明

  1. refer的杂乱性: refer的杂乱度很高,实在的refer处理比上述描绘的还要杂乱许多,关于一般客户端开发人员,想要完好了解,本钱过于高
  2. 开发时通明: 关于开发人员来说,便是在对应的节点上添加相应的参数即可

目标维度的三个标准私参(组成了_scm): cid, ctype, ctraceid, ctrp

  1. 可渠道校验: 目标的事情是否参加链路追寻, 参数完好性,等等,都能够在渠道做合法性校验,进一步保证了refer的正确性

八、H5、RN

  • RN: 做了一层桥接,能够在RN维度给view设置节点,一同设置参数

基于自建 VTree 的全链路埋点方案

  • 站内H5: 采用了半白盒计划,H5内部部分虚拟树,一切埋点经过客户端SDK发生,H5埋点到达SDK后,在native侧做虚拟树交融,然后将站内H5跟native无缝地衔接了起来

基于自建 VTree 的全链路埋点方案

九、可视化东西

客户端上传统的埋点都是看不见摸不着的,根据VTree的计划是结构化的,能够做到可视化查看埋点的数据,以及怎么埋点的,下面是几个东西的截图

基于自建 VTree 的全链路埋点方案
基于自建 VTree 的全链路埋点方案

十、埋点校验&稽察

  • 埋点是结构化的,虚拟树是在埋点渠道办理起来的,埋点的校验,能够做到准确校验,校验出客户端的埋点虚拟树是否正确
  • 以及每一个目标上埋点的参数是否正确

稽察:

  • 在测验包、灰度包中,对发生的一切埋点在渠道侧做稽察,并输出稽察报告,在版本发布前,对有问题的埋点问题进行及时的修正,避免上线带来数据问题

十一、落地

该全链路埋点计划,现已全面在云音乐各个app铺开,而且P0场景现已完成数据侧切割,得到了充沛的验证。

十二、未来规划

根据VTree能够做十分多的事情,比方:

  1. 主动化测验: 要害点是对view做标识,一同能够运用该标识查询到该view(根据VTree的UI主动化测验,现已落地,后面考虑再独自跟大家聊)
  2. 页面标识: 跨端的一致页面标识才能,用来做各种维度的场景标识
  3. 根据VTree的数据可视化才能: 能够在手机上看整个app级别的数据趋势
  4. 站内H5的可视化埋点: 进一步下降H5场景的埋点作业量
  5. refer才能的主动校验和数据稽察: refer才能很强,可是出了问题后排查问题,有了相关东西来合作,会让原本对开发人员通明的refer才能也能轻松排查

本文发布自网易云音乐技术团队,文章未经授权禁止任何办法的转载。咱们终年接收各类技术岗位,如果你预备换作业,又刚好喜爱云音乐,那就加入咱们 grp.music-fe(at)corp.netease.com!