iOS 可视化埋点设计方案

avatar
@北京读我科技有限公司

前言

可视化埋点,也称圈选埋点,是建立在全埋点技术基础上的一种数据埋点机制。通过全埋点技术,尽可能地将用户的所有交互行为进行采集上报,然后通过可视化圈选的方式筛选出感兴趣的行为统计数据,为产品运营提供决策支持。可视化埋点具有“全面、便捷、低技术门槛”的特点,能够有效降低研发、运营成本,是对传统代码埋点技术的有力补充。

本文结合伴鱼 iOS 端在圈选埋点技术上的一些实践经验,对圈选埋点方案的设计和实现进行探讨。

总体思路

从数据采集到生成统计报表,一般需要经过三个步骤,如下图所示

从 0 到 1 搭建技术中台之 iOS 可视化埋点实践

1. 用户行为数据采集:通过全埋点技术采集用户行为事件;

2. 圈选配置匹配规则:由产品或运营人员通过可视化圈选工具,对感兴趣的用户行为事件进行标定,生成事件匹配规则,并上传到服务端;

3. 匹配计算生成报表:数据研发人员根据已配置的事件匹配规则进行数据统计,生成报表

这里采用全埋点的方式采集用户行为数据,会增加 App 端数据流量和服务端数据存储压力。选择该方案的理由参见 4.2 前后端配合方式的选择 。

事件标识定义

全埋点采集用户行为,需要解决的最大问题是:如何精确描述行为事件。通常对页面和页面中的可交互元素分别进行定义。

A. 页面标识

页面标识通常采用 2 种方式来标定:

1. 页面路径:从 Window 的根控制器开始直到页面所在视图控制器的路径。例如

UITabBarController-UINavigationController(1)-MyViewController(2)

括号中的数字代表控制器在父控制器中的索引。

2. 页面类名: 直接已控制器的类名作为页面标识。被 Presented 的控制器也适用于该方式。

例外情况

a. 页面所属控制器存在自定义的父控制器

例如:一个控制器包含了若干子控制器,且通过 UIScrollView 分页的方式呈现各子控制器的视图。对于此类控制器,无法通过 hook viewDidAppear: 的方式来记录 PV。

解决办法:通过 UIScrollViewDelegate 的 scrollViewDidScroll: 和 scrollViewDidEndScrollingAnimation: 方法来监听 UIScrollView 的内容偏移事件,根据 contentOffset 计算当前显示的视图属于哪一个控制器,最后手动触发控制器的 viewDidAppear: 方法。

b. 一些页面需要避免被采集

一些用于调试的页面,或经产品确认不参与采集的页面,通过下发 ignore list 的方式来过滤。

B. 元素标识

理论上,页面中所有可交互的元素都应该能够被采集到。但考虑到 App 交互的多样性和现实成本,这里仅讨论支持点击操作的元素。

通常,元素标识由三部分组成

1. 元素在页面视图树中的路径

路径由视图树根节点开始,到该元素节点的父节点为止。例如:假设视图中有一个按钮控件,那么它的路径可以表示成如下形式:

UIWindow-UITransitionView-UIDropShadowView-UILayoutContainerView-UITransitionView-UIViewControllerWrapperView-UILayoutContainerView-UINavigationTransitionView-UIViewControllerWrapperView-UIView

2. 元素的类型名称 + 索引

以上述按钮为例:它的类型名为 UIButton,索引为其在父视图中的添加顺位。

3. 元素的内容

元素的内容可能是文本、图片、其他包含图片或文字的子元素组合。类似于 UILabel、UIImageView 这样的元素,直接获取其文本信息或图片 URL 即可。对于 UIButton,获取其 currentTitle 文本或 UIControlStateNormal 状态下的图片 URL。文本内容优先于图片内容。

对于图文并存,或包含子元素组合的元素,需要根据元素类型及圈选方式确定,且元素内容标识需要单独生成。在 元素内容 一节中有详细介绍。

上述按钮的完整标识可以表示如下:

UIWindow-UITransitionView-UIDropShadowView-UILayoutContainerView-UITransitionView-UIViewControllerWrapperView-UILayoutContainerView-UINavigationTransitionView-UIViewControllerWrapperView-UIView-UIButton(0)_[click me]

UIButton 后面小括号中的数字”0”表示其在父视图中的索引,中括号内的 “click me” 来自其 currentTitle 的值。

元素索引的添加时机

建议只从视图控制器所在的视图开始添加元素索引。系统内置的视图,如 UITransitionView 会在运行时修改其子元素的索引,造成元素路径发生变化,因此在进行路径追溯时,到达 UIViewController (注意不含 UITabBarController 和 UINavigationController) 就不再添加索引。

独立元素与可重复元素的路径

独立元素是指在视图中独立绘制的元素,通常与其他元素无关联。对于此类型元素,标识定义为:”路径”“类型 + 索引”[“内容”]。

可重复元素是指在列表中绘制的元素。在 iOS 中只考虑 UITableViewCell 和 UICollectionReusableView。通过元素在父视图中的 indexPath 来确定元素的索引,即 (indexPath.section-indexPath.row),那么可重复元素的路径可以定义为:

... UIView-UITableView(0)-UITableViewCell(indexPath.section-indexPath.row)

元素内容

我们将元素内容的分为图片和文本两类。文本类内容可以从控件的 text、title 等属性获取,这里不再赘述。图片内容的获取,有 2 种方式:

  • 通过 imageNamed: 方法设置的图片,通过 description 方法打印其信息,可以得到类似如下结果:

    <UIImage:0x6000005de2e0 named(main: home_search_icon) {24, 24}>

这里的 “main: home_search_icon” 表示图片名称为 “home_search_icon”,来自 “main bundle”。我们可以截取 “main: home_search_icon” 作为图片内容。

如果通过 description 方法打印的信息如下:

<UIImage:0x6000005a1f80 anonymous {75, 75}>

这说明图片是通过其他方式进行设置的,需要通过第二种方式来获取其内容。

  • 通过 SDWebImage 等三方库设置了 UIImageView 的 URL,可以直接在运行时获得其关联属性

    NSString *imageContent; SEL sel = NSSelectorFromString(@"sd_imageURL"); if ([self respondsToSelector:sel]) { NSURL *url = [self performSelector:sel]; if ([url isKindOfClass:[NSURL class]]) { imageContent = url.absoluteString; } }

单一内容、复合内容以及内容标识

如果一个元素只含有一项文本或图片,则称这个元素的内容为单一内容。单一内容本身作为其内容标识。

如果一个元素包含多个文本或图片、或其子元素内也包含文本或图片,则称其内容为复合内容。我们对复合内容进行遍历,遍历结果按键值对保存:

{
	"UIView-UILabel(0)": "text 1",
	"UIView-UIImageView(1)": "main: search_icon",
}

其中,key 对应的是子元素相对路径,作为改内容的内容标识,即从当前元素到子元素的路径,value 对应的是该内容具体的文本或图片内容。

对于具有复合内容的元素,有时会对其中某一项内容进行统计,该内容的内容标识可以参与到事件匹配。

考虑到性能影响,一个元素的内容遍历深度一般不超过 5。

事件匹配规则

我们通过定义事件匹配规则来对事件进行过滤,符合匹配规则的事件被认为是需要进行统计的。匹配规则实质上是对页面标识、元素标识、元素内容定义的一系列正则表达式。将用户行为相关的页面、元素标识、元素内容与事先定义的正则表达式进行匹配,匹配成功则进行统计。

正则表达式符号定义:

为了简化正则表达式的书写,我们将正则表达式中需要精确匹配的字符串进行如下约定:

  • fixedPrefix:表示固定的前缀字符,元素的路径需要精确匹配
  • fixedSuffix:表示固定的后缀字符,元素的索引或其他需要精确匹配的字符
  • fixedStr:表示固定的完整字符,元素的标识或内容需要精确匹配
  • fixedSection:在可重复元素中表示固定的 section,可重复元素的 section 索引需要精确匹配
  • fixedRow:在可重复元素中表示固定的 row,可重复元素的 row 索引需要精确匹配

假设我们要采集一个元素的标识为

UIWindow-UITransitionView-UIDropShadowView-UILayoutContainerView-UITransitionView-UIViewControllerWrapperView-UILayoutContainerView-UINavigationTransitionView-UIViewControllerWrapperView-UIView-UITableView-UITableViewCell(1-2)_[null]

根据上述约定,我们可以定义如下正则表达式来采集该元素:

^UIWindow-UITransitionView-UIDropShadowView-UILayoutContainerView-UITransitionView-UIViewControllerWrapperView-UILayoutContainerView-UINavigationTransitionView-UIViewControllerWrapperView-UIView-UITableView-UITableViewCell(1-2)_[[\S|\s]+\]

用 fixedPrefix 表示 ”UIWindow-UITransitionView-UIDropShadowView-UILayoutContainerView-UITransitionView-UIViewControllerWrapperView-UILayoutContainerView-UINavigationTransitionView-UIViewControllerWrapperView-UIView-UITableView-UITableViewCell”

用 (fixedSection-fixedRow) 表示 “(1-2)”

用 fixedSuffix 表示 “_”

可以得到简化后的正则表达式

^fixedPrefix\(fixedSection-fixedRow\)fixedSuffix\[[\S|\s]+\]$

该规则表示:匹配某个视图列表中 section 为 1,row 为 2 的元素,不关注内容变化。

基于正则表达式的事件匹配规则

  • 页面匹配规则

根据页面标识进行精确匹配即可。

  • 独立元素匹配规则

    • 当前位置
      关注元素的绝对位置,不关注元素内容。如果元素位置发生变化,则不纳入统计。元素标识匹配正则表达式为:^fixedPrefix[[\S|\s]+]$。

    • 当前内容
      关注元素的内容,允许元素在其父视图内的索引发生变化。如果元素内容发生变化,则不纳入统计。单一内容的元素标识匹配正则表达式为:^fixedPrefix(\d*)fixedSuffix

      ,其中fixedSuffix直接涵盖了元素内容;复合内容的元素标识匹配正则表达式为:fixedPrefix(\d∗)[[§|\s]+]其中 fixedSuffix 直接涵盖了元素内容;复合内容的元素标识匹配正则表达式为:^fixedPrefix\(\d*\)[[\S|\s]+\]$,且内容标识正则表达式为:^fixedStr$。
  • 可重复元素匹配规则

    • 不关注内容
      • 同类元素
        关注列表中同一 section 内的所有元素。当用户点击任一元素时产生的事件都会纳入统计。元素标识匹配正则表达式为:^fixedPrefix(fixedSection-\d*)fixedSuffix[[\S|\s]+]$。
      • 当前位置
        只关注列表中固定位置的某个元素。只有当用户点击该元素时产生的事件才会纳入统计。元素标识匹配正则表达式为:^fixedPrefix(fixedSection-fixedRow)fixedSuffix$。
    • 关注内容
      • 同类元素
        关注列表中同一 section 内的所有元素,且对指定内容进行聚合统计。元素标识匹配正则表达式与不关注内容的表达式一致:^fixedPrefix(fixedSection-\d*)fixedSuffix[[\S|\s]+]。内容标识正则表达式为: ^fixedPrefix\[[\S|\s]+\]
      • 当前位置
        只关注列表中固定位置的某个元素。只有当用户点击该元素时产生的事件才会纳入统计,并且对当前位置元素的指定内容进行统计聚合。元素标识匹配正则表达式为:^fixedPrefix(fixedSection-fixedRow)fixedSuffix。内容标识正则表达式为 ^fixedPrefix\[[\S|\s]+\] 。该规则适用这样的场景:运营人员想查看列表指定元素的内容对点击率的影响。
      • 当前内容
        只关注列表中固定位置的某个元素,且该元素的某项内容不能发生改变。位置和内容任意一项发生变化,则不纳入统计。元素标识匹配正则表达式为:^fixedPrefix(fixedSection-fixedRow)fixedSuffix。内容标识正则表达式fixedPrefix。内容标识正则表达式为^fixedPrefix
  • 元素、内容匹配规则表

从 0 到 1 搭建技术中台之 iOS 可视化埋点实践

前后端配合方式的选择

前端匹配

  • 工作方式

    • 圈选配置由服务端统一下发到 App
    • App 根据圈选配置进行匹配采集,将采集到的用户事件上报服务端
    • 服务端进行数据统计处理,生成报表
  • 优点

    • App 只上报被圈选匹配的事件,上报数据量小
    • 服务端只负责圈选配置的下发同步,实现较为简单
  • 缺点

    • 数据统计具有滞后性,依赖圈选配置下发的覆盖程度
    • 无法追溯历史,即无法统计圈选配置生效前发生的事件
    • App 端需要考虑匹配计算对性能的影响

后端匹配

  • 工作方式

    • App 全量采集用户行为事件
    • 服务端根据圈选配置,结合全量采集的事件进行匹配过滤
  • 优点

    • 可以支持实时统计
    • 可追溯历史,即可以统计圈选配置生效前的历史数据
    • App 不做匹配过滤,对性能无影响
  • 缺点

    • App 全量采集的数据量大,需考虑对用户流量的影响
    • 服务端做匹配过滤工作涉及的计算量较大
    • 服务端存储全量采集数据涉及到的存储空间较大

伴鱼的选择

  • 尽可能不影响用户体验。如果匹配规则较多,在端上做匹配过滤势必会对性能有所影响。因此仅在提供了圈选配置功能的 App 上支持前端匹配功能。
  • 实时性、可追溯这两个特性,对于产品和运用来说异常重要,不能妥协。
  • 全埋点采集的数据对于用户流量的影响并不高。根据伴鱼绘本的经验,单个用户平均一天产生的行为数据不超过 5M,相当于上传了一张高清图片。
  • 服务端的存储资源可以定期清理。
  • 服务端的计算资源的问题可以通过弹性扩容的方式解决。

圈选验证

产品或运营完成圈选配置后,需要验证圈选是否生效。App 可以通过集成圈选 SDK 来实现所见即所得的验证方式。如下图所示,符合匹配规则的页面和元素会以不同颜色高亮显示。

从 0 到 1 搭建技术中台之 iOS 可视化埋点实践

元素标识发生变化导致匹配规则失效时如何处理?

无论何种原因导致元素的路径或内容发生变化,最终会使得元素事件无法被事先配置的圈选规则匹配。有 2 种典型场景:

  • 产品需求迭代过程中的页面改版导致元素路径或内容发生了变更。在这种场景下,旧的圈选配置仍然生效,只需在新版本下手工增加新的圈选规则即可。 服务端进行数据统计时,可以按照 App 的版本进行区分。当 App 迭代到一定程度,较早前版本的圈选配置将失效。服务端可以定期对老版本的圈选配置进行清理。

  • App 在运行过程中,因业务条件发生变化导致页面布局或元素内容发生变更。例如,某些页面对于付费用户和非付费用户展示不同的布局效果。这其实和上述场景类似,需要在所有可能的用户场景下分别进行圈选配置操作。

某些元素的父视图层级固定,只是索引会发生变化,例如导航栏右上角的下拉菜单列表,列表中的元素顺序可能会变化,但都限定在菜单容器内。对于这种元素,我们可以在生成圈选配置时,限定元素的文本内容。只要内容不变,仍然能够被匹配命中。

总而言之,如果导致元素的标识变化的场景是可以被枚举的,我们只需枚举所有感兴趣的场景,然后分别进行圈选埋点;如果元素的视图层级固定,仅索引会变,我们可以根据元素内容进行限定,只匹配特定内容的元素;其他情况下建议直接使用代码埋点。

总结

本文尝试阐述这样一种理念:通过全埋点的方式采集用户行为,通过正则匹配的方式构建统计规则,最终为产品决策提供数据支持。圈选埋点技术有效地提高了研发效率,让产品和运营能够更直观便捷地定义指标;但对于复杂的业务场景,代码埋点仍然不可或缺。

参考:

GrowingIO JavaScript SDK 的实现细节和扩展
网易 HubbleData 无埋点 SDK 在 iOS 端的设计与实现