美團點評前端無痕埋點實踐
構建一個數據平臺大體上包括數據采集、數據上報、數據存儲、數據計算,以及數據的可視化展示等幾個重要的環節。前端數據采集與上報是整個流程中最重要的一環,只有確保前端數據生產的全面、準確、及時,最終產生的數據結果才是可靠的、有價值的。
為了解決前端埋點的準確性、及時性、開發效率等問題,業內各家公司從不同角度,提出了多種技術方案,這些方案大體上可以歸為三類:第一類是代碼埋點,即在需要埋點的節點調用接口直接上傳埋點數據,友盟、百度統計等第三方數據統計服務商大都采用這種方案;第二類是可視化埋點,即通過可視化工具配置采集節點,在前端自動解析配置并上報埋點數據,從而實現所謂的“無痕埋點”, 代表方案是已經開源的Mixpanel;第三類是“無埋點”,它并不是真正的不需要埋點,而是前端自動采集全部事件并上報埋點數據,在后端數據計算時過濾出有用數據,代表方案是國內的GrowingIO。
美團點評對于前端埋點的要求很高,總結起來主要有三點需求:第一是數據的準確性和及時性,數據質量的好壞將直接影響依賴埋點數據的后端策略服務、與合作伙伴結算、以及運營數據報表等等。第二是埋點的效率,埋點的復雜度往往與業務需求相關,埋點效率會影響版本迭代的速度。第三是動態部署與修復埋點的能力,本質上這也是提升埋點效率的一種手段,并且使埋點不再依賴于客戶端發版。
公司原有埋點主要采用手動代碼埋點的方案,代碼埋點雖然使用起來靈活,但是開發成本較高,并且一旦上線就很難修改。如果發生嚴重的數據問題,我們只能通過發熱修復解決。如果直接改進為可視化埋點,開發成本較高,并且也不能解決所有埋點需求;改進為無埋點的話,帶來的流量消耗和數據計算成本也是業務不能接受的。因此,我們在原有代碼埋點方案的基礎上,演化出了一套輕量的、聲明式的前端埋點方案,并且在動態埋點、無痕埋點等方向做了進一步的探索和實踐。
代碼埋點
由于后面要介紹的聲明式埋點和無痕埋點方案仍然依賴原有代碼埋點的底層邏輯,這里有必要簡單介紹下代碼埋點。在實現代碼埋點時,我們主要關注的是數據結構的規范性、埋點接口的易用性、上報策略的可靠性等問題。整體的模塊劃分如下圖所示,這里就不再詳述。
開發者需要手動在需要埋點的節點處(例如:點擊事件的回調方法、列表元素的展示回調方法、頁面的生命周期函數等等)插入這些埋點代碼。
EventInfo eventInfo = new EventInfo();
eventInfo.nm = EventName.MGE; // 事件類型為MGE
eventInfo.val_bid = "xxx"; // 事件的唯一標標識
eventInfo.val_lab = new HashMap<>(); // 攜帶的業務數據
eventInfo.val_lab.put(Constants.Business.xx,"xxx");
Statistics.getChannel("hotel").writeEvent(eventInfo);
可以看出,代碼埋點是一種典型的命令式編程,因此埋點代碼常常要侵入具體的業務邏輯,這使埋點代碼變得很繁瑣并且容易出錯。因此,最直接的做法就是將埋點代碼與業務邏輯解耦,也就是“聲明式編程”,從而降低埋點的難度。
聲明式埋點
聲明式埋點的思路是將埋點代碼和具體的交互和業務邏輯解耦,開發者只用關心需要埋點的控件,并且為這些控件聲明需要的埋點數據即可,從而減輕開發者埋點的成本。
Android
在Android中,我們自定義了常用的UI控件,例如TextView、LinearLayout、ListView、ViewPager等,重寫了事件響應方法,在這些方法內部自動填寫埋點代碼。重寫控件的好處在于可以攔截到更多的事件,執行效率高并且運行穩定。但其弊端也非常明顯——移植成本很高!
為了解決這個問題,我們借鑒了Android support v7庫的思路,即通過AppCompatDelegate代理自動替換UI控件。
public class GAAppCompatDelegateV14 extends AppCompatDelegateImplV14 {
@Override
View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
switch (name) {
case "TextView":
return new NovaTextView(context, attrs);
}
return super.callActivityOnCreateView(parent, name, context, attrs);
}
}
這樣,開發者只需要在自己的Activity基類中重寫getDelegate方法,將方法的返回值替換為修改過的AppCompatDelegate,就可以實現自動替換UI控件了。
@Override
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = GAAppCompatUtil.create(this, this);
}
return mDelegate;
}
然而,新的問題又出現了。
如果引用的第三方庫中重寫了UI控件,上述方法是不生效的,也就是說我們需要一種替換UI控件類的父類方法。可是在運行時,我們沒有找到可行的替換UI控件類的父類方法。因此,我們嘗試在編譯時修改父類,并開發了一個gradle插件。事實上,這樣做并不存在運行時效率的問題,只是會犧牲一些編譯速度。這樣開發者只需要運行這個插件,就可以實現自動將UI控件的父類替換為我們重寫的UI控件了。
apply plugin: 'com.meituan.judasplugin'
采用了聲明式埋點后,只需要在控件初始化時聲明一下需要的埋點就可以了。我們不必再侵入程序的各種響應函數,降低了埋點的難度。
GAHelper.bindClick(view, bid, lab);
iOS
在iOS中,利用Objective-C關聯屬性和類別的語法特性,我們無需重寫UI控件,就能實現聲明式打點。對于UIControl,可以在聲明埋點時添加新的action,并在事件發生時自動填寫埋點代碼。
- (void)nvja_setAnalyticsParams:(NVJAMGEParameter *)params mgeType:(SAKStatisticsEventMGEType)type
{
if (self.wmja_clickParams == nil && type == SAKStatisticsEventClick) {
[self addTarget:self action:@selector(wmja_controlDidTapped:) forControlEvents:UIControlEventTouchUpInside];
}
[super nvja_setAnalyticsParams:params mgeType:type];
}
對于UITableView,可以通過重寫UITableViewDelegate,利用消息傳遞機制攔截事件,并在事件回調方法中自動填寫埋點代碼。
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL selector = [anInvocation selector];
if (self.originalDelegate && [self.originalDelegate respondsToSelector:selector]) {
[anInvocation invokeWithTarget:self.originalDelegate];
}
SEL nvjaSelector = [self nvjaSelector:selector];
if ([super respondsToSelector:nvjaSelector]) {
[anInvocation setSelector:nvjaSelector];
[anInvocation invokeWithTarget:self];
}
}
同樣的,采用了聲明式埋點后,埋點代碼得到了簡化。
NVJAMGEParameter *parameter = [[NVJAMGEParameter alloc] init];
parameter.bid = @"bid";
parameter.lab = @{@"poi_id":@"1"};
button.nvja_clickParams = parameter;
聲明式埋點能夠替代所有的代碼埋點,并且能解決早期遇到的移植成本高等問題。但是其本質上還是一種代碼埋點,只是埋點的代碼減少了,并且不再侵入業務邏輯了。如果要滿足動態部署與修復埋點的需求,就需要徹底消滅寫死在前端的埋點代碼。
無痕埋點
我們注意到,之所以聲明式埋點還需要寫死代碼,主要有兩個原因:第一是需要聲明埋點控件的唯一事件標識,即bid;第二是有的業務字段需要在前端埋點時攜帶,而這些字段是在運行時才可獲知的值。
對于第一點,我們可以嘗試在前后端使用一致的規則自動生成事件標識,這樣后端就可以配置前端的埋點行為,從而做到自動化埋點。對于第二點,可以嘗試通過某種方式將業務數據自動與埋點數據關聯,這種關聯可以發生在前端,也可以發生在后端。
事件標識
為了自動生成事件標識,我們需要獲取每個控件自身的ID、類名以及位于所屬父組件的Index等特征信息,并逐級向上遍歷找到根節點。根節點一般是手動標記的,如果沒有標記則默認是視圖層次樹的頂層節點。最后,將遍歷產生的路徑上所有節點的特征信息組合在一起,就是這個事件的標識。考慮到在實際布局中有可能存在一些動態插入的控件,我們允許父組件的Index有一定的誤差。
配置后臺需要維護自動生成的事件標識和bid的映射關系,并且可以下發給前端一個配置文件。當前端控件事件觸發時,自動和配置文件匹配就可以拿到對應的bid了。需要注意的是,配置后臺維護事件標識的工作可不是一件輕松的事情,主要的復雜性在于不同版本之間布局變更導致的事件標識變更,這就是為什么還需要手動標記根節點的原因。所以,一般我們會選取不易變更的視圖節點。
數據關聯
為了實現業務數據與埋點數據的自動關聯,我們起初嘗試了前后端日志關聯的方式。即在前端請求后端API的時機,由后端將業務數據寫入日志,最后在數據清洗時將相對應的前后端日志合并。這種方式帶來的問題是后端改造成本較高,并且數據清洗的開銷較大,因此并不能廣泛應用。但是在一些特殊場景下,例如某些業務數據只有后端可以獲知,而前端不能獲知時,這種關聯是必要的。
更常見的數據關聯發生在前端數據之間。當頁面跳轉時,通過傳遞規范的跳轉Uri Scheme,將業務數據傳遞給下個頁面,并且自動填入這個頁面的PV事件中。而該頁面內產生的所有其他事件,都會攜帶與PV事件相同的業務數據。
這樣,通過自動產生事件標識并進行數據關聯,我們就能夠實現“無痕埋點”了,并且埋點節點可以通過配置文件動態下發,從而具備了動態部署與修復埋點的能力。但需要注意的是,這種“無痕埋點”并不能解決所有問題,當業務字段無法通過數據關聯獲取時(這種情況比較常見),仍然需要開發者代碼埋點或聲明式埋點指定業務字段。就目前實踐階段的數據來看,業務中大約70%左右的埋點需求可以通過無痕埋點解決,而對于另外30%的埋點需求,仍然需要使用聲明式埋點和代碼埋點。
總結
前端數據采集與上報是構建數據平臺過程中最重要的環節,美團點評前端每天上報的數據達到百億次級別。為了更好的滿足公司各業務日益復雜的埋點需求,以及對埋點準確性、及時性、開發效率的要求,我們在代碼埋點方案的基礎上演化出了一套輕量的、聲明式的前端埋點方案,并且在動態埋點、無痕埋點等方向做了進一步的探索和實踐。目前聲明式埋點已經在部分業務上全量使用,從數據質量和開發者反饋來看,取得了預期的收益。而無痕埋點也正在一些業務上驗證和持續優化中,后面也會在公司范圍內進一步推廣。
在實踐中我們認識到,埋點問題不能通過單一一種技術方案來解決,在不同場景下我們需要選擇不同的埋點方案。例如對于簡單的用戶行為類事件,可以使用無痕埋點解決;而對于需要攜帶大量運行時才可獲知的業務字段的埋點需求,就需要聲明式埋點來解決。從更高的層面來看,除了前端埋點技術的優化,埋點數據的規范化、前后端協同埋點、數據清洗和關聯對于未來構建更加自動化、動態化的埋點體系同樣非常重要。
來自:http://tech.meituan.com/mt-mobile-analytics-practice.html