一、背景
上一篇博客分析了Android上的埋点SDK技术原理,这次我看看Web页面上的埋点。Web页面上的埋点主要通过JS完成,在JS里面同样有代码埋点、全埋点、可视化埋点三种方案,如果对这几种方案的概念不了解可以看下上一篇博客。由于mixpanel-js和Sensors Analytics JavaScript SDK都开源了自己的SDK,就以它们为例进行分析。
二、代码埋点
以Mixpanel为例(源码位于/src/mixpanel-core.js
),看一下里面的实现。
2.1 基本用法
埋点之前,需要在head部分嵌入SDK,并调用SDK的初始化接口。以Mixpanel为例,官方介入文档提供的加载、初始化SDK代码如下:
|
|
这是一段立即执行的js代码,作用通常是去异步加载真正的JS SDK,然后调用SDK的初始化接口init方法,完成初始化的操作。
初始化的核心代码为
|
|
也可以简写为
|
|
看下init方里面几个参数的含义:
- 第一个参数是你在后台注册的app token
- 第二个参数是SDK的配置,传入了一堆key-value,如果不传,SDK内部也有个默认配置,长下面这样:
|
|
- 第三个参数是SDK全局变量名
Mixpanel接入文档:https://mixpanel.com/help/reference/javascript
2.2 上报的基本实现
代码埋点的方式通常都会被封装成类似track(eventName, properties)
的接口,例如在Mixpanel中,可以用mixpanel.track("Played song", {"genre": "hip-hop"});
来上报事件。
这里是整个SDK中最重要的地方,使用频率也是最高的。代码位于/src/mixpanel-core.js
里面,先撇开复杂的逻辑和条件控制,看一下track的基本实现,我稍微加了点注释:
|
|
上面就是事件上报代码的核心实现。但是由于Web应用自身的一些特性,比如在追踪页面跳转行为(链接的点击、表单的提交等)时,为了防止数据发送不及时导致的数据丢失,SDK中提供一些诸如track_links
和track_forms
特殊方法,这些方法内部用的其实是setTimeout或者等待服务器返回结果之后再让页面跳转。
三、全埋点
Mixpanel和神策都提供了名为“AutoTrack”的方案,只需要在初始化SDK的时候,传入一个参数即可打开这个功能。JS SDK可以自动监测网页中所有的点击、表单submit等事件,这和AndroidSDK里面监听所有按钮的点击有些类似。
3.1 自动监测的元素、事件类型
神策JS:设置AutoTrack之后,SDK就会自动追踪页面上的按钮(
a
、button
、input
) 这种html标签类型的点击情况,一旦页面某一个按钮发生了点击行为,SDK就会去采集此按钮的一些信息,例如: 这个按钮的标签类型,这个按钮的文本内容,这个按钮的name
,这个按钮的id
、class
名,还有一些按钮特有的属性如href
等。MixpanelJS:设置AutoTrack之后,SDK会监测页面上的所有
form表单
、input标签
、select和textarea标签
产生的submit
、change
、click
事件,并采集这些标签上的属性一起上报。
3.2 全埋点监测的实现
以Mixpanel为例,在/src/autotrack.js
代码中,把几个关键的方法扣出来看一下(不要问我为什么以Mixpanel为例,因为代码少一些。。。)。
当SDK初始化的时候,会执行autotrack里面的_addDomEventHandlers
方法,给整个document的submit
、change
、click
事件设置监听器。当监听到这几类事件时,会执行_trackEvent
方法。
直接看代码,我给代码里面加了一点注释,来说明自动监测上报的过程。
|
|
3.3 全埋点小结
可以看到全埋点还是有点暴力的,会采集的数据量也挺大,并且采集到的属性也比较多,可以看到在MixpanelSDK中,如果页面结构比较深,那么数据报过去分析起来可能还是需要花点时间的,同时也会产生大量可能不会使用的数据,对资源也是一种浪费。在神策SDK的接入文档中也提到,建议那些按钮不是很多的,相对简单的页面可以采用这个方法。一般情况下,如果网页上的按钮比较多的话,因为每次按钮的点击都会发数据,数据量很大。
四、可视化埋点
Mixpanel和神策等平台,都提供了JS可视化埋点功能,与全埋点相比,这种方式可以指定自己想要监测的元素和属性(所有可以点击的元素),既可以做到动态配置,又不会像全埋点那样产生大量的数据(但也有例外,比如Mixpanel的可视化埋点仍然上报了全量点击数据,只是在后台根据可视化配置过滤出实际的数据)。
可视化埋点首先需要进入埋点模式,以Mixpanel为例,可视化埋点的入口在后台管理界面,需要在后台输入需要埋点的页面url,然后再进入我的Web页面,此时就会加载可视化标记的编辑器(代码见autotrack.js
中的_maybeLoadEditor
方法,需要注意的是这个页面必须已经嵌入了JS SDK)。
这里一定要从平台登录才可以进入可视化编辑状态,这实际上是对安全性的一个保证,试想如果只要嵌入SDK就可以做可视化埋点,那岂不是我的Web应用随便就可以被别人埋点,对我的数据产生干扰了。在Mixpanel JS SDK内部,通常会判断当前页面的sessionStorge/localStorage中是否有一个开启可视化编辑器标志字段(例如Mixpanel是
_mpcehash
字段),读取这个字段,解析到其中的打开可视化编辑器的开关开启之后,就会加载可视化编辑器。由此可见其实从SDK后台管理界面跳转到可视化标记页面时,就是向SessionStorage中写入了相应的标志。
可视化埋点的两个关键点是:
- 标记元素,保存配置:这一步要保存好需要追踪的元素的element_path,以及需要追踪的元素。
- 下发配置,查找元素,监听点击,上报行为:这一步要通过element_path找到元素,给它添加一个点击监听器,当点击事件发生时SDK上报事件。
这里最重要的就是:元素的标记和查找,不同的SDK就是实现标记和查找的时候稍微有一些差异。
4.1 标记元素,保存配置
MixpanelJS加载可视化编辑器时,需要从
//mixpanel.com/js-bundle/reports/collect-everything/editor.js?_ts={$timestamp}
去加载一个js文件,这个js差不多可以看成一个独立的标记SDK,最后这个请求会被重定向到一个cdn地址(https://cdn4.mxpnl.com/static/asset-cache/3fc4abfdcebcb5121f1ebf143415b232/compiled/reports/collect-everything/editor.min.js
),随便打开这个js看下就有两万多行,因此单独做成了一个按需加载的模块。
由于Mixpanel就没有提供标记SDK的源码,不过从体验和抓包分析后台下发的配置,我仍然可以推测出技术实现的细节。
从体验的角度来讲,当进入可视化编辑状态时,在开发者web页面上,用户的鼠标经过可以被点击的元素(例如a、button标签等)时,这个元素会被一种颜色高亮提示,此时点击一下这个元素,就会弹出一个浮窗,用户填写信息,设置一个事件和一些属性,保存之后就算完成对这个元素的标记操作了,当标记过的元素的配置保存好了以后,这个元素会用另外一种特殊的颜色高亮标识起来。
从技术的角度来讲,我看下神策JS SDK中的vtrack.sdk.js
这个文件,当神策SDK进入可视化标记模式的时候,会去加载vendor.js
和vendor.css
,这两个文件可以看作一个标记SDK。那么vendor.js
代码里是如何标记需要追踪的元素的呢?
在vendor.js
中,有一个EventDefine
模块,这个模块负责把一个标签处理成我要保存的selector。
EventDefine有三个方法:
- getSelfAttr:获取一个标签内的文本内容,举例来说,一个
<p>This is another paragraph.</p>
得到的内容是This is another paragraph.
。 - toSelector:把一个标签的tagName、id、classNames解析出来,拼成一个串。举例来说,一个
<div id="test" class="uncle chen"></div>
标签,它的selector是div#id.uncle.chen
,这个selector是可以直接给jQuery用来查找元素的。 - toAllSelector:选择一个需要追踪的标签,并给这个标签定义点击时上报的事件(EventDefine),最后将这个事件转成一个selector保存下来,selector就是用于给jQuery来查找元素的选择器,这里需要注意,如果一个元素是在iFrame里面的,那么SDK保存的选择器路径是相对iFrame内部的,而不是最外层的document。
前两个方法都是给toAllSelector
方法调用的,toAllSelector
方法是神策的标记SDK的重点,这个方法的实现如下:
|
|
当选中一个标签时,SDK会提取出这个标签的selector,然后用jQuery选择器查找这个selector指向的元素,如果这个selector指向的元素有多个(selSize !== 1
,也就是说这个元素有着多个兄弟标签),那么还需要进一步去提取其父标签的selector,直到找出可以唯一标识这个元素的selector为止,最后将需要追踪的这个元素以{nthEle: nthEle, selfAttr: selfAttr}`,nthEle是selector,selfAttr是文本内容。
简单总结一下元素的标记,在Web页面中,一个元素的唯一css选择器生成算法,应该记录了从body到这个元素的完整路径,并记录每一个节点是其父亲节点的第几个孩子节点,即这个元素在整个Dom Tree中的深度和下标。此外,为了在一定程度上抵抗Dom Tree的变化,下标应该记录的是这个元素在父节点中相同类型元素的index(nth-of-type),而不是其父节点下面所有孩子节点的index(nth-child)。
4.2 查找元素,监听上报
标记元素,保存配置之后,SDK如何根据配置来监测配置好的元素,并进行上报呢?前面我说到Mixpanel在可视化埋点的上报实现里,仍然保持了全量点击事件上报,并在每个上报中把元素在Dom Tree的完整路径一起上报到了后台,由后台去过滤出可视化事件。
所以这里我看下神策js的代码,在可视化模块vtrack.sdk.js
中,正常模式下,会去解析后台下发的配置,找到标记过的元素,绑定事件。
1.下发配置
|
|
由于神策的后台代码是走私有化部署的,我没有办法体验,这里看一份诸葛IO平台可视化配置:
|
|
注意,上面这份配置有两个字段,一个是url,另一个是editUrl,editUrl表示标记元素的时候,是在哪一个页面里操作的。url表示应该去哪个url下面查找标记的元素。因为有些情况下,虽然我们是在某一个页面标记的元素,但是我们有很多其他页面和这个页面长得类似,比如商品详情类的页面,所以我们其实希望在所有的商品详情页都可以上报某些事件。所以,如果url为一个具体的值,例如”http://localhost:63342/sa-sdk-javascript/zhuge.html?_ijt=hg3r25mmicg2icf0jtndtaskh7#“,说明只应该在这个url对应的页面中查找元素,上报行为即可;如果url=””,说明我们应该在整个Web应用中的所有页面都去依据路径查找元素,上报事件。
2.解析配置,监测元素
还是直接看神策代码实现,里面加了点注释:
|
|
神策查找元素的时候用到了jQuery,而在Mixpanel中没有用jQuery,而是用的Document.querySelectorAll
这个API。毕竟有很多移动页面为了优化加载速度,不会用jQuery这么重的库。
此外,当追踪一些特殊的标签时,可以考虑用XPath去定位,今日头条的广告监测插件其实就用到了XPath。
3.给事件上报添加属性
单独把添加属性拿出来讲,是因为它的原理是类似的。前面我们只提到标记一个元素,当它被点击的时候上报事件,但是这样没有在上报事件的同时带上自定义的一些属性。
其实只要是Web页面上出现了的元素,我们都可以把它记录下来,然后在事件发生的时候,查找到这些元素,并把它们的内容作为事件的属性上报上来。我们可以在标记了一个元素的时候,再去标记其他的一些元素(例如一些文本标签),并设置其他这些元素各自所对应的key,当事件发生时,我们可以找到其他这些元素,并获取到其中显示的文本内容,作为各自参数的值,上报到后台。
4.3 可视化埋点小结
可以看出,在JS上实现可视化埋点不是一件太麻烦的事情,我认为最关键的两件事就是标记元素和查找元素。不过它缺点是只会读取页面上的标签元素的展示出来的属性,也不会像代码埋点的方案那样去理解业务场景,获取上下文(通常在内存里)的一些属性;另外,当页面的结构发生变化的时候,可能要重新进行一次标记操作。有些平台是通过对事件监测的告警来提醒用户的,当事件数量同比大幅减少的时候,大概率是因为某次改版导致页面Dom Tree产生了变化,通过配置下发里面的元素路径找不到之前标记的元素了,这时就应该提醒用户重新标记。
五、总结
本文从代码埋点、全埋点、可视化埋点三个角度,以Mixpanel、神策数据的JS SDK的源代码,分析了Web页面埋点的实现方案的实现。在流量红利逐渐消失的现在,数据的采集、分析和精细化的运营显得更加重要,下面简单列一个表格对以上三种方式的埋点方案进行对比,还是那句话,三种埋点方式相辅相成,结合业务需求搭配使用,适合自己的才是最好的。
埋点方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
代码埋点 | 可以按照业务上报详细、定制化的数据 | 需要开发人员参与,更新维护成本高,无法获得历史数据 | 对上下文理解要求较高的业务数据 |
全埋点 | 对发人员依赖低,仅需嵌入一次SDK,可以全量上报通用数据,可以拿到历史数据 | 数量量太大,占用更多资源,且无法收集业务上下文数据,给后续数据筛选和分析带来一定的难度 | 上下文相对独立的、通用的数据 |
可视化埋点 | 对开发人员依赖低,可以按照业务需求上报数据,对上下文数据有一定收集能力 | 标记事件有一定的操作难度,事件需要被更新时无法获得历史数据,界面变化时标记的元素可能失效 | 业务上下文数据相对简单,操作交互比较固定的界面 |