可视化埋点在React Native中的实践

本文首发于微信公众号“ Shopee技术团队”。

1. 背景

笔者所在团队为 Shopee 的本地生活前端团队,用户可以在我们的平台购买优惠券,然后去线下门店使用。随着用户规模不断增加,研究用户行为数据可以更好地指导产品功能设计,提供更加优秀的用户体验。用户行为数据的研究首先涉及到如何采集,即我们常说的“埋点”。

一直以来,我们项目中的埋点都采用代码埋点,每次新增埋点往往是一些重复性的工作,且需要重新发布代码才能生效,为此我们的开发人员叫苦不迭。为了实现在不修改代码的前提下新增埋点,我们调研了可视化埋点和无埋点两种方式。其中,无埋点(又称全埋点)会收集用户在应用里的所有行为,并上报所有相关的数据,由此产生大量无用数据,于是被我们排除了。

而可视化埋点的方式为:通过埋点平台圈选所需埋点的页面元素,进行埋点上报属性的配置与发布,由采集 SDK 同步埋点配置,并根据配置自动进行用户行为数据的采集和发送。正好可以解决我们的问题,因此我们决定采用可视化埋点方案。

在开始介绍我们的系统前,先来看看在 Web 上进行可视化埋点的基本思路:以点击事件为例(下文如果没有特殊说明,均以点击事件为例),Web 可视化埋点一般会提供一个 SDK,SDK 会在 document 上面监听 click 事件,借助于事件委托的特性,可以捕获到页面上任意元素的 click 事件及元素的信息。同时 Web 可视化埋点会提供一个平台,该平台通过 iframe 嵌入需要进行埋点配置的网页,然后通过 postMessage 来进行平台与目标页面的通信。

可视化埋点在React Native中的实践_第1张图片

由于我们的前端技术栈是 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
  
Click Me

这些导出的组件都是利用高阶组件的思想对原来的组件进行了重写,并加入了埋点相关的逻辑。

2.2 连接客户端与可视化埋点平台

接入完 SDK 后,接下来就可以对埋点进行配置了。进行埋点配置前,首先要将我们的 React Native 客户端跟可视化埋点平台连接起来。

可视化埋点在React Native中的实践_第2张图片

如上图所示,埋点配置人员首先需要在可视化埋点平台开始一个埋点任务,可视化埋点平台前端会通过 WebSocket 连接到服务端,服务端会生成一个 sessionId 发送给前端:

可视化埋点在React Native中的实践_第3张图片

并且会将连接到服务端的 WebSocket 客户端进行登记:

{
    25089: {
        creator: adminWSClient
    }
}

此时埋点配置人员在 React Native 客户端通过 SDK 提供的工具进入连接页面,输入 sessionId 后通过 WebSocket 连接到埋点可视化平台服务端:

可视化埋点在React Native中的实践_第4张图片

服务端也会将连接到的 WebSocket 客户端进行登记:

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

这样,通过可视化埋点平台服务端,就可以将 React Native 客户端同可视化埋点平台前端间接地连接起来了。此时,可视化埋点服务端会通知前端和 React Native 客户端连接成功。得到消息后,前端会进入配置页面,React Native 客户端则进入配置模式。之后每当配置人员在 React Native 客户端对页面元素进行圈选时,SDK 都会将相关数据发送到可视化埋点平台前端,供配置人员进行配置。

2.3 埋点配置

以下是连接成功后 React Native 客户端及可视化埋点前端对应的效果:

可视化埋点在React Native中的实践_第5张图片

如图所示,当埋点配置人员在 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 节点:

可视化埋点在React Native中的实践_第6张图片

通过从当前组件的 FiberNode 出发一直往上遍历到根部,可以得到一条类似于 XPath 的路径作为该组件的 trackId。但是在实施的时候发现相同的代码在 Android 和 iOS 两个平台生成的 trackId 并不一样,这也就意味着如果采取这种方案的话,埋点配置时需要针对两个平台分别配置,这显然会大大增加工作量。所以最后我们不得已放弃了该方案,暂时采用了开发手动给组件设置 trackId 的方案。

在遍历的同时,我们还可以可以得到所有祖先组件的 FiberNode 上的 memoizedProps 和 memoizedState ,它们分别对应组件的 props 和 state,这样我们就可以得到组件的埋点属性数据来源集合了,类似于下图所示:

可视化埋点在React Native中的实践_第7张图片

埋点配置完成后,会发布成 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 首先会去远程拉取最新的埋点配置文件,此时又存在一个问题:拉取埋点配置文件是需要时间的,这就导致这个过程中用户的行为事件全部都会丢失。

可视化埋点在React Native中的实践_第8张图片

如上图所示,为了解决这个问题,我们设计了一个队列,该队列会不断地接收并存储所有用户的行为事件。然后,我们在 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 本地生活前端团队

可视化埋点在React Native中的实践_第9张图片

你可能感兴趣的:(可视化埋点在React Native中的实践)