实现埋点上报方案过程中的思考

9,336 阅读8分钟

任何一个重视数据的公司与部门,都不会忽略前端埋点数据上报的作用。

从直观上看,前端的埋点上报方案有啥难的呀?不就是将用户行为数据收集上来,就可以了么?

从实际开发经验来回答:实际上并没有那么简单。为了能够将收集的用户数据最终展示并从中分析得出有意义的数据,这其中涉及的细节比想象的要多。

在本文中,我们会从两个方面来思考数据埋点上报的过程:数据收集与数据上报。

数据收集

对于数据收集的过程,我们不会对数据上报方式做区分(如常见的埋点上报与无埋点上报),而重点思考不管哪种上报方式都通用的技术点。重点考察如下基本的数据信息:UV、PV、应用浏览总时长。

UV

针对UV的统计维度有两个:注册用户与未注册用户。

注册用户好说,只需要有应用中注册用户的唯一标识符即可。

对于未注册用户,如何识别这是一个用户呢?在这种情况下,问题会转化为:如何识别一个设备?

如何识别一个设备?

最容易想到的就是利用浏览器的存储功能:cookie、localStorage等。具体做法是:在用户首次加载页面的时候,将UUID(如随机生成的字符串)放入到存储中,这种方案是最简单的,其中:

Cookie 有生命时长限制并且有容量大小的限制。

localStorage 会永久保存,并且在容量方面比cookie大得多。

所以,使用localStorage就会比cookie更有效。但是,基于浏览器存储功能的方案,有一个天生的缺陷:用户可以选择主动删除存储

这个时候,开发者就会想:是否能够有一个真正的与设备相关的唯一标识符呢?

于是有了浏览器指纹追踪技术:类似人的外貌和指纹,Web客户端(这里主要指浏览器)也有多种“外貌”信息和“指纹”信息,将这些信息综合分析计算后,可对客户端进行唯一性识别,进而锁定、追踪、了解网民行为和隐私数据。

在 WA 中,引用了公司内部的另一个基于浏览器指纹追踪库作为识别同一个浏览器访问的唯一标识符。经过咨询该库开发者 ,发现浏览器指纹技术在实际应用中的效果并不好(无法识别的概率很高):

前提一:   只针对浏览器,换了浏览器即使同设备,id也不一样。
前提二:   用ip区分用户,不同ip认为是不一样的用户。

统计结果: 18年我们在生产环境做了测试,ios 7800个用户中,有22%的用户id未发生重复id。安卓15000个用户中,有39%未发生重复id。

原因分析: 因为浏览器拿不到设备底层数据,只能通过一些浏览器的配置参数去生成id。如果用户手机系统 型号 浏览器版本和相关配置一样,那生成的就一样。
结论

经过综合考虑,我们放弃了浏览器指纹追中技术。在我们设计的埋点方案中,非注册用户的UV统计方式采用的是植入UUID到localStorage的方案。

PV

当页面发生改变时,我们就会在PV计数上增加1。对于此,我们想问:如何知道页面变化了?

这个问题,可以细分为两个小问题:

  1. 浏览器自己是否可以完全知道页面发生了变化?
  2. 开发者自己是否可以完全知道页面发生了变化?

第二个问题,答案是肯定的。开发者当然了解页面啥时候发生了变化。但是第一个问题,考虑浏览器是否可以知道页面发生了变化的时候,有趣的点就来了:

浏览器认为的页面变化是否就是我们所需要的页面变化呢?

对于多页面应用,通过侦听 window.onLoad 事件与 window.onunload 事件,或者侦听URL的变化 onhashchange 即可。浏览器认为的页面变化就是我们所需要的页面变化。

对于单页面应用呢?这可就不一定了。

因为是单页面应用,那么只会有一次页面加载事件的触发。也就是说,无法通过侦听 window.onLoad 事件与 window.onunload 事件来实现。

那么,通过侦听URL的变化可行么?若应用中存在多层嵌套路由,当URL变化了,不一定代表着页面发生了变化。

这个时候怎么办呢?我们来看看大厂们的做法:

Google Analytics:

ga网页浏览说明

GrowingIO:

企业微信截图_13b09295-1318-4cc0-91c6-1b53ea0e289d

易观方舟:

企业微信截图_52c8297c-f8e4-457c-9933-95ab1e806215

针对单页面应用时的PV统计,基本都提供了这样的方式:

1.配置信息:是否启动基于URL变化的自动页面收集方式。

2.手动上报:提供接口主动上报页面变化。

所以,在我们的实现中,也基于这样的方案来实现。

应用浏览总时长

在应用浏览时长的统计阶段,关键的细节在于如何准确了解应用被关闭了。我们来看正常情况和非正常情况。

正常:在正常应用关闭的操作中,我们可以侦听 window.onunload 事件即可。

非正常:那么非正常情况呢?比如这些:

  • 浏览器闪退

  • 用户很长时间未操作的情况下(比如看视频),浏览器突然被闪退

  • H5 页面/小程序的客户端闪退/被杀死

这些异常情况下,该如何处理呢?

一种推荐的方案是:WebSocket。前端SDK与后台建立一个webSocket持久性连接。webSocket连接中的心跳检测是必备选项,用于应对网络不稳定。当应用被关闭的时候,服务器端可以知道这个关闭事件。缺点:WebSocket方案有一定的兼容性问题。

(ps: webSocket的更多细节原理并未亲自实现)

在我们的应用中,是基于 WA 上报数据。它并没有这样一个webSocket的持久性连接提供出来。该怎么办呢?

现阶段是这么做的:

思考:在客户端被杀死或者崩溃退出的情况下,web 页面本身并没有合适的事件能够精确监听。

那么,采用近似的思路来趋近:在现有的方案中通过设置一个合适时长的定期上报事件(定时心跳上报),通过获取本次会话的最后上报时刻,可近似理解为是用户关闭页面(最大误差为所设置的心跳上报间隔)。

对于这种方案的实现,需要数据统计端的配合:在计算应用总时长的时候,需考虑心跳上报因素。

数据上报

对于一个埋点方案来说,数据上报有两个点需要着重考虑:

  • 对跨域做特殊处理。
  • 页面销毁后,如何还能够将未上传的埋点数据成功上报?

传统的XHR发送数据请求的方式,对上面提到的两个点都无能为力。在数据上报过程中,较为常用的有两种方式:

  • 动态创建img标签,在 img.src 中拼接url的方式发送请求
  • 调用 sendBeacon 接口发送数据

Major tracking upgrade - sendBeacon(), etc! - Clicky Blog

Img 标签

利用img标签的src属性发送请求的方式,具体执行方案如下:

let _img = new Image();
_img.src = `${url}?${util.spliceParam(params)}`;
_img.onload = _img.onerror = function() {}

它非常契合埋点数据上报这个应用场景:

① 只上报的数据不需要接收响应;

② img的src属性天然地不存在跨域问题。

这种使用方式也存在缺陷。首先对于src 中的URL内容是有大小限制的,太大的数据量不适用。详细看这里。其次,在页面卸载的时候,若存在数据未发送的情况,会先将对应的数据发送完,再执行页面卸载。这种情况下,会在体验上给使用者带来不方便。

此时的 sendBeacon方法就是来解决上面提到的缺陷的。

sendBeacon

sendBeacon方法是一个异步、非阻塞的数据传输方法。具体使用方式如下:

window.navigator.sendBeacon && window.navigator.sendBeacon(url, params)

它的特点是:

  • Beacon请求是Post方式。
  • Beacon请求优先避免与关键操作和更高优先级的网络请求竞争。
  • Beacon请求可以有效地合并,以优化移动设备上的能量使用。
  • Beacon保证页面卸载之前启动信标请求,并允许运行完成且不会阻塞请求或阻塞处理用户交互事件的任务。
  • 返回值:sendBeacon 方法被执行后返回一个布尔值,true代表用户代理成功地将信标请求加入到队列中,否则返回false

对于sendBeacon方法,它的局限性体现在:

  • 不能跨域,需要服务端设置。
  • 新特性接口,兼容性存在问题。

因此,在实际的应用过程中,需要针对实际情况,结合 Img 标签 与 sendBeacon 的方式一起使用。

总结

在本文中,我们分别讨论了在埋点上报阶段的两个方面:数据收集与数据上报。这其中,对于数据收集过程中的一些值得留意的情况,我们做了分析。同时,与你分享了两种常用的数据上报方式。希望对你有用!