本文首发于微信公众号“ 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
Click Me
这些导出的组件都是利用高阶组件的思想对原来的组件进行了重写,并加入了埋点相关的逻辑。
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
相关的属性,比如可以通过child
、return
、sibling
三个指针来对FiberNode
树进行遍历,memoizedProps
和memoizedState
可以用来替代组件的props
和state
等;- 使用 babel 插件对代码进行改写,解决组件名称被混淆的问题。
这些知识有些是一些业界比较成熟的方案,可以直接复用,有些在官方文档中并未提及,需要对内部机制有深入的了解才能实现。由此可见,在进行业务开发时,保持对日常所用框架及工具的深入探索是必不可少的。
目前我们已成功接入了一些新的埋点需求。从开发反馈来看,不用写很多重复的埋点上报代码确实是一大福音,同时也可以支持不修改代码来修改或增加埋点,比较显著地提高了埋点需求上线的效率。我们也在不断改进这一系统,比如对埋点的检查及监控,检查的目的是确保上报数据的准确性,而监控的目的是及时发现埋点问题并进行修复。
参考链接
- Web 可视化全埋点使用指南, 神策数据
- babel-plugin-add-react-displayname
- Higher-Order Components
- _reactInternals
- FiberNode
本文作者
Shopee 本地生活前端团队