用户行为数据采集系统
采什么。搞清楚需要什么数据,抽象出一个统一的数据格式。
前端怎么采。解决前端如何有效埋点、全量采集的问题。
后端怎么存。解决数据集中存储、易于分析的问题。
{
"uuid": "2b8c376e-bd20-11e6-9ebf-525499b45be6",
"event_time": "2016-12-08T18:08:12",
"page": "www.example.com/poster.html",
"element": "register",
"attrs": {
"title": "test",
"user_id": 1234
}
}
前端怎么采
整理好数据格式和上报方式后,前端的重点工作便是如何埋点。传统的埋点方式,就是在需要上报的位置组织数据、调用API,将数据传给后端,比如百度统计、google analysis都是这样做的。这是最常用的方式,缺点是需要在代码里嵌入调用,与业务逻辑耦合在一起。近几年,一些新的数据公司提出了“无埋点”的概念,通过在底层hook所有的点击事件,将用户的操作尽量多的采集下来,因此也可以称为“全埋点”。这种方式无需嵌入调用,代码耦合性弱,但是会采集较多的无用数据,可控性差。经过一番调研,结合我们自己的业务,形成了这样几点设计思路:
hook底层的点击事件来做数据上报,在上报的地方统一做数据整理工作。
通过UI元素的属性值来设置是否对该元素的点击事件上报。
通过UI元素的属性值来设置元素的关联关系,用于获取上述的“与元素相关联的其他元素的信息”。
我们首先在Web的H5页面中做了实践,核心的代码很简单。第一,在页面加载时绑定所有的click事件,上报页面浏览事件数据。第二,通过user_action_id属性来表示一个元素是否需要上报点击事件,通过user_action_relation属性来声明当前元素被关联到哪个元素上面,具体代码实现不解释,很简单。
$(d).ready(function() {
// 页面浏览上报
pvUpload({page: getPageUrl()},
$.extend({title: getTitle()}, getUrlParams()));
// 绑定点击事件
$(d).bind('click', function(event) {
var $target = $(event.target);
// 查找是否是需要上报的元素
var $ua = $target.closest('[user_action_id]');
if ($ua.length > 0) {
var userActionId = $ua.attr('user_action_id');
var userActionRelation = $("[user_action_relation=" + userActionId + "]");
var relationData = [];
// 查找相关联的元素的数据信息
if (userActionRelation.length > 0) {
userActionRelation.each(function() {
var jsonStr = JSON.stringify({
"r_placeholder_element": $(this).get(0).tagName,
'r_placeholder_text': $(this).text()
});
jsonStr = jsonStr.replace(/\placeholder/g, $(this).attr('id'));
jsonStr = JSON.parse(jsonStr);
relationData.push(jsonStr);
});
}
// 点击事件上报
clickUpload({page: getPageUrl(), element: userActionId},
$.extend({title: getTitle()}, getUrlParams(), relationData));
}
});
});
<div>
<div>
<textarea id="answer" cols="30" rows="10" user_action_relation="answer-submit"></textarea>
</div>
<button user_action_id="answer-submit">提 交</button>
</div>
后端怎么存
数据进入后台后,首先接入Kafka队列中,采用生产消费者模式来处理。这样做的好处有:第一,功能分离,上报的API接口不关心数据处理功能,只负责接入数据;第二,数据缓冲,数据上报的速率是不可控的,取决于用户使用频率,采用该模式可以一定程度地缓冲数据;第三,易于扩展,在数据量大时,通过增加数据处理Worker来扩展,提高处理速率。
除了前端上报的数据内容外,我们还需要在后端加入一些其他的必要信息。在数据接入Kafka队列之前,需要加入五个维度信息:客户端类型(Web/Android/IOS)、事件类型(浏览/点击)、时间、客户端IP和User Agent。在消费者Worker从Kafka取出数据后,需要加入一个名为event_id的字段数据,具体含义等下解释。因此,最后存入的数据格式便如下所示:
{
"uuid": "2b8c376e-bd20-11e6-9ebf-525499b45be6",
"event_time": "2016-12-08T18:08:12",
"page": "www.example.com/poster.html",
"element": "register",
"client_type": 0,
"event_type": 0,
"user_agent": "Mozilla\/5.0 (Linux; Android 5.1; m3 Build\/LMY47I) AppleWebKit\/537.36 (KHTML, like Gecko) Version\/4.0 Chrome\/37.0.0.0 Mobile MQQBrowser\/6.8 TBS\/036887 Safari\/537.36 MicroMessenger\/6.3.31.940 NetType\/WIFI Language\/zh_CN",
"ip": "59.174.196.123",
"timestamp": 1481218631,
"event_id": 12,
"attrs": {
"title": "test",
"user_id": 1234
}
}
再来看event_id的含义。前端传过来的一组组数据中,通过page和element可以区分出究竟是发生了什么事件,但是这些都是前端UI的名称,大部分是开发者才能看懂的语言,因此我们需要为感兴趣的事件添加一个通俗易懂的名称,比如上面的数据对应的事件名称为“在海报页面中注册”。将page+element、事件名称进行关联映射,然后将相应的数据记录id作为event id添加到上述的数据中,方便后期做数据分析时根据跟event id来做事件聚合。做这件事有两种方式:一种是允许相关人员通过页面进行配置,手动关联;一种是前端上报时带上事件名称,目前这两种方式我们都在使用。
最后,来看看数据存储的问题。传统的关系型数据库在存储数据时,采用的是行列二维结构来表示数据,每一行数据都具有相同的列字段,而这样的存储方式显示不适合上面的数据格式,因为我们无法预知attrs中有哪些字段数据。象用户行为数据、日志数据都属于半结构化数据,所谓半结构化数据,就是结构变化的结构化数据,适合使用NoSQL来做数据存储。我们选用的是ElasticSearch来做数据存储,主要基于这么两点考虑:
Elasticsearch是一个实时的分布式搜索引擎和分析引擎,具有很强的数据搜索和聚合分析能力。
在这之前我们已经搭建了一个ELK日志系统,可以复用Elasticsearch集群做存储,也可以复用Kibana来做一些基础的数据分析可视化。
Elasticsearch的使用方法可以参考Elasticsearch使用总结一文,这里不做过多讲解。使用Elasticsearch来做数据存储,最重要的是两件事:建立Elasticsearch的映射模板、批量插入。Elasticsearch会根据插入的数据自动建立缺失的index和doc type,并对字段建立mapping,而我们要做的创建一个dynamic template,告诉Elasticsearch如何自动建立,参考如下。批量插入,可以通过Elasticsearch的bulk API轻松解决。
"user_action_record": {
"order": 0,
"template": "user_action_record_*",
"settings": {
},
"mappings": {
"_default_": {
"dynamic_templates": [{
"string_fields": {
"mapping": {
"type": "string",
"fields": {
"raw": {
"index": "not_analyzed",
"ignore_above": 256,
"type": "string"
}
}
},
"match_mapping_type": "string"
}
}],
"properties": {
"timestamp": {
"doc_values": true,
"type": "date"
}
},
"_all": {
"enabled": false
}
}
}
}
文章不错?点个【在看】吧! 👇