西瓜客戶端埋點實踐:基於責任鏈的埋點框架

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"埋點的背景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前互聯網\/軟件行業內,廣泛使用數據驅動產品迭代,通過精細的數據分析、模型訓練爲用戶提供更好的服務。在此過程中,數據埋點的工作是後續數據分析、模型訓練等工作的基礎。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"數據埋點通常是產品經理、數據分析師,以及推薦系統工程師,基於業務需求(例如:廣告的下載安裝轉化),產品需求(例如:關注按鈕的曝光次數以及點擊的人數)對用戶行爲的每一個事件確定埋點需求。客戶端工程師進行對應的埋點功能開發,通過 SDK 上報埋點的數據結果,後端記錄數據進行一系列處理,並彙總後提供給產品經理、數據分析師,以及推薦系統工程師進行數據分析或模型訓練,幫助優化產品運營策略。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"經典的消費場景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面有幾種經典的數據消費場景:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/41\/41c153e9d1cd59bf69eb0364b6241d1d.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以看到,行爲分析埋點,需要包括某一事件發生時的前因、後果,以及事件發生對象的特徵。在複雜的數據分析、模型訓練等需求中,不僅僅需要獲知某個事件的發生次數,對埋點上下文尤爲關注。此處上下文指的通常有 2 類,分別是:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事件發生的頁面信息和頁面位置信息"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用戶經過怎樣的路徑來到當前頁面,也就是“來源”信息"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"經典的埋點需求"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們結合具體場景,看 1 個簡單的埋點需求,“點擊收藏”事件"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/2a\/2ab801fff675ee66222c8a5d843dc97e.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面左圖是西瓜放映廳的推薦列表,右圖是某個影片的詳情頁,點擊推薦列表的影片卡片,會跳轉到詳情頁。作爲最常見的消費場景,列表和詳情都有收藏按鈕,我們希望知道每一個收藏事件發生的場景,方便後續優化收藏功能,以及結合用戶收藏的情況,優化推薦模型。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"埋點需求是上報收藏按鈕的點擊事件 click_favorite,要求包含收藏影片的信息,所在的場景信息等。"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"如果收藏事件發生在列表頁,會上報如下的內容"}]}]}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"{\n  \"event\": \"click_favorite\",\n  \"params\": {\n    \"video_id\": \"123\", \/\/ 影片ID\n    \"video_type\": 2, \/\/ 影片類型\n    \"page_name\": \"feed\", \/\/ 當前頁面\n    \"tab_name\": \"long_video\" \/\/ 當前所在的底Tab\n    \"channel_name\": \"lvideo_recommend\", \/\/ 當前所在的頻道\n  }\n}\n"}]},{"type":"numberedlist","attrs":{"start":"2","normalizeStart":"2"},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"如果收藏事件發生在詳情頁,會上報如下的內容"}]}]}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"{\n  \"event\": \"click_favorite\",\n  \"params\": {\n    \"video_id\": \"123\", \/\/ 影片ID\n    \"video_type\": 2, \/\/ 影片類型\n    \"page_name\": \"detail\", \/\/ 當前頁面\n    \"from_page\": \"feed\", \/\/ 來源頁面\n    \"from_tab_name\": \"long_video\" \/\/ 來源底Tab\n    \"from_channel_name\": \"lvideo_recommend\", \/\/ 來源頻道\n  }\n}"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"現有方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前端用戶交互的界面,通常有複雜的頁面層級關係和跳轉邏輯,爲了準確記錄埋點信息,滿足上述埋點需求,主要有以下幾種實現方案。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"直接傳參"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過平臺支持的參數傳遞方式,逐個定義並且讀寫參數;或者基於面向對象程序設計,對每個類添加相應的埋點參數,在類對象的關係中進行埋點參數傳遞。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於上面的埋點需求 click_favorite,我們假設列表頁和詳情頁的層級結構是:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"列表頁:CinemaTabFragment(放映廳 Tab)=> VideoChannelFragment(頻道)=> VideoViewHolder(卡片)"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"詳情頁:VideoDetailActivity(詳情頁 Activity) -> BottomActionBar(底部操作欄)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然實際情況因爲項目的組件抽象複用等原因,往往會有更復雜的層級。直接傳參要怎麼實現這個埋點需求呢:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"列表頁的 click_favorite 埋點,需要從底 Tab 把所在 Tab 信息傳給頻道,頻道再把底 Tab 和頻道信息傳給卡片"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"class CinemaTabFragment {\n fun getItem() {\n fragment = VideoChannelFragment()\n \/\/ 配置頻道所處的底Tab\n fragment.tabName = \"long_video\"\n return fragment\n }\n}\n\nclass VideoChannelFragment {\n var tabName\n var channelName\n\n fun onBindViewHolder(position) {\n holder.videoInfo = items.get(position)\n \/\/ 配置卡片的tabName和channelName\n holder.tabName = this.tabName\n holder.channelName = this.channelName\n }\n}\n\nclass VideoViewHolder {\n var tabName\n var channelName\n var videoInfo\n\n fun clickFavorite() {\n \/\/ 上報埋點的時候,拼接參數\n LogSdk.onEvent(\"click_favorite\", mapOf(\n \"tab_name\" to this.tabName,\n \"channel_name\" to this.channelName,\n \"video_id\" to this.videoInfo.id,\n \"video_type\" to this.videoInfo.type,\n \"page_name\" to \"feed\"\n ))\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"2","normalizeStart":"2"},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"詳情頁的 click_favorite 埋點,首先需要在列表頁點擊卡片跳轉的時候,把上下文信息通過跳轉參數傳遞給詳情頁,然後詳情頁解析出參數,傳給底部操作欄"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"class VideoViewHolder {\n var tabName\n var channelName\n var videoInfo\n\n fun clickJumpDetail() {\n intent.putExtra(\"from_tab_name\", this.tabName)\n intent.putExtra(\"from_channel_name\", this.channelName)\n intent.putExtra(\"from_page\", \"feed\")\n intent.putExtra(\"video_id\", this.videoInfo.id)\n startActivity(intent)\n }\n}\n\nclass VideoDetailActivity {\n \/\/ 詳情頁還有其他埋點需要報這幾個參數,先緩存下來\n var fromTabName\n var fromChannelName\n var fromPage\n\n var videoInfo\n\n fun onCreate() {\n \/\/ 詳情頁還有其他埋點需要報這幾個參數,緩存在變量裏\n fromTabName = intent.getString(\"from_tab_name\")\n fromChannelName = intent.getString(\"from_channel_name\")\n fromPage = intent.getString(\"from_page\")\n\n val videoId = intent.getString(\"video_id\")\n videoInfo = loadVideoInfo(videoId)\n\n \/\/ 設置參數到底部操作組件\n bottomActionBar.fromTabName = fromTabName\n bottomActionBar.fromChannelName = fromChannelName\n bottomActionBar.fromPage = fromPage\n bottomActionBar.videoInfo = videoInfo\n }\n}\n\nclass BottomActionBar {\n var fromTabName\n var fromChannelName\n var fromPage\n var videoInfo\n\n fun clickFavorite() {\n \/\/ 上報埋點的時候,拼接參數\n LogSdk.onEvent(\"click_favorite\", mapOf(\n \"from_tab_name\" to this.fromTabName,\n \"from_channel_name\" to this.fromChannelName,\n \"from_page\" to this.fromPage,\n \"video_id\" to this.videoInfo.id,\n \"video_type\" to this.videoInfo.type,\n \"page_name\" to \"detail\"\n ))\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏是簡化過的僞代碼,即便是這樣,依然可以看出直接傳參有非常顯著的缺陷:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每增加一個參數,都需要寫大量的重複代碼,工程代碼膨脹"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模塊間約定了很多埋點參數的協議,耦合程度高,難以維護"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一些場景的嵌套層次深,經過很多層的參數傳遞,非常容易漏報埋點參數"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"單例傳參"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述問題有一種輕微緩解的辦法,使用單例來進行埋點參數的訪問。通過一個單例進行埋點參數的維護,由於單例提供了全局唯一訪問入口,程序中的任何位置都能方便地讀和寫埋點參數。這種方式帶來的好處是不需要在每個類都定義大量的埋點參數,只需要訪問單例進行修改和讀取。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以詳情頁的 click_favorite 埋點舉例,可以通過跳轉前把值寫入單例,上報埋點時直接從單例獲取,而無須再從詳情頁 Activity 傳值給底部操作欄。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"object VideoDetailTracker {\n var fromTabName\n var fromChannelName\n var fromPage\n var videoInfo\n}\n\nclass VideoViewHolder {\n var tabName\n var channelName\n var videoInfo\n\n fun clickJumpDetail() {\n \/\/ 把上下文信息先存到單例\n VideoDetailTracker.fromTabName = this.tabName\n VideoDetailTracker.fromChannelName = this.channelName\n VideoDetailTracker.fromPage = \"feed\"\n VideoDetailTracker.videoInfo = this.videoInfo\n startActivity(intent)\n }\n}\n\nclass VideoDetailActivity {\n\n fun onCreate() {\n \/\/ 詳情頁不需要再解析埋點參數,也不需要再傳遞給BottomActionBar\n \/\/ 只需有正常的功能代碼\n val videoId = intent.getString(\"video_id\")\n videoInfo = loadVideoInfo(videoId)\n }\n}\n\nclass BottomActionBar {\n\n fun clickFavorite() {\n \/\/ 上報埋點的時候,直接從單例取出來拼接參數\n LogSdk.onEvent(\"click_favorite\", mapOf(\n \"from_tab_name\" to VideoDetailTracker.fromTabName,\n \"from_channel_name\" to VideoDetailTracker.fromChannelName,\n \"from_page\" to VideoDetailTracker.fromPage,\n \"video_id\" to VideoDetailTracker.videoInfo.id,\n \"video_type\" to VideoDetailTracker.videoInfo.type,\n \"page_name\" to \"detail\"\n ))\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看出來,從列表頁 => 詳情頁以後,在詳情頁上報埋點,獲取頁面來源信息,確實比之前更簡單了。但仔細想想,這種方案治標不治本,同樣有明顯的弊端:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,無法解決列表頁這種多實例場景的問題,比如一個推薦列表中有多個卡片,每個卡片的埋點參數都不一樣,卡片的埋點參數還是需要自己傳"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單例的數據可能被多個位置寫入,且一旦被覆蓋就沒法恢復,比如這樣的路徑:列表 -> 詳情頁 1 -> 相關推薦 -> 詳情頁 2,進到詳情頁 2 以後,單例的數據被覆蓋了,這時候再回到詳情頁 1,獲取到的埋點參數實際是詳情頁 2 的,導致埋點參數上報錯誤。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"存放和清理的時機難以控制,清理早了會導致埋點參數缺失,忘記清理可能導致後面的埋點獲取不到參數"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"無埋點"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"無埋點是業界流行的一種埋點方案,所謂的“無埋點”、“全埋點”,是指埋點 SDK 通過編譯時插樁、運行時反射或動態代理的方式,自動進行埋點事件的觸發和上報,無須客戶端工程師手動進行埋點開發工作。由產品經理、數據分析師等在埋點管理後臺,使用 XPath 路徑、頁面視圖 id 或者文本匹配等技術,定位到頁面視圖的位置,過濾出所需的數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此方案的優勢很明顯,客戶端只需要一次性的接入,理論上能夠蒐集到所有頁面、視圖的曝光、點擊等事件,無需客戶端同學進行後續的埋點需求開發。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有這麼好的事?爲什麼字節沒有廣泛使用?此方案的缺陷在於:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"僅能上報有限的簡單事件類型,如頁面視圖曝光、點擊等,無法完成複雜事件的上報,如一次支付行爲的操作路徑、結果、錯誤信息等"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"無法自定義參數,主要指跳轉的來源、所處的場景等上下文信息,無法滿足複雜的數據分析和推薦模型所需的數據要求"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"由產品經理、數據分析師等在埋點管理後臺進行的事件錄入,把複雜度從開發轉嫁給了產品,消費成本較高"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"對頁面視圖的穩定性有很高的要求,需要約定 id、文本、視圖的層級,保持頁面結構不變,如果客戶端工程師因爲一些新需求開發、性能優化等調整了視圖結構,將會導致已錄入的埋點失效,增加了額外的維護成本"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"全場景的數據上報,可能產生大量的無用數據,消耗大量傳輸、存儲、計算資源"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"基於責任鏈的埋點框架"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面介紹下我們正在使用的埋點框架,是怎麼解決埋點傳參困難、代碼冗餘的問題,簡化埋點開發複雜度的。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"分析問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"回顧下剛剛的埋點需求,上報 click_favorite 埋點,複雜度在於上報埋點的對象(列表卡片、詳情頁底部操作欄),爲了埋點需要從其他對象(頻道、底 Tab、前面的頁面)獲取埋點參數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"卡片需要關注自己所在的底 Tab、頻道,詳情頁需要關注自己的來源頁面,這顯然違反了“關注點分離”的原則。如果我們讓每個對象僅關注自己的信息,是否可行?"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"埋點與視圖層級的關係"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們回想下列表頁的視圖層級"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/77\/777be1019b5b1d0130eb62d8ffb5b4d0.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"是不是會發現所需的埋點參數恰好就分佈在視圖樹的責任鏈中?"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/c0\/c0cc76156c08d9c92278f2b80a707c98.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"沒錯,聰明的你已經想到了,我們在收藏按鈕被點擊時,只需要從收藏按鈕的節點按照"},{"type":"codeinline","content":[{"type":"text","text":"卡片 -> 推薦頻道 -> 放映廳Tab"}]},{"type":"text","text":"的順序向上找,就能夠拿到所有需要的參數了。既然這個上下級關係(責任鏈)已經客觀存在,我們爲什麼還需要層層透傳埋點,直接利用這個關係不就好了嗎?"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"埋點與頁面跳轉鏈路的關係"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來源類埋點參數定義,常見的有 from_page、click_position 等,需要在跳轉的過程中,從前序頁面,傳遞到後序頁面,同時會有些映射規則,比如前序頁面的 page_name 到了後序頁面,上報 from_page。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼頁面的跳轉鏈路是什麼樣的呢?我們回想下,跳轉到詳情頁,有很多種路徑,比如下面的 2 種:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/74\/74fb5e045724fabd595ef1cfbf9e7624.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面是直接從推薦列表頁進詳情頁:推薦列表 => 詳情頁"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面是從推薦列表頁,點擊標籤進入選集頁,再從選集頁進入詳情頁:推薦列表 => 選集 => 詳情頁"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看出頁面的跳轉鏈路,邏輯上也是一個樹狀結構。如果我們結合前面說到的頁面內視圖層級,把兩個樹放在一起,會是下面的樣子:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/9a\/9a3a69e1b9f7ccc6b491154b06643f5c.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"是不是發現,我們需要的埋點上下文參數,理論上都可以通過節點的關係找到?"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"解決問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了前面的討論,我們來看一下怎麼把這個問題,抽象成一個框架。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"1. ITrackModel"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ITrackModel 很簡單,這個接口定義了能夠填充埋點參數的對象,只要實現了這個接口,就可以在埋點上報的時候添加參數"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"interface ITrackModel {\n fun fillTrackParams(trackParams: TrackParams)\n}\n"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2. ITrackNode"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ITrackModel 只是定義了填充參數的職責,ITrackModel 對象之間並沒有關聯,怎麼找到所有的 ITrackModel,讓它們填充自己的埋點參數呢?在此基礎上,我們定義了 ITrackNode 接口"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"interface ITrackNode: ITrackModel {\n fun parentTrackNode(): ITrackNode?\n fun referrerTrackNode(): ITrackNode?\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ITrackModel 繼承了 ITrackModel,除了有填充埋點參數的能力外,還會指向父節點和來源節點。"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"parentTrackNode:指向父節點,通過它可以建立一個頁面內的責任鏈,在一個頁面內,根節點通常是頁面的頂層容器,例如 Android 的 Activity"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"referrerTrackNode:指向來源節點,通過它可以建立用戶跳轉的邏輯鏈路,在用戶使用 App 的一個會話中,來源鏈路根節點通常指啓動頁面(也可以由 Push、DeepLink 的啓動參數構造虛擬的 referrer 節點)"}]}]}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3. 建立頁面上下級責任鏈"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"定義了 ITrackModel 和 ITrackNode,接下來就是實現每個節點,並且將這些節點連起來。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最通用的方式,是直接實現 ITrackNode,例如在列表場景中,我們可以建立 ViewHolder -> Adapter -> Fragment 的責任鏈"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\/\/ 放映廳Tab\nclass CinemaTabFragment: ITrackNode {\n\n override fun parentTrackNode(): ITrackNode {\n return activity as ITrackNode\n }\n\n override fun fillTrackParams(trackParams: TrackParams) {\n trackParams.putIfNull(\"tab_name\", \"long_video\")\n }\n}\n\n\/\/ 頻道Fragment\nclass VideoChannelFragment: ITrackNode {\n override fun parentTrackNode(): ITrackNode {\n return parentFragment as ITrackNode\n }\n\n override fun fillTrackParams(trackParams: TrackParams) {\n trackParams.putIfNull(\"channel_name\", \"lvideo_recommend\")\n trackParams.putIfNull(\"page_name\", \"feed\")\n }\n}\n\n\/\/ 列表Adapter,這一層沒有參數,只是作爲中間節點,連接卡片ViewHolder和頻道Fragment\nclass VideoChannelAdapter(private val parent: ITrackNode): ITrackNode {\n override fun parentTrackNode(): ITrackNode {\n return fragment as ITrackNode\n }\n}\n\n\/\/ 卡片ViewHolder\nclass VideoViewHolder(private val parent: ITrackNode, val view: View) : ITrackNode {\n\n var videoInfo\n\n override fun parentTrackNode(): ITrackNode {\n return parentFragment as ITrackNode\n }\n\n override fun fillTrackParams(trackParams: TrackParams) {\n trackParams.putIfNull(\"video_id\", videoInfo.id)\n trackParams.putIfNull(\"video_type\", videoInfo.type)\n }\n\n fun clickFavorite() {\n \/\/ 使用ITrackNode.onEvent上報埋點,會從當前節點開始向上收集埋點參數\n this.onEvent(\"click_favorite\")\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣,我們就建立起了列表頁的上下級責任鏈,我們可以看到埋點參數都在對應的節點添加了,而不需要再從上級層層傳入,上報埋點的代碼變得非常簡單。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"直接實現 ITrackNode 的方式,特別適合 Fragment、Adapter、ViewHolder 等需要我們自定義的類,他們在視圖構建中的作用是將視圖拆分層級,更好的管理局部的視圖、數據和邏輯。然而,我們發現這種方式需要實現每一個節點,並且手動建立節點之間的聯繫,使用起來還是挺麻煩的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大部分情況下,我們發現上下級責任鏈的關係,和視圖層級的關係是一致的,而系統已經爲我們建立了視圖樹 ViewTree,那麼我們可以利用 ViewTree,來建立上下級責任鏈。其中 ViewTree 上的每一個 View,只需要實現 ITrackModel 的能力,就可以負責填充埋點參數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們利用 View.setTag 可以存放任意對象的特性,爲 View 增加了擴展屬性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\/**\n * 設置View的TrackModel\n *\/\nvar View.trackModel: ITrackModel?\n get() = this.getTag(TAG_ID_TRACK_MODEL) as? ITrackModel\n set(value) {\n this.setTag(TAG_ID_TRACK_MODEL, value)\n }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的例子,可以換一種實現方式:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\/\/ 放映廳Tab\nclass CinemaTabFragment: ITrackModel {\n\n override fun fillTrackParams(trackParams: TrackParams) {\n trackParams.putIfNull(\"tab_name\", \"long_video\")\n }\n\n override fun onViewCreated(view: View) {\n \/\/ 放映廳Tab根視圖,ITrackModel由Fragment實現\n view.trackModel = this\n }\n}\n\n\/\/ 頻道Fragment\nclass VideoChannelFragment: ITrackModel {\n\n override fun fillTrackParams(trackParams: TrackParams) {\n trackParams.putIfNull(\"channel_name\", \"lvideo_recommend\")\n trackParams.putIfNull(\"page_name\", \"feed\")\n }\n\n override fun onViewCreated(view: View) {\n \/\/ 頻道根視圖,ITrackModel由Fragment實現\n view.trackModel = this\n }\n}\n\n\/\/ 卡片ViewHolder\nclass VideoViewHolder(val view: View) : ITrackModel {\n\n var videoInfo\n\n fun bind(videoInfo: VideoInfo) {\n this.videoInfo = videoInfo\n this.itemView.trackModel = this\n }\n\n override fun fillTrackParams(trackParams: TrackParams) {\n trackParams.putIfNull(\"video_id\", videoInfo.id)\n trackParams.putIfNull(\"video_type\", videoInfo.type)\n }\n\n fun clickFavorite() {\n \/\/ 使用View.trackEvent上報埋點,會從當前View開始向上收集埋點參數\n itemView.trackEvent(\"click_favorite\")\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到,利用 ViewTree,爲 View 添加 trackModel 的方式,不需要再實現 ITrackNode,手動建立上下級關係。由於 ViewTree 的存在,即便是層級很深的子視圖,也可以直接作爲埋點節點來使用,而不需要再經過中間的橋接節點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"直接在自定義類實現 ITrackNode,和爲 View 添加 ITrackModel,這兩種方式可以組合在一起使用。理想的頁面上下級鏈路是這樣的,實現 ITrackNode 作爲上層節點,更加方便組織邏輯關係複雜的子視圖,如首頁頻道等;層級較深的節點直接利用 ViewTree,方便向上搜索責任鏈。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/cc\/ccf0d82ba8404f4a6bd22882ff3fe9b4.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"4. 建立頁面來源責任鏈"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"建立來源責任鏈的建立,指的是頁面跳轉過程中,將跳轉前的節點\/上下文參數傳遞給跳轉後的頁面,作爲後者的來源節點(referrerTrackNode)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們利用跳轉 Intent 攜帶來源節點信息:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\/\/ 設置當前跳轉的來源節點\nclass VideoViewHolder {\n fun clickJumpDetail() {\n \/\/ 設置跳轉的來源節點是當前節點\n intent.setReferrerTrackNode(this)\n startActivity(intent)\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在跳轉後的頁面,只需要從 intent 再取出來就可以了"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"class VideoDetailActivity {\n override fun referrerTrackNode(): ITrackNode {\n return intent.getReferrerTrackNode()\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"值得注意的是,邏輯上應該直接使用當前節點的引用作爲下個頁面的 referrerTrackNode,但實際使用中,可能會有內存泄漏、鏈路過於複雜的問題,所以在 setReferrerTrackNode 的時候,我們製作了當前節點的快照,把當前節點的上下文參數都添加進了 Map,傳遞給下個頁面的實際上是這個快照節點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"完成了來源節點的傳遞,在下個頁面怎麼使用呢?最簡單的是直接把來源節點的所有參數,添加進埋點中,但我們的埋點需求常常會需要一些轉換規則,比如:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上個頁面的 category_name,跳轉後上報 parent_category_name"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上個頁面的 page_name,跳轉後上報 from_page"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此我們定義了 IPageTrackNode,用來做頁面級別的埋點處理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"interface IPageTrackNode: ITrackNode {\n fun referrerKeyMap(): Map\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常會由頁面的 Activity 實現 IPageTrackNode"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"class VideoDetailActivity: IPageTrackNode {\n\n var videoInfo\n\n \/\/ 定義來源參數映射\n override fun referrerKeyMap(): Map {\n return mapOf(\n \"page_name\" to \"from_page\",\n \"channel_name\" to \"from_channel_name\",\n \"tab_name\" to \"from_tab_name\"\n )\n }\n\n override fun fillTrackParams(trackParams: TrackParams) {\n trackParams.putIfNull(\"video_id\", videoInfo.id)\n trackParams.putIfNull(\"video_type\", videoInfo.type)\n trackParams.putIfNull(\"page_name\", \"detail\")\n }\n}\n\nclass BottomActionBar {\n\n fun clickFavorite() {\n \/\/ 上報埋點的時候,直接從當前節點往上收集埋點參數\n trackEvent(\"click_favorite\")\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到這樣一來,在詳情頁上報 click_favorite 埋點也變得簡單了。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"5. 埋點參數的收集"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面說明了如何建立頁面上下級責任鏈和來源責任鏈。上報埋點的時候,按照下面的流程,順着責任鏈收集埋點參數:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/d3\/d3aef07542ae81165d341d84197c1ebb.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"6. 埋點線索:TrackThread"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"按照前面的內容,我們已經可以建立用戶使用整個 App 的過程中,所有上下文的責任鏈關係,理論上可以上報任意需要的上下文參數。然而實際業務的埋點需求中,還有一類更復雜的場景,需要在多個節點\/頁面間共享埋點參數。例如西瓜視頻創作過程的埋點:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"tab_name:進入創作場景的來源,在一次創作過程中,所有埋點都需要帶上這個信息"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"is_record\/is_cut:是否使用過拍攝、剪輯功能,可能在創作過程中發生變化,在創作過程的任意節點上,需要讀寫這些參數"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以前這類埋點基本會通過單例來維護,單例的話就會遇到前面“單例傳參”部分講到的問題,而我們發現在整個頁面上下級和來源責任鏈都已經建立的情況下,頁面的之間的關聯不就可以方便地共享參數嗎?在一個打開的頁面上添加參數,並且共享到後續的頁面,參數的生命週期和頁面的生命週期綁定,用戶離開這個頁面後自動消失,不用擔心清除和覆蓋的問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此我們引入埋點線索(TrackThread)的定義,任意起始節點都可以初始化一個 TrackThread,TrackThread 上能夠存放各種類型的 TrackModel,在後續的所有關聯節點中,都能夠通過已經建立的責任鏈,訪問到 Thread 進行讀寫。通過任意節點上報埋點,可以指定需要添加哪些 TrackModel 的埋點參數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\/\/實現ITrackModel接口\nclass RecordInfo : ITrackModel {\n var isRecord = false\n\n override fun fillTrackParams(params: TrackParams) {\n params.put(\"is_record\", isRecord.toYesOrNo())\n }\n}\n\n\/\/ 在某個合適的時機,比如進入拍攝頁面,開啓埋點thread,添加TrackModel\nnode.startTrackThread().putTrackModel(RecordInfo())\n\n\/\/ 任意節點上更新thread\nnode.trackThread?.getTrackModel(RecordInfo::class.java).isRecord = true\n\n\/\/ 上報埋點\nview.newTrackEvent(\"click_publish\") \/\/ 通過newTrackEvent創建Event實例\n .with(RecordInfo::class.java) \/\/ 聲明需要上報TrackThread中的RecordInfo\n .emit() \/\/ 最終計算並上報埋點\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"埋點線索適合用於具有會話特性的流程中,方便在流程中共享參數,常見的還有登錄、註冊的流程,訂單創建流程等。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此,我們基於責任鏈的埋點開發框架已經差不多介紹完了,從上面的內容可以看出,這個框架更多是約定了一套責任鏈的協議,通過責任鏈的存在,方便埋點參數的收集上報。當然我們爲了方便使用,也使用到很多語言特性來簡化框架的 API,比如通過接口默認實現,Fragment 的父節點默認指向 Activity,通過擴展函數,讓 View 可以直接添加 TrackModel 和上報埋點,但這些都不影響協議原本的內涵。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總結下這個方案的優勢和問題:"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"優勢"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"埋點需求對原有的功能代碼侵入性小,只需實現 ITrackNode 接口建立鏈路,或者直接使用 ViewTree 的責任鏈,就可以達到跨層級傳參的目的,傳參複雜度大大降低,減少代碼量和人力成本"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"各個節點不再需要關注其他節點的埋點參數,只需要負責填充自己的參數即可,做到“關注點分離”,同時還能夠讓每個節點的參數得到複用,無需反覆添加同樣的參數,理想情況下,一個場景的一個新增參數,只需在相關節點上添加一處,即可做到所有的子節點都能收集到"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"問題"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"理解成本,就像前端同學使用 React 一樣,不可避免地有一些學習成本;同時還需要和數據分析師約定好埋點規範,只有在良好的規範下,埋點框架才能發揮更好的作用"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"無法通過類、參數定義等方式,強制約束埋點參數列表,新來的開發同學做新需求時,可能不知道該傳哪些參數"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"重構現有埋點的建議"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一些場景已經有大量埋點邏輯,無法短時間全部改掉。主要原因是因爲埋點往往沒有“邊界”:埋點需要大量的“上下文”與“來源”參數,而我們已經在實踐中發現,這些參數是埋點錯綜複雜的主要原因。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"舉 2 個例子:進入個人主頁埋點(enter_pgc)和點擊關注埋點(rt_follow),開發面臨的情況是進入個人主頁的入口有 100+處,關注組件被引用的場景有 58 處。短時間要修改全部來源參數和上下文參數的傳遞方式,開發和測試成本很大,一次迭代基本不可能完成。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此我們建議弄清埋點的“邊界”,在此基礎上控制每一次重構的影響範圍。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"重構通用業務組件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通用業務組件指的是像“關注按鈕”或者“關注操作”這種有特定業務邏輯、大量使用於各種業務場景的下沉組件。這類組件往往有特定的埋點要求,觸發埋點本身不是很複雜的事情,複雜的是怎麼獲取到觸發事件時的上下文信息。對於此類組件,建議:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"保留新舊兩種埋點傳參的接口"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在老接口中對埋點參數進行封裝,轉發到新接口執行實際的邏輯,同時對老接口標註@Deprecated"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"未來逐步替換到新接口上"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"重構一個頁面的結構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"重構一個頁面時,期望可以把各個層級定義成 TrackNode 節點,構建完整的責任鏈。但是一個頁面結構中,常常使用了大量其他模塊的組件或者功能,而依賴的這些模塊還沒完成埋點重構。因此重構時依賴到其他模塊\/組件的情況,建議:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對當前模塊內的部分構建責任鏈"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不改變其他模塊的接口,涉及到這些模塊埋點參數傳遞的,在傳參的節點上,獲取節點的埋點參數,使用老的方式傳參"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"重構一個頁面的來源傳參方式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個頁面的來源參數,外部可能通過很多種方式,如 Intent、單例等傳遞過來。爲了讓頁面內責任鏈上的每個節點,都能夠獲取到來源的參數,同時兼容外部新老傳參的方式,建議:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當前頁面改造成新的獲取來源參數的方式,同時支持按照老的傳參方式讀取來源參數,需要時可在頁面初始化的時機 Mock 一個 referrerTrackNode"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"外部跳轉逐步切換到新的來源參數傳參方式"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文轉載自:字節跳動技術團隊(ID:toutiaotechblog)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/iMn--4FNugtH26G90N1MaQ","title":"xxx","type":null},"content":[{"type":"text","text":"西瓜客戶端埋點實踐:基於責任鏈的埋點框架"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章