1. 程式人生 > 其它 >美團點評前端無痕埋點實踐

美團點評前端無痕埋點實踐

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

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

  • 第一類是程式碼埋點,即在需要埋點的節點呼叫介面直接上傳埋點資料,友盟、百度統計等第三方資料統計服務商大都採用這種方案;
  • 第二類是視覺化埋點,即通過視覺化工具配置採集節點,在前端自動解析配置並上報埋點資料,從而實現所謂的“無痕埋點”,代表方案是已經開源的
    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 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%的埋點需求,仍然需要使用宣告式埋點和程式碼埋點。

總結

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

在實踐中我們認識到,埋點問題不能通過單一一種技術方案來解決,在不同場景下我們需要選擇不同的埋點方案。例如對於簡單的使用者行為類事件,可以使用無痕埋點解決;而對於需要攜帶大量執行時才可獲知的業務欄位的埋點需求,就需要宣告式埋點來解決。從更高的層面來看,除了前端埋點技術的優化,埋點資料的規範化、前後端協同埋點、資料清洗和關聯對於未來構建更加自動化、動態化的埋點體系同樣非常重要。