構建一個數據平臺,大致上包括數據採集、數據上報、數據存儲、數據計算以及數據可視化展現等幾個重要的環節。其中,數據採集與上報是整個流程中重要的一環,只有確保前端數據生產的全面、準確、及時,最終產生的數據結果纔是可靠的、有價值的。

爲了解決前端埋點的準確性、及時性、開發效率等問題,業內各家公司從不一樣角度,提出了多種技術方案,這些方案大致上能夠歸爲三類:

  1. 第一類是代碼埋點,即在須要埋點的節點調用接口直接上傳埋點數據,友盟、百度統計等第三方數據統計服務商大都採用這種方案;

  2. 第二類是可視化埋點,即經過可視化工具配置採集節點,在前端自動解析配置並上報埋點數據,從而實現所謂的「無痕埋點」, 表明方案是已經開源的Mixpanel

  3. 第三類是「無埋點」,它並非真正的不須要埋點,而是前端自動採集所有事件並上報埋點數據,在後端數據計算時過濾出有用數據,表明方案是國內的GrowingIO。

美團點評對於前端埋點的要求很高,總結起來主要有三點需求:

  1. 數據的準確性和及時性,數據質量的好壞將直接影響依賴埋點數據的後端策略服務、與合做夥伴結算、以及運營數據報表等等。

  2. 埋點的效率,埋點的複雜度每每與業務需求相關,埋點效率會影響版本迭代的速度。

  3. 動態部署與修復埋點的能力,本質上這也是提高埋點效率的一種手段,而且使埋點再也不依賴於客戶端發版。

公司原有埋點主要採用手動代碼埋點的方案,代碼埋點雖然使用起來靈活,可是開發成本較高,而且一旦上線就很難修改。若是發生嚴重的數據問題,咱們只能經過發熱修復解決。若是直接改進爲可視化埋點,開發成本較高,而且也不能解決全部埋點需求;改進爲無埋點的話,帶來的流量消耗和數據計算成本也是業務不能接受的。所以,咱們在原有代碼埋點方案的基礎上,演化出了一套輕量的、聲明式的前端埋點方案,而且在動態埋點、無痕埋點等方向作了進一步的探索和實踐。

代碼埋點

因爲後面要介紹的聲明式埋點和無痕埋點方案仍然依賴原有代碼埋點的底層邏輯,這裏有必要簡單介紹下代碼埋點。在實現代碼埋點時,咱們主要關注的是數據結構的規範性、埋點接口的易用性、上報策略的可靠性等問題。總體的模塊劃分以下圖所示。

開發者須要手動在須要埋點的節點處(例如:點擊事件的回調方法、列表元素的展現回調方法、頁面的生命週期函數等等)插入這些埋點代碼。

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 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%的埋點需求,仍然須要使用聲明式埋點和代碼埋點。

總結

前端數據採集與上報是構建數據平臺過程當中最重要的環節,美團點評前端天天上報的數據達到百億次級別。爲了更好的知足公司各業務日益複雜的埋點需求,以及對埋點準確性、及時性、開發效率的要求,咱們在代碼埋點方案的基礎上演化出了一套輕量的、聲明式的前端埋點方案,而且在動態埋點、無痕埋點等方向作了進一步的探索和實踐。目前聲明式埋點已經在部分業務上全量使用,從數據質量和開發者反饋來看,取得了預期的收益。而無痕埋點也正在一些業務上驗證和持續優化中,後面也會在公司範圍內進一步推廣。

在實踐中咱們認識到,埋點問題不能經過單一一種技術方案來解決,在不一樣場景下咱們須要選擇不一樣的埋點方案。例如對於簡單的用戶行爲類事件,可使用無痕埋點解決;而對於須要攜帶大量運行時纔可獲知的業務字段的埋點需求,就須要聲明式埋點來解決。從更高的層面來看,除了前端埋點技術的優化,埋點數據的規範化、先後端協同埋點、數據清洗和關聯對於將來構建更加自動化、動態化的埋點體系一樣很是重要。