基於自建 VTree 的全鏈路埋點方案

本文作者: dl

一、背景

在當前移動網際網路時代,一個產品想快速、準確的搶佔市場,無疑是需要產品快速迭代更新,如何協助產品經理對產品當前的資料做出最優判斷是關鍵,這就需要客戶端側提供

高精度

穩定

全鏈路

的埋點資料;做客戶端開發的同學都深刻知道,想要在開發過程中滿足上述三點,開發過程都是頭大的;

針對這個問題,我們自研了一套全鏈路埋點方案,從埋點設計、到客戶端三端(

iOS

Android

H5

)開發、以及埋點校驗&稽查、再到埋點資料使用,目前已經廣泛應用於雲音樂各個主要APP。

二、先聊聊傳統埋點方案的弊端

傳統埋點,就是BI資料人員根據策劃想要的資料,設計出一個個的

單點

的坑位埋點,然後客戶端人員逐個埋進來,這些埋點經常都存在以下特點:

坑位的事件埋點很簡單:點選/雙擊/滑動等明確的事件類埋點,很簡單,根據需求一個一個埋上去即可

資源位曝光埋點是噩夢:在列表/非列表資源的曝光埋點場景,想做到

高精度

(埋點精度提到

99.99%

)難度很大,你有可能每一個曝光埋點都需要考慮如下大部分場景:

基於自建 VTree 的全鏈路埋點方案

每個坑位都是獨立的:坑位之間的埋點沒有關係,需要給每一個坑位

起名字

(比如透過隨機字串,或者組合引數來標識),頁面、列表、元素之間,存在大量的重複引數,以達到資料分析要求

漏斗/歸因分析難:由於每一個坑位埋點都是獨立的,APP使用過程中先後產生的埋點是無關聯的,想要做到漏斗/歸因分析,需要客戶端做

魔鬼引數

傳遞,然後資料分析時再逐個場景的做引數關聯分析

坑位黑盒:想知道一個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

虛擬樹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的方式來內部承接掉,這裡簡單說明下如何做的;

1。UIView: 透過 Method Swizzling 方式來進行對關鍵方法進行hock,當需要給view新增TapGesture時,順便新增一個我們自己的 TapGesture, 這樣我們就可以在點選事件觸發的時候增加點選埋點,關鍵方法如下:

initWithTarget:action:

addTarget:action:

removeTarget:action:

1。對UIView點選事件的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 *> *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

2。UIControl: 透過 Method Swizzling 方式對關鍵方法進行hock,關鍵方法: sendAction:to:forEvent:

對UIcontrol點選事件的hock需要注意業務側添加了多個 Target-Action 事件,不能埋點埋了多次

關鍵程式碼如下:

@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 *actions = @[]。mutableCopy; [self。allTargets enumerateObjectsUsingBlock:^(id _Nonnull obj, BOOL * _Nonnull stop) { NSArray *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

3。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做曝光的:

持續構建VTree: 前面提到,VTree是源源不斷的構建的,每一個view發生了變化,View的新增/刪除/層級變化/位移/大小變動/hidden/alpha,等等(這裡均是AOP方式hock),都會引起重新構建一顆新的VTree

VTree Diff: 先後兩個VTree的diff,就是我們曝光埋點的結果

隨著時間,會源源不斷的生成新的VTree:

基於自建 VTree 的全鏈路埋點方案

遠遠不斷地生成VTree

比如T1時刻生成的VTree:

基於自建 VTree 的全鏈路埋點方案

T1時刻的VTree

T2時刻生成的VTree:

基於自建 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 的全鏈路埋點方案

第一步: 給View設定oid

給物件設定埋點引數

基於自建 VTree 的全鏈路埋點方案

第二步: 給物件設定埋點引數

六、VTree的構建

6。1 VTree構建過程

構建一個VTree,是需要遍歷原始view樹的,構建過程中有如下特點:

一個節點是否可見,跟 view 的 hidden, alpha 有關,並且必須新增到window上

子節點的可見區域小於等於父節點的可見區域

節點的可見區域,可以自定義的

擴大

或者

縮小

, 就像 UIButton 的 contentEdgeInsets 那樣

基於自建 VTree 的全鏈路埋點方案

修改可見區域

節點是可以被遮擋的: 一個page節點可以遮擋父節點名下新增順序早於自己的其他節點

基於自建 VTree 的全鏈路埋點方案

被遮擋了

從虛擬樹上來看,被遮擋的結果:

基於自建 VTree 的全鏈路埋點方案

從虛擬樹上來看,被遮擋的結果

可打破原有view層級關係: 可以手工干預上下層級關係,以做到邏輯掛載的能力

> 事實上,目前提供了三種邏輯掛載能力,這裡簡單提下,不做詳細展開

> 1。 手動邏輯掛載: 指定將 A 掛載到 B 名下

> 2。 自動邏輯掛載: 將 A 掛載到當前 rootPage(當前VTree最下層最右側的page節點) 名下

> 3。 spm形式邏輯掛載: 指定將 A 掛載到

spm

名下(對於解耦特別有用)

虛擬父節點: 可以給多個節點虛擬出一個父節點,對於雙端UI差異時,但是要求同一套埋點結構時,很有用

一個常見的例子,拿雲音樂首頁列表舉例子,每一個模組的title和資源容器(內部可橫向滑動),分別是一個cell;圖中的淺紅色(模組)其實沒有一個UIView與之對應,業務側埋點需要我們提供

模組

維度的曝光資料(但是Android開發過程中,通常都有UI與之對應)

基於自建 VTree 的全鏈路埋點方案

虛擬父節點

精細化埋點:

自定義可見區域 & 遮擋 & 節點的遞迴可見性 結合起來,可以做到精細化埋點效果

針對 tabbar, navbar, 再或者雲音樂app底部的mini播放條等場景引起的列表cell是否曝光的問題,可做到精細化控制

以及配合遮擋能力,真正做到了節點所見及曝光,不可見即曝光結束的效果

6。2 構建過程的效能考慮

view的任何變化,都會引起VTree構建,看上去這是一件很恐怖的事情,因為每一次構建VTree都需要遍歷整顆原始view樹,我們做了如下最佳化來保障效能:

主執行緒runloop空閒的時候構建VTree(而且需要該runloop已經執行的時間,小於等於16。7ms/3,這是拿固定幀率60幀舉例)

runloop構建限流器

基於自建 VTree 的全鏈路埋點方案

主執行緒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 Funcvoid 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

列表滑動中限流器

基於自建 VTree 的全鏈路埋點方案

滾動中構建VTree

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 先來看看如何認識一個埋點坑位

[cid:ctype:ctraceid:ctrp]

7。3 refer格式解析

格式:

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

基於自建 VTree 的全鏈路埋點方案

option解析

undefine-xpath: 用以標識該refer指向的內容是被

降級

了的,隨著埋點覆蓋越來越全,有該標識的refer會越來越少

7。4 refer的使用

先舉一個典型的使用場景

基於自建 VTree 的全鏈路埋點方案

歌曲播放-refer

過程解讀:

_addrefer_pgrefer

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設定節點,同時設定引數

基於自建 VTree 的全鏈路埋點方案

RN橋接

站內H5: 採用了半白盒方案,H5內部區域性虛擬樹,所有埋點透過客戶端SDK產生,H5埋點到達SDK後,在native側做虛擬樹融合,從而將站內H5跟native無縫地銜接了起來

基於自建 VTree 的全鏈路埋點方案

H5半白盒方案

九、視覺化工具

客戶端上傳統的埋點都是看不見摸不著的,基於VTree的方案是結構化的,可以做到視覺化檢視埋點的資料,以及如何埋點的,下面是幾個工具的截圖

基於自建 VTree 的全鏈路埋點方案

視覺化工具-埋點層級結構

基於自建 VTree 的全鏈路埋點方案

視覺化工具-埋點資料

十、埋點校驗&稽查

埋點是結構化的,虛擬樹是在埋點平臺管理起來的,埋點的校驗,可以做到精確校驗,校驗出客戶端的埋點虛擬樹是否正確

以及每一個物件上埋點的引數是否正確

稽查:

在測試包、灰度包中,對產生的所有埋點在平臺側做稽查,並輸出稽查報告,在版本釋出前,對有問題的埋點問題進行及時的修復,避免上線帶來資料問題

十一、落地

該全鏈路埋點方案,已經全面在雲音樂各個app鋪開,並且P0場景已經完成資料側切割,得到了充分的驗證。

十二、未來規劃

基於VTree可以做非常多的事情,比如:

自動化測試: 關鍵點是對view做標識,同時可以使用該標識查詢到該view(基於VTree的UI自動化測試,已經落地,後面考慮再單獨跟大家聊)

頁面標識: 跨端的統一頁面標識能力,用來做各種維度的場景標識

基於VTree的資料視覺化能力: 可以在手機上看整個app級別的資料趨勢

站內H5的視覺化埋點: 進一步降低H5場景的埋點工作量

refer能力的自動校驗和資料稽查: refer能力很強,但是出了問題後排查問題,有了相關工具來配合,會讓本來對開發人員透明的refer能力也能輕鬆排查

作者:雲音樂技術團隊-dl