关于ios:基于自建-VTree-的全链路埋点方案

7次阅读

共计 14003 个字符,预计需要花费 36 分钟才能阅读完成。

本文作者:dl

一、背景

  在以后挪动互联网时代,一个产品想疾速、精确的抢占市场,无疑是须要产品疾速迭代更新,如何帮助产品经理对产品以后的数据做出最优判断是要害,这就须要客户端侧提供 高精度 稳固 全链路 的埋点数据;做客户端开发的同学都粗浅晓得,想要在开发过程中满足上述三点,开发过程都是头大的;

  针对这个问题,咱们自研了一套全链路埋点计划,从埋点设计、到客户端三端 (iOSAndroidH5) 开发、以及埋点校验 & 稽查、再到埋点数据应用,目前曾经广泛应用于云音乐各个次要 APP。

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

  传统埋点,就是 BI 数据人员依据策动想要的数据,设计出一个个的 单点 的坑位埋点,而后客户端人员一一埋进来,这些埋点常常都存在以下特点:

  1. 坑位的事件埋点很简略:点击 / 双击 / 滑动等明确的事件类埋点,很简略,依据需要一个一个埋下来即可
  2. 资源位曝光埋点是噩梦:在列表 / 非列表资源的曝光埋点场景,想做到 高精度 (埋点精度提到 99.99%) 难度很大,你有可能每一个曝光埋点都须要思考如下大部分场景:
  3. 每个坑位都是独立的:坑位之间的埋点没有关系,须要给每一个坑位 起名字(比方通过随机字符串,或者组合参数来标识),页面、列表、元素之间,存在大量的反复参数,以达到数据分析要求
  4. 漏斗 / 归因剖析难:因为每一个坑位埋点都是独立的,APP 应用过程中先后产生的埋点是无关联的,想要做到漏斗 / 归因剖析,须要客户端做 魔鬼参数 传递,而后数据分析时再一一场景的做参数关联剖析
  5. 坑位黑盒:想晓得一个 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": "数字,毫秒单位"
}
  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:

比方 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 的曝光策略,得出如下:

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

5.4 埋点开发步骤

  基于 VTree 的埋点,不论是点击、滑动等事件埋点,还是元素、页面的曝光埋点,转化成了如下两个开发步骤:

  1. 给 View 设置 oid => 成为对象 (构建 VTree)
  1. 给对象设置埋点参数

六、VTree 的构建

6.1 VTree 构建过程

  构建一个 VTree,是须要遍历原始 view 树的,构建过程中有如下特点:

  1. 一个节点是否可见,跟 view 的 hidden, alpha 无关,并且必须增加到 window 上
  2. 子节点的可见区域小于等于父节点的可见区域
  3. 节点的可见区域,能够自定义的 扩充 或者 放大, 就像 UIButton 的 contentEdgeInsets 那样
  1. 节点是能够被遮挡的: 一个 page 节点能够遮挡父节点名下增加程序早于本人的其余节点

从虚构树上来看,被遮挡的后果:

  1. 可突破原有 view 层级关系: 能够手工干涉高低层级关系,以做到逻辑挂载的能力

    事实上,目前提供了三种逻辑挂载能力,这里简略提下,不做具体开展

    1. 手动逻辑挂载: 指定将 A 挂载到 B 名下
    2. 主动逻辑挂载: 将 A 挂载到以后 rootPage(以后 VTree 最上层最右侧的 page 节点)名下
    3. spm 形式逻辑挂载: 指定将 A 挂载到 spm 名下(对于解耦特地有用)
  2. 虚构父节点: 能够给多个节点虚构出一个父节点,对于双端 UI 差别时,然而要求同一套埋点构造时,很有用

一个常见的例子,拿云音乐首页列表举例子,每一个模块的 title 和资源容器 (外部可横向滑动),别离是一个 cell;图中的浅红色(模块) 其实没有一个 UIView 与之对应,业务侧埋点须要咱们提供 模块 维度的曝光数据(然而 Android 开发过程中,通常都有 UI 与之对应)

精细化埋点:

  1. 自定义可见区域 & 遮挡 & 节点的递归可见性 联合起来,能够做到精细化埋点成果
  2. 针对 tabbar, navbar, 再或者云音乐 app 底部的 mini 播放条等场景引起的列表 cell 是否曝光的问题,可做到精细化管制
  3. 以及配合遮挡能力,真正做到了节点所见及曝光,不可见即曝光完结的成果

6.2 构建过程的性能思考

view 的任何变动,都会引起 VTree 构建,看上去这是一件很恐怖的事件,因为每一次构建 VTree 都须要遍历整颗原始 view 树,咱们做了如下优化来保障性能:

  1. 主线程 runloop 闲暇的时候构建 VTree(而且须要该 runloop 曾经运行的工夫,小于等于 16.7ms/3,这是拿固定帧率 60 帧举例)
  2. 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
}
  1. 列表滑动中部分虚构树 VTree
  1. 部分构建 VTree,能够大大减少构建一次 VTree 的工作量
  2. 部分构建的前提时,间隔上次构建虚构树,产生变动的 view 都是 ScrollView 或者是 ScrollView 的子 view
  1. 列表滑动中限流器

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 的字符串模式,可读性强(目前仅开发期间才有,不便人工辨认)
  1. undefine-xpath: 用以标识该 refer 指向的内容是被 降级 了的,随着埋点笼罩越来越全,有该标识的 refer 会越来越少

7.4 refer 的应用

先举一个典型的应用场景

过程解读:

  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

  3. 可平台校验: 对象的事件是否参加链路追踪, 参数完整性,等等,都能够在平台做合法性校验,进一步保障了 refer 的正确性

八、H5、RN

  • RN: 做了一层桥接,能够在 RN 维度给 view 设置节点,同时设置参数
  • 站内 H5: 采纳了半白盒计划,H5 外部部分虚构树,所有埋点通过客户端 SDK 产生,H5 埋点达到 SDK 后,在 native 侧做虚构树交融,从而将站内 H5 跟 native 无缝地连接了起来

九、可视化工具

客户端上传统的埋点都是看不见摸不着的,基于 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!

正文完
 0