基於自建 VTree 的全鏈路埋點方案
本文作者: 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), 埋點時間等一些基本資訊之外,還得有業務埋點引數,以及能體現出物件上下級的結構
先來看下一個普通埋點的格式:
json
{
"_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, 這樣我們就可以在點選事件觸發的時候增加點選埋點,關鍵方法如下: 1. initWithTarget:action: 2. addTarget:action: 3. removeTarget:action:
- 對UIView點選事件的hock注意需要做到隨著業務側事件的增加/刪除而一起增加/刪除
- 同時,我們做到了在 所有業務側點選事件觸發之前(pre) & 所有業務側點選事件觸發之後(after) 兩個維度的hock
關鍵程式碼如下: ```objc @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
@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
關鍵程式碼如下:
```objc
@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
// 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等
關鍵示例程式碼幾個重要的相關方法 (程式碼較多不再展示,三方有多個庫均可以借鑑):
objc
- (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層級關係: 可以手工干預上下層級關係,以做到邏輯掛載的能力
> 事實上,目前提供了三種邏輯掛載能力,這裡簡單提下,不做詳細展開
> 1. 手動邏輯掛載: 指定將 A 掛載到 B 名下
> 2. 自動邏輯掛載: 將 A 掛載到當前 rootPage(當前VTree最下層最右側的page節點) 名下
> 3. 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構建限流器
關鍵程式碼如下: ```objc /// 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的使用
先舉一個典型的使用場景
過程解讀:
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使用場景
- 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可以做非常多的事情,比如: 1. 自動化測試: 關鍵點是對view做標識,同時可以使用該標識查詢到該view(基於VTree的UI自動化測試,已經落地,後面考慮再單獨跟大家聊) 2. 頁面標識: 跨端的統一頁面標識能力,用來做各種維度的場景標識 3. 基於VTree的資料視覺化能力: 可以在手機上看整個app級別的資料趨勢 4. 站內H5的視覺化埋點: 進一步降低H5場景的埋點工作量 5. refer能力的自動校驗和資料稽查: refer能力很強,但是出了問題後排查問題,有了相關工具來配合,會讓本來對開發人員透明的refer能力也能輕鬆排查
本文釋出自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!
- 不一樣的Android堆疊抓取方案
- 雲音樂 Swift 混編 Module 化實踐
- iOS雲音樂APM效能監控實踐
- 雲音樂iOS端程式碼靜態檢測實踐
- 網易雲音樂全面開源一款雲原生應用部署平臺:Horizon
- dex 優化編年史
- 如何實現 iOS 16 帶來的 Depth Effect 圖片效果
- 雲音樂 iOS 跨端快取庫 - NEMichelinCache
- 雲音樂 Android 記憶體監控探索篇
- Android APP 出海實踐
- Android 除錯實戰與原理詳解
- 社交場景下iOS訊息流互動層實踐
- 你構建的程式碼為什麼這麼大
- 扒一扒 Jetpack Compose 實現原理
- Recoil 狀態管理方案的淺入淺出
- 基於自建 VTree 的全鏈路埋點方案
- 雲音樂 iOS 啟動效能優化「開荒篇」
- 雲音樂播放頁直播推薦實戰
- 雲音樂iOS端網路圖片下載優化實踐
- 雲音樂 iOS 啟動效能優化「開荒篇」