<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          西瓜客戶端埋點(diǎn)實(shí)踐:基于責(zé)任鏈的埋點(diǎn)框架

          共 24691字,需瀏覽 50分鐘

           ·

          2021-07-17 03:47


          埋點(diǎn)的背景

          目前互聯(lián)網(wǎng)/軟件行業(yè)內(nèi),廣泛使用數(shù)據(jù)驅(qū)動產(chǎn)品迭代,通過精細(xì)的數(shù)據(jù)分析、模型訓(xùn)練為用戶提供更好的服務(wù)。在此過程中,數(shù)據(jù)埋點(diǎn)的工作是后續(xù)數(shù)據(jù)分析、模型訓(xùn)練等工作的基礎(chǔ)。

          數(shù)據(jù)埋點(diǎn)通常是產(chǎn)品經(jīng)理、數(shù)據(jù)分析師,以及推薦系統(tǒng)工程師,基于業(yè)務(wù)需求(例如:廣告的下載安裝轉(zhuǎn)化),產(chǎn)品需求(例如:關(guān)注按鈕的曝光次數(shù)以及點(diǎn)擊的人數(shù))對用戶行為的每一個事件確定埋點(diǎn)需求。客戶端工程師進(jìn)行對應(yīng)的埋點(diǎn)功能開發(fā),通過 SDK 上報(bào)埋點(diǎn)的數(shù)據(jù)結(jié)果,后端記錄數(shù)據(jù)進(jìn)行一系列處理,并匯總后提供給產(chǎn)品經(jīng)理、數(shù)據(jù)分析師,以及推薦系統(tǒng)工程師進(jìn)行數(shù)據(jù)分析或模型訓(xùn)練,幫助優(yōu)化產(chǎn)品運(yùn)營策略。

          經(jīng)典的消費(fèi)場景

          下面有幾種經(jīng)典的數(shù)據(jù)消費(fèi)場景:

          我們可以看到,行為分析埋點(diǎn),需要包括某一事件發(fā)生時的前因、后果,以及事件發(fā)生對象的特征。在復(fù)雜的數(shù)據(jù)分析、模型訓(xùn)練等需求中,不僅僅需要獲知某個事件的發(fā)生次數(shù),對埋點(diǎn)上下文尤為關(guān)注。此處上下文指的通常有 2 類,分別是:

          • 事件發(fā)生的頁面信息和頁面位置信息
          • 用戶經(jīng)過怎樣的路徑來到當(dāng)前頁面,也就是“來源”信息

          經(jīng)典的埋點(diǎn)需求

          下面我們結(jié)合具體場景,看 1 個簡單的埋點(diǎn)需求,“點(diǎn)擊收藏”事件

          上面左圖是西瓜放映廳的推薦列表,右圖是某個影片的詳情頁,點(diǎn)擊推薦列表的影片卡片,會跳轉(zhuǎn)到詳情頁。作為最常見的消費(fèi)場景,列表和詳情都有收藏按鈕,我們希望知道每一個收藏事件發(fā)生的場景,方便后續(xù)優(yōu)化收藏功能,以及結(jié)合用戶收藏的情況,優(yōu)化推薦模型。

          埋點(diǎn)需求是上報(bào)收藏按鈕的點(diǎn)擊事件 click_favorite,要求包含收藏影片的信息,所在的場景信息等。

          1. 如果收藏事件發(fā)生在列表頁,會上報(bào)如下的內(nèi)容
          {
            "event""click_favorite",
            "params": {
              "video_id""123", // 影片ID
              "video_type": 2, // 影片類型
              "page_name""feed", // 當(dāng)前頁面
              "tab_name""long_video" // 當(dāng)前所在的底Tab
              "channel_name""lvideo_recommend", // 當(dāng)前所在的頻道
            }
          }
          1. 如果收藏事件發(fā)生在詳情頁,會上報(bào)如下的內(nèi)容
          {
            "event""click_favorite",
            "params": {
              "video_id""123", // 影片ID
              "video_type": 2, // 影片類型
              "page_name""detail", // 當(dāng)前頁面
              "from_page""feed", // 來源頁面
              "from_tab_name""long_video" // 來源底Tab
              "from_channel_name""lvideo_recommend", // 來源頻道
            }
          }

          現(xiàn)有方案

          前端用戶交互的界面,通常有復(fù)雜的頁面層級關(guān)系和跳轉(zhuǎn)邏輯,為了準(zhǔn)確記錄埋點(diǎn)信息,滿足上述埋點(diǎn)需求,主要有以下幾種實(shí)現(xiàn)方案。

          直接傳參

          通過平臺支持的參數(shù)傳遞方式,逐個定義并且讀寫參數(shù);或者基于面向?qū)ο蟪绦蛟O(shè)計(jì),對每個類添加相應(yīng)的埋點(diǎn)參數(shù),在類對象的關(guān)系中進(jìn)行埋點(diǎn)參數(shù)傳遞。

          對于上面的埋點(diǎn)需求 click_favorite,我們假設(shè)列表頁和詳情頁的層級結(jié)構(gòu)是:

          • 列表頁:CinemaTabFragment(放映廳 Tab)=> VideoChannelFragment(頻道)=> VideoViewHolder(卡片)
          • 詳情頁:VideoDetailActivity(詳情頁 Activity) -> BottomActionBar(底部操作欄)

          當(dāng)然實(shí)際情況因?yàn)轫?xiàng)目的組件抽象復(fù)用等原因,往往會有更復(fù)雜的層級。直接傳參要怎么實(shí)現(xiàn)這個埋點(diǎn)需求呢:

          1. 列表頁的 click_favorite 埋點(diǎn),需要從底 Tab 把所在 Tab 信息傳給頻道,頻道再把底 Tab 和頻道信息傳給卡片
          class CinemaTabFragment {
              fun getItem() {
                  fragment = VideoChannelFragment()
                  // 配置頻道所處的底Tab
                  fragment.tabName = "long_video"
                  return fragment
              }
          }

          class VideoChannelFragment {
              var tabName
              var channelName

              fun onBindViewHolder(position) {
                  holder.videoInfo = items.get(position)
                  // 配置卡片的tabName和channelName
                  holder.tabName = this.tabName
                  holder.channelName = this.channelName
              }
          }

          class VideoViewHolder {
              var tabName
              var channelName
              var videoInfo

              fun clickFavorite() {
                  // 上報(bào)埋點(diǎn)的時候,拼接參數(shù)
                  LogSdk.onEvent("click_favorite", mapOf(
                      "tab_name" to this.tabName,
                      "channel_name" to this.channelName,
                      "video_id" to this.videoInfo.id,
                      "video_type" to this.videoInfo.type,
                      "page_name" to "feed"
                  ))
              }
          }
          1. 詳情頁的 click_favorite 埋點(diǎn),首先需要在列表頁點(diǎn)擊卡片跳轉(zhuǎn)的時候,把上下文信息通過跳轉(zhuǎn)參數(shù)傳遞給詳情頁,然后詳情頁解析出參數(shù),傳給底部操作欄
          class VideoViewHolder {
              var tabName
              var channelName
              var videoInfo

              fun clickJumpDetail() {
                  intent.putExtra("from_tab_name", this.tabName)
                  intent.putExtra("from_channel_name", this.channelName)
                  intent.putExtra("from_page""feed")
                  intent.putExtra("video_id", this.videoInfo.id)
                  startActivity(intent)
              }
          }

          class VideoDetailActivity {
              // 詳情頁還有其他埋點(diǎn)需要報(bào)這幾個參數(shù),先緩存下來
              var fromTabName
              var fromChannelName
              var fromPage

              var videoInfo

              fun onCreate() {
                  // 詳情頁還有其他埋點(diǎn)需要報(bào)這幾個參數(shù),緩存在變量里
                  fromTabName = intent.getString("from_tab_name")
                  fromChannelName = intent.getString("from_channel_name")
                  fromPage = intent.getString("from_page")

                  val videoId = intent.getString("video_id")
                  videoInfo = loadVideoInfo(videoId)

                  // 設(shè)置參數(shù)到底部操作組件
                  bottomActionBar.fromTabName = fromTabName
                  bottomActionBar.fromChannelName = fromChannelName
                  bottomActionBar.fromPage = fromPage
                  bottomActionBar.videoInfo = videoInfo
              }
          }

          class BottomActionBar {
              var fromTabName
              var fromChannelName
              var fromPage
              var videoInfo

              fun clickFavorite() {
                  // 上報(bào)埋點(diǎn)的時候,拼接參數(shù)
                  LogSdk.onEvent("click_favorite", mapOf(
                      "from_tab_name" to this.fromTabName,
                      "from_channel_name" to this.fromChannelName,
                      "from_page" to this.fromPage,
                      "video_id" to this.videoInfo.id,
                      "video_type" to this.videoInfo.type,
                      "page_name" to "detail"
                  ))
              }
          }

          這里是簡化過的偽代碼,即便是這樣,依然可以看出直接傳參有非常顯著的缺陷:

          • 每增加一個參數(shù),都需要寫大量的重復(fù)代碼,工程代碼膨脹
          • 模塊間約定了很多埋點(diǎn)參數(shù)的協(xié)議,耦合程度高,難以維護(hù)
          • 一些場景的嵌套層次深,經(jīng)過很多層的參數(shù)傳遞,非常容易漏報(bào)埋點(diǎn)參數(shù)

          單例傳參

          上述問題有一種輕微緩解的辦法,使用單例來進(jìn)行埋點(diǎn)參數(shù)的訪問。通過一個單例進(jìn)行埋點(diǎn)參數(shù)的維護(hù),由于單例提供了全局唯一訪問入口,程序中的任何位置都能方便地讀和寫埋點(diǎn)參數(shù)。這種方式帶來的好處是不需要在每個類都定義大量的埋點(diǎn)參數(shù),只需要訪問單例進(jìn)行修改和讀取。

          以詳情頁的 click_favorite 埋點(diǎn)舉例,可以通過跳轉(zhuǎn)前把值寫入單例,上報(bào)埋點(diǎn)時直接從單例獲取,而無須再從詳情頁 Activity 傳值給底部操作欄。

          object VideoDetailTracker {
              var fromTabName
              var fromChannelName
              var fromPage
              var videoInfo
          }

          class VideoViewHolder {
              var tabName
              var channelName
              var videoInfo

              fun clickJumpDetail() {
                  // 把上下文信息先存到單例
                  VideoDetailTracker.fromTabName = this.tabName
                  VideoDetailTracker.fromChannelName = this.channelName
                  VideoDetailTracker.fromPage = "feed"
                  VideoDetailTracker.videoInfo = this.videoInfo
                  startActivity(intent)
              }
          }

          class VideoDetailActivity {

              fun onCreate() {
                  // 詳情頁不需要再解析埋點(diǎn)參數(shù),也不需要再傳遞給BottomActionBar
                  // 只需有正常的功能代碼
                  val videoId = intent.getString("video_id")
                  videoInfo = loadVideoInfo(videoId)
              }
          }

          class BottomActionBar {

              fun clickFavorite() {
                  // 上報(bào)埋點(diǎn)的時候,直接從單例取出來拼接參數(shù)
                  LogSdk.onEvent("click_favorite", mapOf(
                      "from_tab_name" to VideoDetailTracker.fromTabName,
                      "from_channel_name" to VideoDetailTracker.fromChannelName,
                      "from_page" to VideoDetailTracker.fromPage,
                      "video_id" to VideoDetailTracker.videoInfo.id,
                      "video_type" to VideoDetailTracker.videoInfo.type,
                      "page_name" to "detail"
                  ))
              }
          }

          可以看出來,從列表頁 => 詳情頁以后,在詳情頁上報(bào)埋點(diǎn),獲取頁面來源信息,確實(shí)比之前更簡單了。但仔細(xì)想想,這種方案治標(biāo)不治本,同樣有明顯的弊端:

          • 首先,無法解決列表頁這種多實(shí)例場景的問題,比如一個推薦列表中有多個卡片,每個卡片的埋點(diǎn)參數(shù)都不一樣,卡片的埋點(diǎn)參數(shù)還是需要自己傳
          • 單例的數(shù)據(jù)可能被多個位置寫入,且一旦被覆蓋就沒法恢復(fù),比如這樣的路徑:列表 -> 詳情頁 1 -> 相關(guān)推薦 -> 詳情頁 2,進(jìn)到詳情頁 2 以后,單例的數(shù)據(jù)被覆蓋了,這時候再回到詳情頁 1,獲取到的埋點(diǎn)參數(shù)實(shí)際是詳情頁 2 的,導(dǎo)致埋點(diǎn)參數(shù)上報(bào)錯誤。
          • 存放和清理的時機(jī)難以控制,清理早了會導(dǎo)致埋點(diǎn)參數(shù)缺失,忘記清理可能導(dǎo)致后面的埋點(diǎn)獲取不到參數(shù)

          無埋點(diǎn)

          無埋點(diǎn)是業(yè)界流行的一種埋點(diǎn)方案,所謂的“無埋點(diǎn)”、“全埋點(diǎn)”,是指埋點(diǎn) SDK 通過編譯時插樁、運(yùn)行時反射或動態(tài)代理的方式,自動進(jìn)行埋點(diǎn)事件的觸發(fā)和上報(bào),無須客戶端工程師手動進(jìn)行埋點(diǎn)開發(fā)工作。由產(chǎn)品經(jīng)理、數(shù)據(jù)分析師等在埋點(diǎn)管理后臺,使用 XPath 路徑、頁面視圖 id 或者文本匹配等技術(shù),定位到頁面視圖的位置,過濾出所需的數(shù)據(jù)。

          此方案的優(yōu)勢很明顯,客戶端只需要一次性的接入,理論上能夠搜集到所有頁面、視圖的曝光、點(diǎn)擊等事件,無需客戶端同學(xué)進(jìn)行后續(xù)的埋點(diǎn)需求開發(fā)。

          有這么好的事?為什么字節(jié)沒有廣泛使用?此方案的缺陷在于:

          1. 僅能上報(bào)有限的簡單事件類型,如頁面視圖曝光、點(diǎn)擊等,無法完成復(fù)雜事件的上報(bào),如一次支付行為的操作路徑、結(jié)果、錯誤信息等
          2. 無法自定義參數(shù),主要指跳轉(zhuǎn)的來源、所處的場景等上下文信息,無法滿足復(fù)雜的數(shù)據(jù)分析和推薦模型所需的數(shù)據(jù)要求
          3. 由產(chǎn)品經(jīng)理、數(shù)據(jù)分析師等在埋點(diǎn)管理后臺進(jìn)行的事件錄入,把復(fù)雜度從開發(fā)轉(zhuǎn)嫁給了產(chǎn)品,消費(fèi)成本較高
          4. 對頁面視圖的穩(wěn)定性有很高的要求,需要約定 id、文本、視圖的層級,保持頁面結(jié)構(gòu)不變,如果客戶端工程師因?yàn)橐恍┬滦枨箝_發(fā)、性能優(yōu)化等調(diào)整了視圖結(jié)構(gòu),將會導(dǎo)致已錄入的埋點(diǎn)失效,增加了額外的維護(hù)成本
          5. 全場景的數(shù)據(jù)上報(bào),可能產(chǎn)生大量的無用數(shù)據(jù),消耗大量傳輸、存儲、計(jì)算資源

          基于責(zé)任鏈的埋點(diǎn)框架

          下面介紹下我們正在使用的埋點(diǎn)框架,是怎么解決埋點(diǎn)傳參困難、代碼冗余的問題,簡化埋點(diǎn)開發(fā)復(fù)雜度的。

          分析問題

          回顧下剛剛的埋點(diǎn)需求,上報(bào) click_favorite 埋點(diǎn),復(fù)雜度在于上報(bào)埋點(diǎn)的對象(列表卡片、詳情頁底部操作欄),為了埋點(diǎn)需要從其他對象(頻道、底 Tab、前面的頁面)獲取埋點(diǎn)參數(shù)。

          卡片需要關(guān)注自己所在的底 Tab、頻道,詳情頁需要關(guān)注自己的來源頁面,這顯然違反了“關(guān)注點(diǎn)分離”的原則。如果我們讓每個對象僅關(guān)注自己的信息,是否可行?

          埋點(diǎn)與視圖層級的關(guān)系

          我們回想下列表頁的視圖層級

          是不是會發(fā)現(xiàn)所需的埋點(diǎn)參數(shù)恰好就分布在視圖樹的責(zé)任鏈中?

          沒錯,聰明的你已經(jīng)想到了,我們在收藏按鈕被點(diǎn)擊時,只需要從收藏按鈕的節(jié)點(diǎn)按照卡片 -> 推薦頻道 -> 放映廳Tab的順序向上找,就能夠拿到所有需要的參數(shù)了。既然這個上下級關(guān)系(責(zé)任鏈)已經(jīng)客觀存在,我們?yōu)槭裁催€需要層層透傳埋點(diǎn),直接利用這個關(guān)系不就好了嗎?

          埋點(diǎn)與頁面跳轉(zhuǎn)鏈路的關(guān)系

          來源類埋點(diǎn)參數(shù)定義,常見的有 from_page、click_position 等,需要在跳轉(zhuǎn)的過程中,從前序頁面,傳遞到后序頁面,同時會有些映射規(guī)則,比如前序頁面的 page_name 到了后序頁面,上報(bào) from_page。

          那么頁面的跳轉(zhuǎn)鏈路是什么樣的呢?我們回想下,跳轉(zhuǎn)到詳情頁,有很多種路徑,比如下面的 2 種:

          上面是直接從推薦列表頁進(jìn)詳情頁:推薦列表 => 詳情頁

          下面是從推薦列表頁,點(diǎn)擊標(biāo)簽進(jìn)入選集頁,再從選集頁進(jìn)入詳情頁:推薦列表 => 選集 => 詳情頁

          可以看出頁面的跳轉(zhuǎn)鏈路,邏輯上也是一個樹狀結(jié)構(gòu)。如果我們結(jié)合前面說到的頁面內(nèi)視圖層級,把兩個樹放在一起,會是下面的樣子:

          是不是發(fā)現(xiàn),我們需要的埋點(diǎn)上下文參數(shù),理論上都可以通過節(jié)點(diǎn)的關(guān)系找到?

          解決問題

          有了前面的討論,我們來看一下怎么把這個問題,抽象成一個框架。

          1. ITrackModel

          ITrackModel 很簡單,這個接口定義了能夠填充埋點(diǎn)參數(shù)的對象,只要實(shí)現(xiàn)了這個接口,就可以在埋點(diǎn)上報(bào)的時候添加參數(shù)

          interface ITrackModel {
              fun fillTrackParams(trackParams: TrackParams)
          }

          2. ITrackNode

          ITrackModel 只是定義了填充參數(shù)的職責(zé),ITrackModel 對象之間并沒有關(guān)聯(lián),怎么找到所有的 ITrackModel,讓它們填充自己的埋點(diǎn)參數(shù)呢?在此基礎(chǔ)上,我們定義了 ITrackNode 接口

          interface ITrackNode: ITrackModel {
              fun parentTrackNode(): ITrackNode?
              fun referrerTrackNode(): ITrackNode?
          }

          ITrackModel 繼承了 ITrackModel,除了有填充埋點(diǎn)參數(shù)的能力外,還會指向父節(jié)點(diǎn)和來源節(jié)點(diǎn)。

          • parentTrackNode:指向父節(jié)點(diǎn),通過它可以建立一個頁面內(nèi)的責(zé)任鏈,在一個頁面內(nèi),根節(jié)點(diǎn)通常是頁面的頂層容器,例如 Android 的 Activity
          • referrerTrackNode:指向來源節(jié)點(diǎn),通過它可以建立用戶跳轉(zhuǎn)的邏輯鏈路,在用戶使用 App 的一個會話中,來源鏈路根節(jié)點(diǎn)通常指啟動頁面(也可以由 Push、DeepLink 的啟動參數(shù)構(gòu)造虛擬的 referrer 節(jié)點(diǎn))

          3. 建立頁面上下級責(zé)任鏈

          定義了 ITrackModel 和 ITrackNode,接下來就是實(shí)現(xiàn)每個節(jié)點(diǎn),并且將這些節(jié)點(diǎn)連起來。

          最通用的方式,是直接實(shí)現(xiàn) ITrackNode,例如在列表場景中,我們可以建立 ViewHolder -> Adapter -> Fragment 的責(zé)任鏈

          // 放映廳Tab
          class CinemaTabFragment: ITrackNode {

              override fun parentTrackNode(): ITrackNode {
                  return activity as ITrackNode
              }

              override fun fillTrackParams(trackParams: TrackParams) {
                  trackParams.putIfNull("tab_name""long_video")
              }
          }

          // 頻道Fragment
          class VideoChannelFragment: ITrackNode {
              override fun parentTrackNode(): ITrackNode {
                  return parentFragment as ITrackNode
              }

              override fun fillTrackParams(trackParams: TrackParams) {
                  trackParams.putIfNull("channel_name""lvideo_recommend")
                  trackParams.putIfNull("page_name""feed")
              }
          }

          // 列表Adapter,這一層沒有參數(shù),只是作為中間節(jié)點(diǎn),連接卡片ViewHolder和頻道Fragment
          class VideoChannelAdapter(private val parent: ITrackNode): ITrackNode {
              override fun parentTrackNode(): ITrackNode {
                  return fragment as ITrackNode
              }
          }

          // 卡片ViewHolder
          class VideoViewHolder(private val parent: ITrackNode, val view: View) : ITrackNode {

              var videoInfo

              override fun parentTrackNode(): ITrackNode {
                  return parentFragment as ITrackNode
              }

              override fun fillTrackParams(trackParams: TrackParams) {
                  trackParams.putIfNull("video_id", videoInfo.id)
                  trackParams.putIfNull("video_type", videoInfo.type)
              }

              fun clickFavorite() {
                  // 使用ITrackNode.onEvent上報(bào)埋點(diǎn),會從當(dāng)前節(jié)點(diǎn)開始向上收集埋點(diǎn)參數(shù)
                  this.onEvent("click_favorite")
              }
          }

          這樣,我們就建立起了列表頁的上下級責(zé)任鏈,我們可以看到埋點(diǎn)參數(shù)都在對應(yīng)的節(jié)點(diǎn)添加了,而不需要再從上級層層傳入,上報(bào)埋點(diǎn)的代碼變得非常簡單。

          直接實(shí)現(xiàn) ITrackNode 的方式,特別適合 Fragment、Adapter、ViewHolder 等需要我們自定義的類,他們在視圖構(gòu)建中的作用是將視圖拆分層級,更好的管理局部的視圖、數(shù)據(jù)和邏輯。然而,我們發(fā)現(xiàn)這種方式需要實(shí)現(xiàn)每一個節(jié)點(diǎn),并且手動建立節(jié)點(diǎn)之間的聯(lián)系,使用起來還是挺麻煩的。

          大部分情況下,我們發(fā)現(xiàn)上下級責(zé)任鏈的關(guān)系,和視圖層級的關(guān)系是一致的,而系統(tǒng)已經(jīng)為我們建立了視圖樹 ViewTree,那么我們可以利用 ViewTree,來建立上下級責(zé)任鏈。其中 ViewTree 上的每一個 View,只需要實(shí)現(xiàn) ITrackModel 的能力,就可以負(fù)責(zé)填充埋點(diǎn)參數(shù)。

          我們利用 View.setTag 可以存放任意對象的特性,為 View 增加了擴(kuò)展屬性

          /**
           * 設(shè)置View的TrackModel
           */
          var View.trackModel: ITrackModel?
              get() = this.getTag(TAG_ID_TRACK_MODEL) as? ITrackModel
              set(value) {
                  this.setTag(TAG_ID_TRACK_MODEL, value)
              }

          上面的例子,可以換一種實(shí)現(xiàn)方式:

          // 放映廳Tab
          class CinemaTabFragment: ITrackModel {

              override fun fillTrackParams(trackParams: TrackParams) {
                  trackParams.putIfNull("tab_name""long_video")
              }

              override fun onViewCreated(view: View) {
                  // 放映廳Tab根視圖,ITrackModel由Fragment實(shí)現(xiàn)
                  view.trackModel = this
              }
          }

          // 頻道Fragment
          class VideoChannelFragment: ITrackModel {

              override fun fillTrackParams(trackParams: TrackParams) {
                  trackParams.putIfNull("channel_name""lvideo_recommend")
                  trackParams.putIfNull("page_name""feed")
              }

              override fun onViewCreated(view: View) {
                  // 頻道根視圖,ITrackModel由Fragment實(shí)現(xiàn)
                  view.trackModel = this
              }
          }

          // 卡片ViewHolder
          class VideoViewHolder(val view: View) : ITrackModel {

              var videoInfo

              fun bind(videoInfo: VideoInfo) {
                  this.videoInfo = videoInfo
                  this.itemView.trackModel = this
              }

              override fun fillTrackParams(trackParams: TrackParams) {
                  trackParams.putIfNull("video_id", videoInfo.id)
                  trackParams.putIfNull("video_type", videoInfo.type)
              }

              fun clickFavorite() {
                  // 使用View.trackEvent上報(bào)埋點(diǎn),會從當(dāng)前View開始向上收集埋點(diǎn)參數(shù)
                  itemView.trackEvent("click_favorite")
              }
          }

          可以看到,利用 ViewTree,為 View 添加 trackModel 的方式,不需要再實(shí)現(xiàn) ITrackNode,手動建立上下級關(guān)系。由于 ViewTree 的存在,即便是層級很深的子視圖,也可以直接作為埋點(diǎn)節(jié)點(diǎn)來使用,而不需要再經(jīng)過中間的橋接節(jié)點(diǎn)。

          直接在自定義類實(shí)現(xiàn) ITrackNode,和為 View 添加 ITrackModel,這兩種方式可以組合在一起使用。理想的頁面上下級鏈路是這樣的,實(shí)現(xiàn) ITrackNode 作為上層節(jié)點(diǎn),更加方便組織邏輯關(guān)系復(fù)雜的子視圖,如首頁頻道等;層級較深的節(jié)點(diǎn)直接利用 ViewTree,方便向上搜索責(zé)任鏈。

          4. 建立頁面來源責(zé)任鏈

          建立來源責(zé)任鏈的建立,指的是頁面跳轉(zhuǎn)過程中,將跳轉(zhuǎn)前的節(jié)點(diǎn)/上下文參數(shù)傳遞給跳轉(zhuǎn)后的頁面,作為后者的來源節(jié)點(diǎn)(referrerTrackNode)。

          我們利用跳轉(zhuǎn) Intent 攜帶來源節(jié)點(diǎn)信息:

          // 設(shè)置當(dāng)前跳轉(zhuǎn)的來源節(jié)點(diǎn)
          class VideoViewHolder {
              fun clickJumpDetail() {
                  // 設(shè)置跳轉(zhuǎn)的來源節(jié)點(diǎn)是當(dāng)前節(jié)點(diǎn)
                  intent.setReferrerTrackNode(this)
                  startActivity(intent)
              }
          }

          在跳轉(zhuǎn)后的頁面,只需要從 intent 再取出來就可以了

          class VideoDetailActivity {
              override fun referrerTrackNode(): ITrackNode {
                  return intent.getReferrerTrackNode()
              }
          }

          值得注意的是,邏輯上應(yīng)該直接使用當(dāng)前節(jié)點(diǎn)的引用作為下個頁面的 referrerTrackNode,但實(shí)際使用中,可能會有內(nèi)存泄漏、鏈路過于復(fù)雜的問題,所以在 setReferrerTrackNode 的時候,我們制作了當(dāng)前節(jié)點(diǎn)的快照,把當(dāng)前節(jié)點(diǎn)的上下文參數(shù)都添加進(jìn)了 Map,傳遞給下個頁面的實(shí)際上是這個快照節(jié)點(diǎn)。

          完成了來源節(jié)點(diǎn)的傳遞,在下個頁面怎么使用呢?最簡單的是直接把來源節(jié)點(diǎn)的所有參數(shù),添加進(jìn)埋點(diǎn)中,但我們的埋點(diǎn)需求常常會需要一些轉(zhuǎn)換規(guī)則,比如:

          • 上個頁面的 category_name,跳轉(zhuǎn)后上報(bào) parent_category_name
          • 上個頁面的 page_name,跳轉(zhuǎn)后上報(bào) from_page

          因此我們定義了 IPageTrackNode,用來做頁面級別的埋點(diǎn)處理

          interface IPageTrackNode: ITrackNode {
              fun referrerKeyMap(): Map<String, String>
          }

          通常會由頁面的 Activity 實(shí)現(xiàn) IPageTrackNode

          class VideoDetailActivity: IPageTrackNode {

              var videoInfo

              // 定義來源參數(shù)映射
              override fun referrerKeyMap(): Map<String, String> {
                  return mapOf(
                      "page_name" to "from_page",
                      "channel_name" to "from_channel_name",
                      "tab_name" to "from_tab_name"
                  )
              }

              override fun fillTrackParams(trackParams: TrackParams) {
                  trackParams.putIfNull("video_id", videoInfo.id)
                  trackParams.putIfNull("video_type", videoInfo.type)
                  trackParams.putIfNull("page_name""detail")
              }
          }

          class BottomActionBar {

              fun clickFavorite() {
                  // 上報(bào)埋點(diǎn)的時候,直接從當(dāng)前節(jié)點(diǎn)往上收集埋點(diǎn)參數(shù)
                  trackEvent("click_favorite")
              }
          }

          可以看到這樣一來,在詳情頁上報(bào) click_favorite 埋點(diǎn)也變得簡單了。

          5. 埋點(diǎn)參數(shù)的收集

          前面說明了如何建立頁面上下級責(zé)任鏈和來源責(zé)任鏈。上報(bào)埋點(diǎn)的時候,按照下面的流程,順著責(zé)任鏈?zhǔn)占顸c(diǎn)參數(shù):

          6. 埋點(diǎn)線索:TrackThread

          按照前面的內(nèi)容,我們已經(jīng)可以建立用戶使用整個 App 的過程中,所有上下文的責(zé)任鏈關(guān)系,理論上可以上報(bào)任意需要的上下文參數(shù)。然而實(shí)際業(yè)務(wù)的埋點(diǎn)需求中,還有一類更復(fù)雜的場景,需要在多個節(jié)點(diǎn)/頁面間共享埋點(diǎn)參數(shù)。例如西瓜視頻創(chuàng)作過程的埋點(diǎn):

          • tab_name:進(jìn)入創(chuàng)作場景的來源,在一次創(chuàng)作過程中,所有埋點(diǎn)都需要帶上這個信息
          • is_record/is_cut:是否使用過拍攝、剪輯功能,可能在創(chuàng)作過程中發(fā)生變化,在創(chuàng)作過程的任意節(jié)點(diǎn)上,需要讀寫這些參數(shù)

          以前這類埋點(diǎn)基本會通過單例來維護(hù),單例的話就會遇到前面“單例傳參”部分講到的問題,而我們發(fā)現(xiàn)在整個頁面上下級和來源責(zé)任鏈都已經(jīng)建立的情況下,頁面的之間的關(guān)聯(lián)不就可以方便地共享參數(shù)嗎?在一個打開的頁面上添加參數(shù),并且共享到后續(xù)的頁面,參數(shù)的生命周期和頁面的生命周期綁定,用戶離開這個頁面后自動消失,不用擔(dān)心清除和覆蓋的問題。

          因此我們引入埋點(diǎn)線索(TrackThread)的定義,任意起始節(jié)點(diǎn)都可以初始化一個 TrackThread,TrackThread 上能夠存放各種類型的 TrackModel,在后續(xù)的所有關(guān)聯(lián)節(jié)點(diǎn)中,都能夠通過已經(jīng)建立的責(zé)任鏈,訪問到 Thread 進(jìn)行讀寫。通過任意節(jié)點(diǎn)上報(bào)埋點(diǎn),可以指定需要添加哪些 TrackModel 的埋點(diǎn)參數(shù)。

          //實(shí)現(xiàn)ITrackModel接口
          class RecordInfo : ITrackModel {
              var isRecord = false

              override fun fillTrackParams(params: TrackParams) {
                  params.put("is_record", isRecord.toYesOrNo())
              }
          }

          // 在某個合適的時機(jī),比如進(jìn)入拍攝頁面,開啟埋點(diǎn)thread,添加TrackModel
          node.startTrackThread().putTrackModel(RecordInfo())

          // 任意節(jié)點(diǎn)上更新thread
          node.trackThread?.getTrackModel(RecordInfo::class.java).isRecord = true

          // 上報(bào)埋點(diǎn)
          view.newTrackEvent("click_publish") // 通過newTrackEvent創(chuàng)建Event實(shí)例
              .with(RecordInfo::class.java) // 聲明需要上報(bào)TrackThread中的RecordInfo
              .emit() // 最終計(jì)算并上報(bào)埋點(diǎn)

          埋點(diǎn)線索適合用于具有會話特性的流程中,方便在流程中共享參數(shù),常見的還有登錄、注冊的流程,訂單創(chuàng)建流程等。

          總結(jié)

          至此,我們基于責(zé)任鏈的埋點(diǎn)開發(fā)框架已經(jīng)差不多介紹完了,從上面的內(nèi)容可以看出,這個框架更多是約定了一套責(zé)任鏈的協(xié)議,通過責(zé)任鏈的存在,方便埋點(diǎn)參數(shù)的收集上報(bào)。當(dāng)然我們?yōu)榱朔奖闶褂?,也使用到很多語言特性來簡化框架的 API,比如通過接口默認(rèn)實(shí)現(xiàn),F(xiàn)ragment 的父節(jié)點(diǎn)默認(rèn)指向 Activity,通過擴(kuò)展函數(shù),讓 View 可以直接添加 TrackModel 和上報(bào)埋點(diǎn),但這些都不影響協(xié)議原本的內(nèi)涵。

          總結(jié)下這個方案的優(yōu)勢和問題:

          優(yōu)勢

          • 埋點(diǎn)需求對原有的功能代碼侵入性小,只需實(shí)現(xiàn) ITrackNode 接口建立鏈路,或者直接使用 ViewTree 的責(zé)任鏈,就可以達(dá)到跨層級傳參的目的,傳參復(fù)雜度大大降低,減少代碼量和人力成本
          • 各個節(jié)點(diǎn)不再需要關(guān)注其他節(jié)點(diǎn)的埋點(diǎn)參數(shù),只需要負(fù)責(zé)填充自己的參數(shù)即可,做到“關(guān)注點(diǎn)分離”,同時還能夠讓每個節(jié)點(diǎn)的參數(shù)得到復(fù)用,無需反復(fù)添加同樣的參數(shù),理想情況下,一個場景的一個新增參數(shù),只需在相關(guān)節(jié)點(diǎn)上添加一處,即可做到所有的子節(jié)點(diǎn)都能收集到

          問題

          • 理解成本,就像前端同學(xué)使用 React 一樣,不可避免地有一些學(xué)習(xí)成本;同時還需要和數(shù)據(jù)分析師約定好埋點(diǎn)規(guī)范,只有在良好的規(guī)范下,埋點(diǎn)框架才能發(fā)揮更好的作用
          • 無法通過類、參數(shù)定義等方式,強(qiáng)制約束埋點(diǎn)參數(shù)列表,新來的開發(fā)同學(xué)做新需求時,可能不知道該傳哪些參數(shù)

          重構(gòu)現(xiàn)有埋點(diǎn)的建議

          一些場景已經(jīng)有大量埋點(diǎn)邏輯,無法短時間全部改掉。主要原因是因?yàn)槁顸c(diǎn)往往沒有“邊界”:埋點(diǎn)需要大量的“上下文”與“來源”參數(shù),而我們已經(jīng)在實(shí)踐中發(fā)現(xiàn),這些參數(shù)是埋點(diǎn)錯綜復(fù)雜的主要原因。

          舉 2 個例子:進(jìn)入個人主頁埋點(diǎn)(enter_pgc)和點(diǎn)擊關(guān)注埋點(diǎn)(rt_follow),開發(fā)面臨的情況是進(jìn)入個人主頁的入口有 100+處,關(guān)注組件被引用的場景有 58 處。短時間要修改全部來源參數(shù)和上下文參數(shù)的傳遞方式,開發(fā)和測試成本很大,一次迭代基本不可能完成。

          因此我們建議弄清埋點(diǎn)的“邊界”,在此基礎(chǔ)上控制每一次重構(gòu)的影響范圍。

          重構(gòu)通用業(yè)務(wù)組件

          通用業(yè)務(wù)組件指的是像“關(guān)注按鈕”或者“關(guān)注操作”這種有特定業(yè)務(wù)邏輯、大量使用于各種業(yè)務(wù)場景的下沉組件。這類組件往往有特定的埋點(diǎn)要求,觸發(fā)埋點(diǎn)本身不是很復(fù)雜的事情,復(fù)雜的是怎么獲取到觸發(fā)事件時的上下文信息。對于此類組件,建議:

          • 保留新舊兩種埋點(diǎn)傳參的接口
          • 在老接口中對埋點(diǎn)參數(shù)進(jìn)行封裝,轉(zhuǎn)發(fā)到新接口執(zhí)行實(shí)際的邏輯,同時對老接口標(biāo)注@Deprecated
          • 未來逐步替換到新接口上

          重構(gòu)一個頁面的結(jié)構(gòu)

          重構(gòu)一個頁面時,期望可以把各個層級定義成 TrackNode 節(jié)點(diǎn),構(gòu)建完整的責(zé)任鏈。但是一個頁面結(jié)構(gòu)中,常常使用了大量其他模塊的組件或者功能,而依賴的這些模塊還沒完成埋點(diǎn)重構(gòu)。因此重構(gòu)時依賴到其他模塊/組件的情況,建議:

          • 對當(dāng)前模塊內(nèi)的部分構(gòu)建責(zé)任鏈
          • 不改變其他模塊的接口,涉及到這些模塊埋點(diǎn)參數(shù)傳遞的,在傳參的節(jié)點(diǎn)上,獲取節(jié)點(diǎn)的埋點(diǎn)參數(shù),使用老的方式傳參

          重構(gòu)一個頁面的來源傳參方式

          一個頁面的來源參數(shù),外部可能通過很多種方式,如 Intent、單例等傳遞過來。為了讓頁面內(nèi)責(zé)任鏈上的每個節(jié)點(diǎn),都能夠獲取到來源的參數(shù),同時兼容外部新老傳參的方式,建議:

          • 當(dāng)前頁面改造成新的獲取來源參數(shù)的方式,同時支持按照老的傳參方式讀取來源參數(shù),需要時可在頁面初始化的時機(jī) Mock 一個 referrerTrackNode
          • 外部跳轉(zhuǎn)逐步切換到新的來源參數(shù)傳參方式

          加入我們

          歡迎加入字節(jié)跳動西瓜視頻客戶端團(tuán)隊(duì),我們專注于西瓜視頻 App 的開發(fā)和基礎(chǔ)技術(shù)建設(shè),在客戶端架構(gòu)、性能、穩(wěn)定性、編譯構(gòu)建、研發(fā)工具等方向都有投入。如果你也想一起攻克技術(shù)難題,迎接更大的技術(shù)挑戰(zhàn),歡迎加入我們!

          西瓜視頻客戶端團(tuán)隊(duì)正在熱招 Android、iOS 架構(gòu)師和研發(fā)工程師,最 Nice 的工作氛圍和成長機(jī)會,各種福利各種機(jī)遇,在北京、杭州、上海三地均有職位,歡迎投遞簡歷!聯(lián)系郵箱:[email protected] ;郵件標(biāo)題:姓名-工作年限-西瓜-Android/iOS/基礎(chǔ)技術(shù)。

          點(diǎn)個在看殺個 Bug ?

          瀏覽 176
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  青娱乐永久在线视频 | 色婷婷久久综合久色 | 日皮视频在线免费看 | 国产精品98 | 超碰精品|