視覺化埋點在React Native中的實踐

語言: CN / TW / HK

本文首發於微信公眾號“Shopee技術團隊”。

1. 背景

筆者所在團隊為 Shopee 的本地生活前端團隊,使用者可以在我們的平臺購買優惠券,然後去線下門店使用。隨著使用者規模不斷增加,研究使用者行為資料可以更好地指導產品功能設計,提供更加優秀的使用者體驗。使用者行為資料的研究首先涉及到如何採集,即我們常說的“埋點”。

一直以來,我們專案中的埋點都採用程式碼埋點,每次新增埋點往往是一些重複性的工作,且需要重新發布程式碼才能生效,為此我們的開發人員叫苦不迭。為了實現在不修改程式碼的前提下新增埋點,我們調研了視覺化埋點和無埋點兩種方式。其中,無埋點(又稱全埋點)會收集使用者在應用裡的所有行為,並上報所有相關的資料,由此產生大量無用資料,於是被我們排除了。

而視覺化埋點的方式為: 通過埋點平臺圈選所需埋點的頁面元素,進行埋點上報屬性的配置與釋出,由採集 SDK 同步埋點配置,並根據配置自動進行使用者行為資料的採集和傳送 。正好可以解決我們的問題,因此我們決定採用視覺化埋點方案。

在開始介紹我們的系統前,先來看看在 Web 上進行視覺化埋點的基本思路:以點選事件為例(下文如果沒有特殊說明,均以點選事件為例),Web 視覺化埋點一般會提供一個 SDK,SDK 會在 document 上面監聽  click 事件,藉助於事件委託的特性,可以捕獲到頁面上任意元素的  click 事件及元素的資訊。同時 Web 視覺化埋點會提供一個平臺,該平臺通過  iframe 嵌入需要進行埋點配置的網頁,然後通過  postMessage 來進行平臺與目標頁面的通訊。

由於我們的前端技術棧是 React Native,很多地方實現起來都比較有難度,比如無法通過 iframe 嵌入頁面及  postMessage 實現平臺與目標頁面的通訊,無法藉助事件委託的特性來實現我們的 SDK 等。那麼,最後究竟是如何實現的呢?下文將詳細展開介紹。

2. 系統介紹

下面按照使用流程來介紹我們的系統。首先,需要在 React Native 客戶端接入我們的 SDK。

2.1 客戶端接入 SDK

如下所示,我們通過執行 SDK 的 initGoblin 方法匯出了  TouchableComponent ,該物件又匯出了跟點選相關的一些元件供業務方使用,我們直接使用匯出的這些點選相關的元件,並指定  trackId 即可(關於  trackId 後文會做介紹):

import { initGoblin } from '@dp/goblin-sdk-react-native'
  
export const { TouchableComponent } = initGoblin({ ... })
  
const {
  GButton,
  GTouchableHighlight,
  GTouchableNativeFeedback,
  GTouchableOpacity,
  GTouchableWithoutFeedback
} = TouchableComponent
  
<GButton trackId="button">Click Me</GButton>

這些匯出的元件都是利用高階元件的思想對原來的元件進行了重寫,並加入了埋點相關的邏輯。

2.2 連線客戶端與視覺化埋點平臺

接入完 SDK 後,接下來就可以對埋點進行配置了。進行埋點配置前,首先要將我們的 React Native 客戶端跟視覺化埋點平臺連線起來。

如上圖所示,埋點配置人員首先需要在視覺化埋點平臺開始一個埋點任務,視覺化埋點平臺前端會通過 WebSocket 連線到服務端,服務端會生成一個  sessionId 傳送給前端:

並且會將連線到服務端的 WebSocket 客戶端進行登記:

{
    25089: {
        creator: adminWSClient
    }
}

此時埋點配置人員在 React Native 客戶端通過 SDK 提供的工具進入連線頁面,輸入 sessionId 後通過  WebSocket 連線到埋點視覺化平臺服務端:

服務端也會將連線到的 WebSocket 客戶端進行登記:

{
    25089: {
        creator: adminWSClient,
        connector: rnWSClient
    }
}

這樣,通過視覺化埋點平臺服務端,就可以將 React Native 客戶端同視覺化埋點平臺前端間接地連線起來了。此時,視覺化埋點服務端會通知前端和 React Native 客戶端連線成功。得到訊息後,前端會進入配置頁面,React Native 客戶端則進入配置模式。之後每當配置人員在 React Native 客戶端對頁面元素進行圈選時,SDK 都會將相關資料傳送到視覺化埋點平臺前端,供配置人員進行配置。

2.3 埋點配置

以下是連線成功後 React Native 客戶端及視覺化埋點前端對應的效果:

如圖所示,當埋點配置人員在 React Native 客戶端點選選擇所需要埋點的元素時,SDK 會高亮該元素。同時,SDK 還會將當前所選元素的 trackId 及埋點屬性資料來源集合傳送到平臺服務端,其中埋點屬性資料來源集合由元素對應的 React 元件本身和其祖先元件的  props 和  state 屬性所組成。

此時埋點配置人員可在平臺上新增需要上報的欄位並指定欄位名、欄位值來源, 比如圖中新增了一個名為  title  的欄位,並指定其值來自於  Item  這個元件  props  下的  title  屬性

上文所說的 trackId 是當前所選擇元素的唯一標識,類似於 Web 中頁面元素的 id 或 XPath。其中 id 的優勢是比較準確,不會因為頁面結構變化而失效,缺點是需要開發人員事先設定,而 XPath 的優勢是可以自動生成,但是對頁面結構變化比較敏感。

我們知道,每個 React 應用背後其實都對應著一顆由 FiberNode 節點組成的樹,而 React 類元件中可以通過  this._reactInternals (16 版本)得到當前元件所對應的  FiberNode 節點:

通過從當前元件的 FiberNode 出發一直往上遍歷到根部,可以得到一條類似於 XPath 的路徑作為該元件的  trackId 。但是在實施的時候發現相同的程式碼在 Android 和 iOS 兩個平臺生成的  trackId 並不一樣,這也就意味著如果採取這種方案的話,埋點配置時需要針對兩個平臺分別配置,這顯然會大大增加工作量。所以最後我們不得已放棄了該方案,暫時採用了開發手動給元件設定  trackId 的方案。

在遍歷的同時,我們還可以可以得到所有祖先元件的 FiberNode 上的  memoizedProps 和  memoizedState ,它們分別對應元件的  props 和  state ,這樣我們就可以得到元件的埋點屬性資料來源集合了,類似於下圖所示:

埋點配置完成後,會發布成 JSON 格式的檔案,比如上文的例子釋出後會如下所示:

{
  ...
  "item-button": {
    "constant": {
      "operation": "click"
    },
    "variable": {
      "title": "props.Item.title"
    }
  }
  ...
}

每一個配置都是以 trackId 為 key 的一個物件,其中物件中  constant 屬性表示需要上報的欄位的值是固定的,例如  operation 為  click 表示當前使用者的操作為點選, variable 則表示需要上報的欄位的值是動態的,其值是一條取值路徑,這裡表示  title 這個欄位的值需要從  Item 元件的  props 中的  title 屬性來獲取。

然而在實際使用時又遇到了一個問題:我們的程式碼在生產環境中打包以後,元件的名稱都被混淆了,導致配置人員進行配置的時候根本無法識別。

為了解決這個問題,我們參考 babel-plugin-add-react-displayname 庫編寫了一個 babel 外掛,在打包的時候自動給元件新增 displayName ,埋點 SDK 在收集埋點資料的時候不再取元件的名字而是取元件上的  displayName 屬性。

埋點配置釋出後,使用者在使用我們的產品時,SDK 會同步配置檔案,並根據配置檔案匹配使用者的行為進行資料上報。

2.4 埋點上報

當用戶開啟頁面時,SDK 首先會去遠端拉取最新的埋點配置檔案,此時又存在一個問題:拉取埋點配置檔案是需要時間的,這就導致這個過程中使用者的行為事件全部都會丟失。

如上圖所示,為了解決這個問題,我們設計了一個佇列,該佇列會不斷地接收並存儲所有使用者的行為事件。然後,我們在 requestIdleCallback 中進行處理,使用  requestIdleCallback 的好處是可以在空閒的時候執行,因而不影響動畫及使用者輸入等關鍵事件。

當發現配置檔案拉取成功時,會開始消費佇列中的使用者行為事件,如果使用者行為事件對應的元件不能在配置檔案中找到,則直接丟棄;否則,會對其進行處理。處理方法同埋點配置過程類似,首先也會通過 FiberNode 樹收集到埋點屬性資料來源集合,然後通過該集合給埋點配置中  variable 中的欄位進行賦值,最後合併  constant 中的資料進行上報。

比如下面這條埋點配置:

{
  "item-button": {
    "constant": {
      "operation": "click"
    },
    "variable": {
      "title": "props.Item.title"
    }
  }
}

最後會生成如下所示的上報資料:

{
  "operation": "click",
  "title": "Second Item"
}

3. 總結

本文介紹了一套在 React Native 應用中實施視覺化埋點的方案,實現這一套方案涉及到以下知識:

  • React 高階元件的思想,通過對 React Native 元件進行重寫,新增我們埋點相關的邏輯;
  • 通過類元件的  _reactInternals  可獲取對應的  FiberNode  節點;
  • FiberNode  相關的屬性,比如可以通過  childreturnsibling  三個指標來對  FiberNode  樹進行遍歷, memoizedProps  和  memoizedState  可以用來替代元件的  props  和  state  等;
  • 使用 babel 外掛對程式碼進行改寫,解決元件名稱被混淆的問題。

這些知識有些是一些業界比較成熟的方案,可以直接複用,有些在官方文件中並未提及,需要對內部機制有深入的瞭解才能實現。由此可見,在進行業務開發時,保持對日常所用框架及工具的深入探索是必不可少的。

目前我們已成功接入了一些新的埋點需求。從開發反饋來看,不用寫很多重複的埋點上報程式碼確實是一大福音,同時也可以支援不修改程式碼來修改或增加埋點,比較顯著地提高了埋點需求上線的效率。我們也在不斷改進這一系統,比如對埋點的檢查及監控,檢查的目的是確保上報資料的準確性,而監控的目的是及時發現埋點問題並進行修復。

參考連結

本文作者

Shopee 本地生活前端團隊