从电商页面埋点说起
背景
互联网发展到现在,数据的重要性已经不需要再多的强调,那如何做好数据搜集的工作则是每一家公司都要面临的问题。尤其是像天猫、京东、寺库这样的电商公司,数据的统计可以提升用户购买的用户体验,可以方便运营和产品调整销售策略等等。可见页面埋点多么重要。今天就让我们从无到有制作一个埋点上报工具。
主要内容:
- 什么是埋点
- 埋点原理
- 埋点的种类
- 电商页面前端埋点规范
- 封装一个异步请求
- IntersectionObserver -新一代元素观察接口
- 基于VUE从零开始封装一个前端数据埋点工具
- 后端日志格式(前端理解即可)
- 后端nginx配置(前端理解即可)
- 需要改进的地方
什么是埋点
所谓“埋点”,是数据采集领域(尤其是用户行为数据采集领域)的术语,指的是针对特定用户行为或事件进行捕获、处理和发送的相关技术及其实施过程。比如用户某个icon点击次数、观看某个视频的时长等等。
埋点原理分析和流程概述
简单来说,网站统计分析工具需要收集到用户浏览目标网站的行为(如打开某网页、点击某按钮、将商品加入购物车等)及行为附加数据(如某下单行为产生的订单金额等)。早期的网站统计往往只收集一种用户行为:页面的打开。而后用户在页面中的行为均无法收集。这种收集策略能满足基本的流量分析、来源分析、内容分析及访客属性等常用分析视角,但是,随着ajax技术的广泛使用及电子商务网站对于电子商务目标的统计分析的需求越来越强烈,这种传统的收集策略已经显得力不能及。
后来,Google在其产品谷歌分析中创新性的引入了可定制的数据收集脚本,用户通过谷歌分析定义好的可扩展接口,只需编写少量的javascript代码就可以实现自定义事件和自定义指标的跟踪和分析。目前百度统计、搜狗分析等产品均照搬了谷歌分析的模式。
其实说起来两种数据收集模式的基本原理和流程是一致的,只是后一种通过javascript收集到了更多的信息。下面看一下现在各种网站统计工具的数据收集基本原理。
首先,用户的行为会—这里姑且先认为行为就是打开网页。当网页被打开,页面中的埋点javascript片段会被执行,一般网站统计工具都会要求用户在网页中加入一小段javascript代码,这个代码片段一般会动态创建一个script标签,并将src指向一个单独的js文件,例子中为dot.js.此时这个单独的js文件(图中绿色节点)会被浏览器请求到并执行,这个js往往就是真正的数据收集脚本。数据收集完成后,js会请求一个后端的数据收集脚本(图中的backend),这个脚本一般是一个伪装成图片的动态脚本程序,可能由php、python或其它服务端语言编写,js会将收集到的数据通过http参数的方式传递给后端脚本,后端脚本解析参数并按固定格式记录到访问日志。
上面是一个数据收集的大概流程,下面以寺库商城为例,对每一个阶段进行一个相对详细的分析。
1.1 埋点脚本执行阶段
技术栈为vue,当页面中的资源加载完成后,执行埋点的脚本,如下图
1.2 数据收集脚本执行阶段
数据收集脚本(dot.js)被请求后当页面展示会被执行,这个脚本一般要做如下几件事:
(1)通过浏览器内置javascript对象收集页面基本信息,如页面title(通过document.title)、url(页面链接)、用户显示器分辨率(通过windows.screen)、cookie信息(通过document.cookie)等等一些信息。
(2)收集曝光楼层信息。
(3)将上面两步收集的数据进行拼接。
(4)请求一个后端脚本,将信息放在http request参数中携带给后端脚本。
这里唯一的问题是步骤4,javascript请求后端脚本常用的方法是ajax,但是ajax是不能跨域请求的。这里dot.js在被统计网站的域内执行,而后端脚本在另外的域,ajax行不通。一种通用的方法是js脚本创建一个Image对象(log.gif),将Image对象的src属性指向后端脚本并携带参数,此时即实现了跨域请求后端。这也是后端脚本为什么通常伪装成gif文件的原因。通过http抓包可以看到dot.js对log.gif的请求:
1.3 后端脚本执行阶段
log.gif是一个伪装成gif的脚本。这种后端脚本一般要完成以下几件事情:
(1)解析http请求参数的到信息。
(2)从服务器(WebServer)中获取一些客户端无法获取的信息,如访客ip等。
(3)将信息按格式写入log。
(4)生成一副1×1的空gif图片作为响应内容并将响应头的Content-type设为image/gif。
(5)在响应头中通过Set-cookie设置一些需要的cookie信息。
之所以要设置cookie是因为如果要跟踪唯一访客,通常做法是如果在请求时发现客户端没有指定的跟踪cookie,则根据规则生成一个全局唯一的cookie并种植给用户,否则Set-cookie中放置获取到的跟踪cookie以保持同一用户cookie不变(见图4)。
埋点的种类
业界的埋点方案主要分为以下三类:
代码埋点
代码埋点就是在需要数据统计的地方植入数据上报的代码,统计用户行为。
优点:可以非常精确的选择什么时候发送数据。
缺点:维护代价较大,每一次更新都要对埋点代码进行维护,否则大概率搜集不到旧版本的数据。
可视化埋点
利用可视化交互手段,数据产品/数据分析师可以通过可视化界面(管理后台连接设备) 配置事件,如下是腾讯移动分析的可视化埋点界面。可视化埋点仍需要先配置相关事件,再采集。
- 优点:埋点只需业务同学接入,无需开发支持;
- 缺点:仅支持客户端行为。
无埋点
无埋点是指开发人员集成采集 SDK 后,SDK 便直接开始捕捉和监测用户在应用里的所有行为,并全部上报,不需要开发人员添加额外代码。
数据分析师/数据产品 通过管理后台的圈选功能来选出自己关注的用户行为,并给出事件命名。之后就可以结合时间属性、用户属性、事件进行分析了。所以无埋点并不是真的不用埋点了。
优点:
无需开发,业务人员埋点即可;
支持先上报数据,后进行埋点。
缺点:
数据量大;
仅仅支持客户端。
无埋点和可视化埋点均不需要开发支持,仅数据业务同学进行设置即可。但两者数据上报-埋点设置存在加大的差异:无埋点支持在数据上报之后再进行埋点设置,因而数据采集/上报的量远大于可视化埋点。
因而无埋点的数据大都有清空机制,例如growingIO,允许版本发布后7天内设置埋点,超过7天数据清空,无法追溯。
这次主要讲代码埋点
代码埋点分为 命令式埋点 与 声明式埋点 :
命令式埋点,顾名思义,开发者需要手动在需要埋点的节点处进行埋点。如点击按钮或链接后的回调函数、页面ready时进行请求的发送。大家肯定都很熟悉这样的代码:
// 页面加载时发送埋点请求
$(document).ready(function(){
// ... 这里存在一些业务逻辑
sendRequest(params);
});
// 按钮点击时发送埋点请求
$(\'button\').click(function(){
// ... 这里存在一些业务逻辑
sendRequest(params);
});
可以很容易发现,这样的做法很有可能会将埋点代码侵入业务代码,这使整体业务代码变得繁琐,容易出错,且后续代码会愈加膨胀,难以维护。所以,我们需要让埋点的代码与具体的业务逻辑解耦,即 声明式埋点 ,从而提高埋点的效率和代码的可维护性。
声明式埋点理论上,只需要关注两个问题:
- 需要埋点的DOM节点;
- 所需携带的数据
因此,可以很快想出一个声明式埋点的方法:
// key表示埋点的唯一标识;act表示埋点方式
<span v-clstag-dot = "{\'act\':\'thumbs\', \'key\': \'details_product_1_dot_thumbs\', \'productId\': product.productId}">点赞</span>
稍后会详细讲声明式埋点的实现原理
电商页面前端埋点规范
建立一个好的规范非常重要,包括命名规范、上报规范、数据规范和使用规范*。
1.埋点命名规范
埋点名称为上报日志中的key字段,第三条会讲到,我们当前的做法是埋点名称只能是由字母、数字、下划线组成,并保证在应用内唯一。
常用规则的举例如下:
比如行为埋点:{页面名称}+{组件名称}+{组件id}+{功能}+{动作}
组件名称和动作最为重要,它决定着后端收到埋点后要进行哪种操作,开发过程中要和后端严格制定好名称,比如点击的是商品列表。我们约定好了商品为product,那么组件名称就必须为product.比如点击了收藏,和后端约定好的是thumbs,动作就必须为:thumbs。另外,id从0开始。
组件名称列表:
广告:ad
商品:product
购物车:car
其他:可和后端协商
动作列表:
点击:click
收藏:collection
评论:comment
点赞; thumbs
加入购物车: add
其他:可和后端协商
示例:
// key表示埋点的唯一标识;act表示埋点方式
<span v-clstag-dot = "{\'act\':\'thumbs\', \'key\': \'details_product_1_dot_thumbs\', \'productId\': product.productId}">点赞</span>
埋点启动日志和错误上报日志:{页面名称}+{页面id}+{动作}
示例:
\’key\’: \’details_123_show\’
\’key\’:\’details_123_error\’
2.埋点上报规范
(1)针对曝光埋点数据的上报策略一般如下:
-
基于时间间隔:每隔 n秒(时间间隔可以根据公司的业务情况自定义)
-
基于数据条数:每累积 n条数据(条数可以自定义)
-
不间断实时上报,如果是低频率,数据量小,实时性要求高的数据可以不设限制
-
为以防用户卸载 App或者关闭浏览器造成本地数据的丢失,会将未上报的埋点存储在localstorage,浏览器关闭埋点数据并不会被删除,如果用户再次访问,会启动上报。基于Native提供的bridge,让Native帮忙持久化数据,并在再次进入时,启动上报。这里也可以创建一个单独的串行队列,来实现对本地持久化数据的逐个上报。
(2)事件埋点和错误埋点的上报策略
- 事件发生后及时上报
-
数据规范
每个公司都有自己的埋点数据规范,里面汇总了需要上报的埋点数据,例如
- 使用规范
(1)引入埋点脚本一定要在页面资源加载完,例如:
import { dot } from \'./assets/js/dot\' // 中央事件总线封装 Vue.use(VueBus) Vue.config.productionTip = false /* eslint-disable no-new */ // Vue.directive() 这个方法写在new Vue之前 dot.clickExpDot(Vue) window.onload = function () { dot.postError() dot.dotPageReadyData() dot.show() } new Vue({ el: \'#app\', router, store, // 使用store components: { App }, template: \'<App/>\' })
(2)声明式埋点在html中引入的规范,例如:
#曝光埋点的用法
<div class="exposure-statistics" show-dot="{\'act\':\'show\',\' key\':\'details_ad_1_flowtab_show\'}">
#事件埋点的用法 <span v-clstag-dot = "{\'act\':\'thumbs\', \'key\': \'details_product_1_dot_thumbs\', \'productId\': product.productId}">点赞</span>
封装一个异步请求
1. axios(考虑到跨域问题,本次没有使用)
在vue项目中,和后台交互获取数据这块,我们通常使用的是axios库,它是基于promise的http库,可运行在浏览器端和node.js中。他有很多优秀的特性,例如拦截请求和响应、取消请求、转换json、客户端防御cSRF等。所以我们的尤大大也是果断放弃了对其官方库vue-resource的维护,直接推荐我们使用axios库。如果还对axios不了解的,可以移步axios文档。
安装
npm install axios; // 安装axios复制代码
引入
一般我会在项目的src目录中,新建一个request文件夹,然后在里面新建一个http.js和一个api.js文件。http.js文件用来封装我们的axios,api.js用来统一管理我们的接口。
代码如下:
import axios from \'axios\'
// import QS from \'qs\'
import { Toast } from \'vant\'
// 环境的切换
if (process.env.NODE_ENV === \'development\') {
axios.defaults.baseURL = \'http://localhost:8080\'
} else if (process.env.NODE_ENV === \'production\') {
axios.defaults.baseURL = \'http://localhost:8080\'
}
// 请求超时时间
axios.defaults.timeout = 10000
// post请求头
axios.defaults.headers.post[\'Content-Type\'] = \'application/x-www-form-urlencoded;charset=UTF-8\'
// 请求拦截器
axios.interceptors.request.use(config => {
// 请求处理
return config
}, err => {
// 处理请求错误
return Promise.reject(err)
})
// 响应拦截器
axios.interceptors.response.use(
response => {
if (response.status === 200) {
return Promise.resolve(response.data)
} else {
return Promise.reject(response)
}
},
// 服务器状态码不是200的情况
error => {
if (error.response.status) {
switch (error.response.status) {
case 500:
Toast({
message: \'系统错误\',
duration: 1000,
forbidClick: true
})
break
case 201:
Toast({
message: \'业务失败!\',
duration: 1000,
forbidClick: true
})
break
// 其他错误,直接抛出错误提示
default:
Toast({
message: \'失败\',
duration: 1500,
forbidClick: true
})
}
return Promise.reject(error.response)
}
}
)
/**
* get方法,对应get请求
* @param {String} url [请求的url地址]
* @param {Object} params [请求时携带的参数]
*/
export function get (url, params) {
return new Promise((resolve, reject) => {
axios.get(url, {
params: params
}).then(res => {
resolve(res)
}).catch(err =>