APP 埋点那些事

1,888 阅读13分钟

本文内容根据神策大数据技术直播系列第一季第二讲《数据采集与埋点》整理而成。
▼▼▼
主持人(刘鑫):来到神策以后参与的一个项目,就是灼洲主导的可视化全埋点,而我来神策之前其实有很长时间,都是类似神策 SDK 这种产品的用户,这个领域从多方面来说,对我都是一个很亲切的话题,我这边看不到大家的入场的情况,不过七点钟就正式开始了,接下来我那把话筒交给灼洲老师。
讲师(王灼洲):大家下午好,我叫王灼洲,是神策 SDK 研发负责人。今天非常高兴能有机会跟大家一起探讨埋点。今天主要跟大家分享埋点的整体概况,涉及到的内容比较多,因此不会深入探讨具体细节。
神策数据采集 SDK 属于开源项目,任何个人和组织都可以在 GitHub 上看到我们的源码(https://github.com/sensorsdata/)。
我们的 SDK 全端覆盖,包括 Android、iOS、Web,以及主流的后端开发语言如 Java、C++、C、PHP、.NET、Node、Go、Python、Ruby 等,还有小程序如微信、百度、字节跳动、支付宝、QQ、快应用等,同时也覆盖了主流的跨平台开发框架如 React Native、APICloud、mPaaS、Flutter 等。
数据分析的常见应用场景:
  • 用户画像
  • 个性化推荐
  • 反作弊
  • 精准广告
  • 在线分析
  • 搜索优化
  • 文本挖掘
数据采集的数据类型可以通过两个维度划分。
按照数据类型:用户行为数据、用户相关数据、业务相关数据、文本类型数据
按照数据的所有者:第一方数据、第三方数据
▼▼▼
数据采集到底重不重要?神策成立 5 年来,我们已服务 1000+ 家企业客户,我们发现一个现象:很多客户的产品迭代速度特别快,基本上是两周一个版本,甚至一周一个版本。在这么短的周期内,既要做业务开发,又要进行埋点,着实不容易。若两者产生冲突,更多的客户会选择放弃埋点。
数据分析的流程大概是:数据采集 → 数据传输(实时 / 批量) → 数据建模(存储) → 数据统计 / 分析 / 挖掘 → 数据可视化(反馈),从这个流程中我们可以发现,数据采集是数据分析的基本,更是源头,数据采集与业务开发同等重要。
一个完整的埋点流程应该是什么样的?整体上与产品研发流程是类似的。
在神策分析中,我们使用事件模型(Event 模型)来描述用户在产品上的各种行为,这也是神策分析所有接口和功能设计的核心依据。简单来说,事件模型包括事件(Event)和用户(User)两个核心实体。
事件 Event,就是谁在什么时间、什么地点,以什么样的方式干了什么。对于用户 User 则是这个事件发生的主题。

▼▼▼
在进行埋点的时候,我们应该遵循「大」、「全」、「细」、「时」四个原则。
「大」,主要是考虑到随着我们业务的发展,用户规模和数据规模会指数增长,不仅体量大,更是我们积累数据资产的重要前提。
「全」,主要是指多端采集。比如神策的 SDK 种类比较多,会覆盖主流的平台和开发语言。只有这样,我们再进行数据分析时,就可以做到全量用户行为分析,而不是抽样分析。同时,我们采集到的用户行为数据,也能做到贯穿用户的整个生命周期。
「细」,主要是指我们可以采集更全面的属性和更细的粒度,让采集的数据更全面、更优质,同时更能满足我们更精细化的分析需求,更方便我们积累数据资产。
「时」,主要是指实时性。比如,用户搜索了“牙疼”,但我们的数据分析结果一周后才出来,而此时用户牙疼可能“康复了”。神策非常注重实时性,考虑到技术和成本,神策会最大限度的提高实时性,从而确保数据分析应用的时效性。目前,从数据采集到数据分析并展现出来,神策已达到分钟级别。
数据采集 SDK 的功能,概括起来就三点:
  • 通过埋点来采集数据
  • 将采集到的数据传输到指定的服务器端
  • 最大限度的保证数据的准确性和完整性
常见的埋点方式:
  • 代码埋点
  • 全埋点(无埋点)
  • 可视化全埋点(圈选)

『代码埋点』

代码埋点即在我们的产品内,当用户的某个行为发生的时候,调用 SDK 的接口触发事件。
代码埋点优点:
  • 精准控制埋点(方便、灵活的自定义事件和属性)
  • 采集数据丰富
  • 可以满足更精细化的分析需求
代码埋点缺点:
  • 埋点代价比较大
  • 需要伴随着 APP 发版

『全埋点』

全埋点也叫无埋点、无码埋点、自动埋点、无痕埋点,即无需研发工程师写代码或者写少量代码,即可达到自动(预先)埋点的目的。全埋点的宗旨,既要满足客户实际业务分析的需求,又要减少客户埋点的代价。
全埋点可以采集的事件:
  • 激活
  • APP 启动
  • APP 退出
  • APP 页面浏览
  • 控件点击
  • 曝光
激活即用户安装 APP 后的首次启动。对激活的判断,Android 和 iOS 面临的最大挑战,就是如何解决 APP 卸载重装的问题。通常情况下,都是通过去唯一标识一台设备,比如 Android 可能会采用 AndroidID,iOS 可能会采用 IDFA 等。激活事件一般会携带渠道信息,比如当前用户是从哪个渠道来的。
APP 启动,即点击应用图标,或者其他方式,让 APP 运行并在前台展示。
启动事件需要采集的属性:
  • 是否首次启动(安装后的首次启动)
  • 冷启动、热启动(从后台恢复)
  • 启动来源
启动来源包括:
  • 用户点击图标启动?
  • 从 Web 跳转?
  • 从小程序跳转?
  • 从其他 APP 跳转?
  • 用户点击推送唤醒?
  • 通过 Deeplink 唤醒?
对于 APP 退出的常规定义是 APP 不在前端显示。这个定义,可以覆盖绝大部分的业务场景,但对于播放器类型的 APP,这个定义就不再适用了:我们让播放器进入后台,但仍在继续使用 APP,此时到底算不算退出呢?因此,对于不同的业务类型场景,其定义也略有差异。APP 退出事件采集的属性:
  • APP 使用时长
  • 退出方式
其中退出方式包括:按 HOME 键让 APP 进入后台、把 APP 强杀、APP 发生崩溃、跳转到其他 APP。
不同的业务,对曝光的定义也不同。比如有一个列表页面,有的认为只要列表从服务端拉取到 APP 端就算曝光了,而有的认为,只有每一项在 APP 端显示出来了才算曝光。
适合采集曝光的场景:
  • 广告位
  • 列表
  • Toast(吐司)
  • Dialog(对话框)
对于 Android,页面是指 Activity、Fragment;对于 iOS,页面是指 UIViewController。
页面浏览事件采集的属性:
  • 页面标题($title)
  • 页面名称($screen_name)
  • 前向地址($referrer)
  • 自定义属性
对于 APP 而言,用户频率最高的行为就属点击控件。因此,全埋点最重要的工作就是采集控件点击。
控件点击事件采集的属性:
  • 哪个控件被点击了
  • 控件是什么类型
  • 控件所属哪个页面
  • 控件上显示的文本信息
  • 扩展自定义属性
全埋点,默认情况下是全量采集,即默认采集所有页面上的所有控件的点击,但有一些点击确实是业务分析不需要的,这样不仅会占用带宽也会占用大量存储空间。因此,全埋点采集的点击事件,还需要提供丰富的 API 来对点击事件进行灵活配置,比如:
  • 不采集哪些页面上的 $AppClick 事件
  • 不采集哪些类型控件的 $AppClick 事件
  • 不采集某个特定控件的 $AppClick 事件
  • 给控件的 $AppClick 事件添加自定义属性
全埋点优点:
  • 埋点代价比较小
  • 无需更新 APP
  • 解决数据「回溯」的问题
  • 其他高级功能的强依赖(可视化全埋点、点击图)
全埋点缺点:
  • 覆盖的功能有限
  • 无法自动采集业务相关的数据
  • 无法满足更精细化的分析需求
  • 兼容性问题
  • 传输的数据量大、浪费资源
对于 Android 和 iOS,其全埋点原理略有差异。由于实现细节比较多,在此仅仅做一个简单的介绍,更多细节可参考我们的 SDK 源码(后台回复【SDK源码】即可)。
关于 iOS 全埋点更多的技术细节,可参考《iOS 全埋点解决方案》一书。
▼▼▼
关于 Android 的启动和退出,也需要考虑多进程的问题,神策目前对 Android APP 启动和退出的定义如下:
启动:若一个页面显示出来了,与上一个页面退出的时间间隔超过了 30s,则算一次启动。
退出:若一个页面进到后台,30s 之内没有新的页面显示,则算一次退出。
对于 Android 采集控件点击事件的技术也比较多,比如 AspectJ、ASM、 Javassist、AST 等 8 种方案,神策目前是采用 ASM。关于其他方案的技术原理,可参考《Android 全埋点解决方案》一书。

『可视化全埋点』

可视化全埋点,也叫圈选,即通过可视化的方式对全埋点进行配置,比如:
  • 设置 / 修改控件 ID
  • 设置 / 修改控件“内容”
  • 设置 / 修改控件所属页面的页面标题
  • 控制不采集哪些控件的点击事件
  • 添加静态属性
  • 重命名点击事件名
可视化全埋点的演示,可参考下图,更详细的体验,可申请神策的 Demo。

可视化全埋点的技术原理,主要依赖三个概念:
  • 截图
  • ViewTree
  • ViewPath
截图:即 APP 当前页面的截图。
ViewTree:Android 和 iOS 的页面跟网页一样,其结构都可以用一棵树来表示,这棵树我们叫 ViewTree。在 ViewTree 里,会包含控件的层级(父子)关系和控件可视化信息,比如控件类型、坐标、长宽、显示文本等。
ViewPath:在 ViewTree 里,任何一个控件,从根节点到当前节点的路径都是唯一的,这个路径称为 ViewPath。
当 APP 连接神策分析后,SDK 会定时(每隔一秒)将 APP 当前的截图和对应的 ViewTree 信息发给服务端,前端以截图为背景,根据用户鼠标点击的位置在 ViewTree 里找到对应控件的 ViewPath,再根据 ViewPath 里的元素信息绘制元素矩形框,同时通过 ViewPath 在系统里唯一标识当前控件,最后与控件点击事件采集的 ViewPath 进行匹配。
点击图与可视化全埋点类似,均是以上技术不同的应用,他以一种更直观的方式展示用户在当前页面上的点击情况。

▼▼▼
面对这么多埋点方式,我们到底选用哪一种?
神策这 5 年来,已服务 1000+ 家企业客户,通过深度服务客户,我们发现其实并没有一种非常完美的埋点方案,它们各有优缺点。
面对这么多的埋点方案,不能一味追求省事,更不能追求埋点方式的「酷炫」,最主要的还是要根据实际的分析需求和业务场景,选择最能满足我们需求的埋点方式。若有多种埋点方案都能满足,我们可以再追求「省事」和「酷炫」的方案。
在这 5 年来,我们也积累了一些经验,借此机会跟大家分享一下。
『设备 ID』
用户在产品上登录之前,我们需要唯一标识当前设备,也即设备 ID。对于 Android 和 iOS,设备 ID 也略有差异。
1、iOS 设备 ID
UDID
即 iOS 设备的唯一识别码,是 Unique Device Identifier 的缩写。它由 40 位十六进制组成的序列。从 iOS 5 开始,苹果为了保护用户的隐私,就禁止应用程序通过代码获取 UDID了。而且还对外宣称,一旦发现应用程序继续获取 UDID,将禁止上架 APP Store。所以不能保证唯一识别该设备。
UUID
即通用唯一标识符,是 Universally Unique Identifier 的缩写。由 32 位十六进制组成的序列,使用小横线来连接,格式为:8-4-4-4-12。如果用户删除该应用再次安装时,又会生成新的字符串,所以不能保证唯一识别该设备。
Mac Address
用来表示互联网上每一个站点的标识符,采用十六进制数表示,共 6 个字节(48位)。在 iOS 7 之后,Mac Address 也属于用户隐私。如果请求 Mac Address都会返回一个固定值:02:00:00:00:00:00。所以不能保证唯一识别该设备。
IDFA
即广告标示符,是 Identifier For Advertising 的缩写。适用于对外:例如广告推广、换量等跨应用的用户追踪等。由 32 位十六进制组成的序列,格式与 UUID 一致。在同一个设备上的所有 APP 都会取到相同的值。在 iOS 10.0 以后,当用户开启限制广告跟踪,获取的值为:00000000-0000-0000-0000-000000000000。比较适合作为设备 ID。
IDFV
即 Vendor 标示符,是 Identifier For Vendor 的缩写,适用于分析用户在应用内的行为等。也是一个由 32 位十六进制组成的序列,格式与 UUID 一致。每个设备在所属同一个 Vendor 的应用里,都有相同的值。如果用户将属于此 Vendor 的所有 APP 卸载,则 IDFV 的值会被重置。
最佳实践
采用 IDFA/IDFV + Keychain 的方式,数据并不是存放在 APP 的 SandBox 中,即使删除了 APP,数据依然保存在 Keychain 中。如果重新安装了 APP,还可以从 Keychain 获取数据。可以有效的解决卸载重新安装新增的问题。
2、Android 设备 ID
IMEI
需要 android.permission.READ_PHONE_STATE 权限,这个方法只对有电话功能的设备有效。Android Q 开始,非系统应用或者不在白名单内,无法获取 IMEI。不适合作为设备 ID。
Serial Number
可以通过 String SerialNumber = android.os.Build.SERIAL 方式获取。不需要权限,部分机型可能会返回 0123456789ABCDEF,最新的 Android OS 可能会限制读取。不适合作为设备 ID。
Mac Address
需要权限,Android 6.0 后会返回固定值: 02:00:00:00:00:00。不适合作为设备 ID。
最佳实践
使用 AndroidID 作为设备 ID。在设备首次启动时,系统会随机生成一个 64 位的数字,并把这个数字以 16 进制字符串的形式保存下来。不需要权限,平板设备通用,缺点是设备恢复出厂设置会重置,但已最大限度的满足实际分析需求。也要考虑 Android Q,同一个设备,不同的 APP 获取到的 AndroidID 不同。

『时间』

在数据采集时,和时间相关的有两个:
  • 记录事件发生的时间戳
  • 统计某个行为持续的时长
如果我们以客户端时间为准,可能会有如下两个问题:
  • 统计事件时长 ($event_duration) 不准,为负、超大
  • 事件发生时间 (time) 不合理,超前、超后
可能会有同学说,为什么不用服务端时间呢?为了提高效率和减少数据丢失,客户端采集的事件一般是先缓存在本地,待符合一定的策略之后再同步。
又会有同学说,为什么不通过服务端校准客户端时间呢?既然是校准,就需要网络请求,如果没有网络怎么办?离线的 APP 怎么办?在校准之前的事件或者时长统计怎么办?
因此,以上两种方案均无法兼容各种场景。下面,我们介绍神策的解决方案。
1、统计时长
Android 和 iOS,都有一个 boot time 的概念,即手机启动了多长时间。可以理解为系统里有一个定时器,每次手机启动后,这个定时器就从零开始计时,与客户端时间无关,如果我们使用这个 time 来计算间隔,就是 100% 准确的。
// Android
SystemClock.elapsedRealtime()
// iOS
NSProcessInfo.processInfo.systemUptime
2、事件发生的时间戳
我们采用的是“时间纠正”这个策略。
用户在手机时间戳 T1 时触发了一个事件(比如登录事件),然后事件存入本地缓存。数据采集 SDK 在用户手机时间戳 T2 时开始同步事件数据(包含登录事件),然后服务端通过 HTTP(S) 的 Request Header Date 可以获取到数据采集 SDK 发起 Request 时用户手机时间戳 T2 。
如果 T2 与当前服务端的时间戳 T3 的误差在一定的可接受范围之内(比如 30s,这是因为网络请求也需要一定的时间),就可以说明当前用户手机的时间戳是准确的(我们假设服务端时间戳是准确的,绝大部分情况下也都是准确的),同时说明登录事件发生时的时间戳 T1 也是准确的,服务端在接收到登录事件时无需做任何的特殊处理。
如果 T2 与 T3 相差比较大(比如上图中的 T2 为 15:00,T3 为 16:00),我们可以确定当前用户手机的时间戳 T2 是不准确的,即比服务端的时间戳晚了一个小时(16:00 - 15:00 = 一个小时),从而就可以假设登录事件发生时的时间戳 T1 也比服务端晚了一个小时,即 14:00 加上一个小时应该为 15:00。
因此,服务端在接收到事件时会把登录事件的时间戳 T1 再加上一个小时,变成 15:00,从而就达到了“时间纠正”的效果。
当然,“时间纠正”策略,也无法确保可以 100% 解决所有和时间戳相关的问题。比如,还是以上图为例,如果在时间戳 T1 和 T2 之间,用户手机的时间戳再次发生了人为变更,那么,该方案就无法正确的进行时间纠正了,这是因为时间戳 T2 和 T3 之间的误差额,与时间戳 T1 和当时服务端对应时间戳的差额就不相等了。
但从实际情况来看,由于数据采集 SDK 一般都会以较短的固定时间间隔同步数据(比如 15s),所以时间戳 T1 和 T2 之间的时间间隔会比较短,本身发生时间戳变更的可能性就比较小,即使真正发生变更了,影响到的事件数量也比较少(15s 内可能只会触发几条事件)。因此,“时间纠正”策略可以解决 99% 以上的和事件时间戳相关的问题。
另外,由于“时间纠正”策略的实现逻辑都是在服务端处理的,而且实现起来也比较简单,在此我们不再详细描述实现细节。
『救火队员』
对于 SDK 来说,最核心的目的就是采集数据,但有一个前提,即不能够影响客户的正常业务。不管研发如何努力,QA 如何测试,总是会出现无法预料的异常。此时,我们就必须要有救火队员,即通过远程下发配置控制 SDK 的行为,比如关闭 SDK。

『同步策略』

为了最大限度的保证数据的完整性和准确性,SDK 采集的数据,一般会先缓存到本地,待符合一定条件之后再同步。
常见的数据同步策略:
  • 事件先缓存到本地 sqlite3
  • 本地缓存一定条数量的事件会同步(默认 100 条)
  • 固定时间间隔同步(默认 15 秒)
  • 核心事件发生同步(trackInstallation、login 等)
  • APP 退出尝试同步
  • 本地缓存太多事件时的处理
在 APP 退出时尝试同步数据,是为了防止用户之后长时间不启动 APP、不再启动 APP 或者直接卸载 APP。

『网络策略』

同步数据需要网络请求,不同的业务、不同的场景对网络策略要求也不同。比如,对用户体验要求较高的业务,有可能要求只有在 WiFi 网络下才去同步数据。因此,SDK 就需要支持可以灵活的配置网络策略,以满足不同的需求。默认情况下,在 3G、4G、5G、WiFi 网络下 SDK 会同步数据。
SDK 支持的网络配置如下:
  • NetworkType.TYPE_2G
  • NetworkType.TYPE_3G
  • NetworkType.TYPE_4G
  • NetworkType.TYPE_5G
  • NetworkType.TYPE_WIF
  • NetworkType.TYPE_ALL
  • NetworkType.TYPE_NONE

『H5 与 APP 打通』

近年来,iOS 混合开发越来越流行,APP 与 H5 的打通需求也越来越迫切。那什么是 APP 与 H5 打通呢?所谓“打通”,是指 H5 集成 JavaScript 数据采集 SDK 后,H5 触发的事件不直接同步给服务端,而是先发给 APP 端的数据采集 SDK,经 APP 端数据采集 SDK 二次加工处理后入本地缓存再进行同步。
APP 为什么要与 H5 打通呢?
主要是从以下几个角度考虑。
1、数据丢失率
在业界,APP 端采集数据的丢失率一般在 1% 左右,而 H5 采集数据的丢失率一般在 5% 左右(主要是因为缓存、网络或切换页面等原因)。因此,如果 APP 与 H5 打通,H5 触发的所有事件都可以先发给 APP 端数据采集 SDK,经过 APP 端二次加工处理后并入本地缓存,在符合特定策略之后再进行同步数据,即可把数据丢失率由 5% 降到 1% 左右。
2、数据准确率
众所周知,H5 无法直接获取设备相关的信息,只能通过解析 UserAgent 值获取到有限的信息,而解析 UserAgent 值,至少会面临如下两个问题:
1)有些信息通过解析 UserAgent 值根本获取不到,比如应用程序的版本号等。
2)有些信息通过解析 UserAgent 值可以获取到,但内容可能不正确。
如果 APP 与 H5 打通,由 App 端数据采集 SDK 补充这些信息,即可确保事件信息的准确性和完整性。
3、用户标识
如果用户在 APP 端注册或登录之前使用我们的产品,我们一般都是使用匿名 ID 来标识用户。而 APP 与 H5 标识匿名用户的规则不一样(iOS 一般使用 IDFA 或 IDFV,H5 一般使用 Cookie),进而就会导致一个用户使用了我们的产品,结果产生了两个匿名用户的情况。如果 APP 与 H5 打通,就可以将两个匿名 ID 做归一化处理(以 APP 端匿名 ID 为准)。
打通的场景比较多,也比较复杂,需要重点考虑以下几个常见:
  • 自家的 H5
  • 第三方 H5(非神策客户)
  • 第三方 H5(神策客户)
今天由于时间的关系,大概就跟大家分享这么多的内容。如果大家要对这方面有兴趣,或者说对于神策的产品有兴趣,我们可以在线下进行更进一步的交流。