所谓的埋点就是在应用中特定的流程收集一些信息,用来跟踪应用使用的状况,后续用来进一步优化产品或是提供运营的数据支撑,包括访问数(Visits),访客数(Visitor),停留时长(Time On Site),页面浏览数(Page Views)和跳出率(Bounce Rate)。这样的信息收集可以大致分为两种:页面统计(track this virtual page view),统计操作行为(track this button by an event),在为一些网页和App接下来进行流量分析以及描绘用户画像提供支持,是进行数据分析和挖掘的“第一步”。
数据埋点作为数据采集的一种重要方式,主要用来记录和收集终端用户的操作行为,其基本原理是在App/H5/PC等终端部署采集的SDK代码,当用户的行为满足某种条件的时候,比如进入某个页面、点击某个按钮等,会自动触发记录和存储,然后这些数据会被收集并被传输到终端提供商,或者是通过后端采集用户使用服务过程中的请求数据。
终端提供商在收集到埋点数据之后,通过大数据处理、数据统计、数据分析、数据挖掘等加工处理,可以得到衡量产品状态的一些基本指标,比如活跃、留存、新增等大盘数据,从而洞察产品的状态。此外更重要的是随着数据挖掘等技术的兴起,埋点采集到的数据在以下方面的作用也越来越凸显:
驱动决策:ABtest、漏斗优化、用户增长、bug修复、精准营销、流失用户预警
驱动产品智能:智能推荐(千人千面)、场景化提示(私人助理)等
驱动安全:风险识别
为了将海量数据采集得更加精准,为后续营造“纯净”的数据分析环境,埋点技术应运而生。数据基础夯实与否,取决于数据的采集方式。
埋点方式多种多样,按照埋点位置不同,可以分为前端(客户端)埋点与后端(服务器端)埋点,其中前端埋点包括:代码埋点、全埋点、可视化埋点。
前端埋点是在用户端(APP、Web、客户端)等嵌入数据采集代码,比如友盟等均采用的是前端埋点,比如通过嵌入一段代码就就可以对网页数据的访问数据进行采集。相比于后端埋点,前端埋点能方便收集到用户在界面上的行为数据,比如用户点了哪个按钮、页面之间的跳转次序、停留时长等,这些数据是后面进行数据分析的主要来源。
前端埋点技术有以下三类:
代码埋点是直接将采集SDK集成在终端,然后不断在此基础上添加调整采集方案,是目前主流的埋点采集方案,其优缺点如下:
优点:
高度定制、控制精准、采集的数据丰富准确
缺点:
首先是每当有采集需求,需要开发人员不断添加采集代码,工作量大;
其次变更采集策略,需要发布新版本,代价巨大,存在滞后效应;
最后由于采集代码常驻终端,不断将采集的用户行为数据进行记录和上报,对于终端尤其是移动终端来说还有耗电、消耗数据流量等负载,此外在数据上报传输的过程中也存在丢失数据的风险。
由于代码埋点需要终端开发人员来执行采集方案,对业务的功能开发侵入性较高。有的公司开发出了可视化埋点技术,只需要产品与运营人员通过GUI界面进行鼠标简单点击,就可以随时增加、取消、调整采集数据的位置和方式,此种埋点方式避开了终端开发人员的介入,由需求人员直接执行采集,减轻了需求传递过程中的信息损耗和误解,另外可视化埋点技术往往由服务端直接下发采集的配置文件,而不用跟随版本发布,从而加快了数据采集的流程。
具体实现方式参考:
具体实现是SDK定时做界面截图,在截图的同时从界面UI的根对象开始遍历所有的可视化子对象,得到其层级关系。根据截图和UI元素的可视化信息重新渲染页面,识别可埋点的控件。当产品人员在后台管理端的截屏画面上点击可埋点控件,设置事件关联方面的配置,服务器保存这些配置,客户端在获取到这些配置信息以后,按照新配置采集数据。
无埋点与可视化埋点原理基本一致,区别在于无埋点是先遍历所有的控件和操作行为的组合情况,然后将这些组合情况交给埋点后台,由数据分析人员选择对哪些组合的埋点数据进行分析,其优缺点如下:
优点:
收集数据全面,无漏报
缺点:
采集数据量巨大,增加了终端流量消耗和服务器存储负担。
埋点的上报时机相对呆板,不能灵活的根据特定的场景进行特殊设置
前端埋点的注意事项:
页面和控件标示上报要从顶层进行合理的设计,层次感要明显
埋点数据的漏报和重复上报如何衡量
前端埋点不仅可以处理不需要和服务器交互的曝光和点击事件,也可以将与服务器交互的结果,比如关注成功、分享成功、优惠券领取成功等原属于后端埋点里的事件放在前端来上报。
后端埋点为了避免前端埋点的以下问题:
前端埋点需要对采集的数据压缩、暂存,为减少移动端的数据流量,除一些需要实时上报的重要事件不限制网络环境,其它事件一般只在wifi情况下上报,因此数据会有延迟,丢数据等弊端,而在后端采集数据,由于数据是在内网传输,数据传输的即时性强,丢失数据的风险小。
前端埋点采集程序由于需要常驻,监测实时和延迟埋点上报,不可避免的带来额外的耗电。
前端埋点若要新增或调整采集方案,需要开发人员修改客户端代码,然后发版之后才能解决,受发布周期的影响较大,而且通常用户的版本更新并不会及时,这将导致新方案不能及时覆盖所有用户。虽然现在部分埋点管理后台也支持热配置更新,但功能一般都很弱,只支持一些基础的埋点事件热更新部署,
注意:
很多时候并不把后端埋点独立出来,而是混合在前端埋点中,等用户和服务器端的交互返回结果之后,将结果进行上报。
对一下需要精确采集的数据,比如代金券发放等,实施的时候尽量采用后端埋点,除非后端无法采集到所需要的数据,前端埋点只是用来参考。此外也可以将业务数据库代金券领取数据同步到数据仓库中进行分析。
路径埋点和独立埋点:
这部分的埋点根据业务对路径的追踪需求和SDK的开发能力,可为每个事件设计上下文的路径信息,路径信息的组成一般由页面、控件、行为三部分组成,而路径的深度也不宜太深,一般小于五层。
显性埋点和隐性埋点:
显性和隐性是从用户有感和无感来区分的,有感事件是用户的主动事件,比如展示和点击事件;无感事件主要用来处理后台的数据请求和拉取,用以监控和服务器的数据交互是否正常等,无感事件中常用的是扫描采集,比如app启动之后,扫描各设置开关的状态信息进行上报等
业务埋点和监测埋点:
业务埋点是从业务需求的角度而言,比如产品需要统计某个页面的曝光和点击,算法人员需要的推荐项点击率等;而监测埋点是从业务的流程上来讲的,一般是指隐性的(比如服务器交互的内容拉取情况、本地潜在信息的生成情况等),此外业务埋点中的关键部分也可以用作监测埋点。
回到一开始的问题:何种埋点方式最理想呢?
正如同硬币有两面,任何单一的埋点方式都存在优点与缺点,企图通过简单粗暴的几行代码/一次部署、甚至牺牲用户体验的埋点方式,都不是企业所期望的。要满足精细化、精准化的数据分析需求,可根据实际需要的分析场景,选择一种或多种组合的采集方式,毕竟采集全量数据不是目的,实现有效的数据分析,从数据中找到关键决策信息实现增长才是重中之重。
因此,数据采集只是数据分析的第一步,数据分析的目的是洞察用户行为,挖掘用户价值,进而促进业务增长,故最理想的埋点方案是根据根据不同的业务和场景以及行业特性和自身实际需求,将埋点通过优劣互补方式进行组合,比如:
1、代码埋点+全埋点:在需要对落地页进行整体点击分析时,细节位置逐一埋点的工作量相对较大,且在频繁优化调整落地页时,更新埋点的工作量更加不容小觑,但复杂的页面存在着全埋点不能采集的死角,因此,可将代码埋点作为辅助,将用户核心行为进行采集,从而实现精准的可交叉的用户行为分析;
2、代码埋点+服务端埋点:以电商平台为例, 用户在支付环节,由于中途会跳转到第三方支付平台,是否支付成功需要通过服务器中的交易数据来验证,此时可通过代码埋点和服务端埋点相结合的方式,提升数据的准确性;
3、代码埋点+可视化埋点:因代码埋点的工作量大,可通过核心事件代码埋点,可视化埋点用于追加和补充的方式采集数据。
客户端埋点:支持 iOS、安卓、Web/H5、微信小程序,主要用于分析 UV、PV、点击量等基本指标。例:下图是Web端的埋点技术图:
在网页流量分析系统中,采用客户端网页埋点实现,在其中需要埋点的页面中的中加入如下代码:
注:tj.js 就是需埋点的 js 文件
/**函数可对字符串进行编码,这样就可以在所有的计算机上读取该字符串。*/
function ar_encode(str)
{
//进行URL编码
return encodeURI(str);
}
/**屏幕分辨率*/
function ar_get_screen()
{
var c = "";
if (self.screen) {
c = screen.width+"x"+screen.height;
}
return c;
}
/**颜色质量*/
function ar_get_color()
{
var c = "";
if (self.screen) {
c = screen.colorDepth+"-bit";
}
return c;
}
/**返回当前的浏览器语言*/
function ar_get_language()
{
var l = "";
var n = navigator;
if (n.language) {
l = n.language.toLowerCase();
}
else
if (n.browserLanguage) {
l = n.browserLanguage.toLowerCase();
}
return l;
}
/**返回浏览器类型IE,Firefox*/
function ar_get_agent()
{
var a = "";
var n = navigator;
if (n.userAgent) {
a = n.userAgent;
}
return a;
}
/**方法可返回一个布尔值,该值指示浏览器是否支持并启用了Java*/
function ar_get_jvm_enabled()
{
var j = "";
var n = navigator;
j = n.javaEnabled() ? 1 : 0;
return j;
}
/**返回浏览器是否支持(启用)cookie */
function ar_get_cookie_enabled()
{
var c = "";
var n = navigator;
c = n.cookieEnabled ? 1 : 0;
return c;
}
/**检测浏览器是否支持Flash或有Flash插件*/
function ar_get_flash_ver()
{
var f="",n=navigator;
if (n.plugins && n.plugins.length) {
for (var ii=0;ii<n.plugins.length;ii++) {
if (n.plugins[ii].name.indexOf('Shockwave Flash')!=-1) {
f=n.plugins[ii].description.split('Shockwave Flash ')[1];
break;
}
}
}
else
if (window.ActiveXObject) {
for (var ii=10;ii>=2;ii--) {
try {
var fl=eval("new ActiveXObject('ShockwaveFlash.ShockwaveFlash."+ii+"');");
if (fl) {
f=ii + '.0';
break;
}
}
catch(e) {}
}
}
return f;
}
/**匹配顶级域名*/
function ar_c_ctry_top_domain(str)
{
var pattern = "/^aero$|^cat$|^coop$|^int$|^museum$|^pro$|^travel$|^xxx$|^com$|^net$|^gov$|^org$|^mil$|^edu$|^biz$|^info$|^name$|^ac$|^mil$|^co$|^ed$|^gv$|^nt$|^bj$|^hz$|^sh$|^tj$|^cq$|^he$|^nm$|^ln$|^jl$|^hl$|^js$|^zj$|^ah$|^hb$|^hn$|^gd$|^gx$|^hi$|^sc$|^gz$|^yn$|^xz$|^sn$|^gs$|^qh$|^nx$|^xj$|^tw$|^hk$|^mo$|^fj$|^ha$|^jx$|^sd$|^sx$/i";
if(str.match(pattern)){ return 1; }
return 0;
}
/**处理域名地址*/
function ar_get_domain(host)
{
//如果存在则截去域名开头的 "www."
var d=host.replace(/^www\./, "");
//剩余部分按照"."进行split操作,获取长度
var ss=d.split(".");
var l=ss.length;
//如果长度为3,则为xxx.yyy.zz格式
if(l == 3){
//如果yyy为顶级域名,zz为次级域名,保留所有
if(ar_c_ctry_top_domain(ss[1]) && ar_c_ctry_domain(ss[2])){
}
//否则只保留后两节
else{
d = ss[1]+"."+ss[2];
}
}
//如果长度大于3
else if(l >= 3){
//如果host本身是个ip地址,则直接返回该ip地址为完整域名
var ip_pat = "^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$";
if(host.match(ip_pat)){
return d;
}
//如果host后两节为顶级域名及次级域名,则保留后三节
if(ar_c_ctry_top_domain(ss[l-2]) && ar_c_ctry_domain(ss[l-1])) {
d = ss[l-3]+"."+ss[l-2]+"."+ss[l-1];
}
//否则保留后两节
else{
d = ss[l-2]+"."+ss[l-1];
}
}
return d;
}
/**返回cookie信息*/
function ar_get_cookie(name)
{
//获取所有cookie信息
var co=document.cookie;
//如果名字是个空 返回所有cookie信息
if (name == "") {
return co;
}
//名字不为空 则在所有的cookie中查找这个名字的cookie
var mn=name+"=";
var b,e;
b=co.indexOf(mn);
//没有找到这个名字的cookie 则返回空
if (b < 0) {
return "";
}
//找到了这个名字的cookie 获取cookie的值返回
e=co.indexOf(";", b+name.length);
if (e < 0) {
return co.substring(b+name.length + 1);
}
else {
return co.substring(b+name.length + 1, e);
}
}
/**
设置cookie信息
操作符:
0 表示不设置超时时间 cookie是一个会话级别的cookie cookie信息保存在浏览器内存当中 浏览器关闭时cookie消失
1 表示设置超时时间为10年以后 cookie会一直保存在浏览器的临时文件夹里 直到超时时间到来 或用户手动清空cookie为止
2 表示设置超时时间为1个小时以后 cookie会一直保存在浏览器的临时文件夹里 直到超时时间到来 或用户手动清空cookie为止
* */
function ar_set_cookie(name, val, cotp)
{
var date=new Date;
var year=date.getFullYear();
var hour=date.getHours();
var cookie="";
if (cotp == 0) {
cookie=name+"="+val+";";
}
else if (cotp == 1) {
year=year+10;
date.setYear(year);
cookie=name+"="+val+";expires="+date.toGMTString()+";";
}
else if (cotp == 2) {
hour=hour+1;
date.setHours(hour);
cookie=name+"="+val+";expires="+date.toGMTString()+";";
}
var d=ar_get_domain(document.domain);
if(d != ""){
cookie +="domain="+d+";";
}
cookie +="path="+"/;";
document.cookie=cookie;
}
/**返回客户端时间*/
function ar_get_stm()
{
return new Date().getTime();
}
/**返回指定个数的随机数字串*/
function ar_get_random(n) {
var str = "";
for (var i = 0; i < n; i ++) {
str += String(parseInt(Math.random() * 10));
}
return str;
}
/* main function */
function ar_main() {
//收集完日志 提交到的路径
var dest_path = "http://127.0.0.1:8081/log?";
var expire_time = 30 * 60 * 1000;//会话超时时长
//处理uv
//--获取cookie ar_stat_uv的值
var uv_str = ar_get_cookie("ar_stat_uv");
var uv_id = "";
//--如果cookie ar_stat_uv的值为空
if (uv_str == ""){
//--为这个新uv配置id,为一个长度20的随机数字
uv_id = ar_get_random(20);
//--设置cookie ar_stat_uv 保存时间为10年
ar_set_cookie("ar_stat_uv", uv_id, 1);
}
//--如果cookie ar_stat_uv的值不为空
else{
//--获取uv_id
uv_id = uv_str;
}
//处理ss
//--获取cookie ar_stat_ss
var ss_stat = ar_get_cookie("ar_stat_ss");
var ss_id = ""; //sessin id
var ss_count = 0; //session有效期内访问页面的次数
var ss_time = "";
//--如果cookie中不存在ar_stat_ss 说明是一次新的会话
if (ss_stat == ""){
//--随机生成长度为10的session id
ss_id = ar_get_random(10);
//--session有效期内页面访问次数为0
ss_count = 0;
//--当前事件
ss_time = ar_get_stm()
} else { //--如果cookie中存在ar_stat_ss
//获取ss相关信息
var items = ss_stat.split("_");
//--ss_id
ss_id = items[0];
//--ss_count
ss_count = parseInt(items[1]);
//--ss_stm
ss_time = items[2];
//如果当前时间-当前会话上一次访问页面的时间>30分钟,虽然cookie还存在,但是其实已经超时了!仍然需要重新生成cookie
if (ar_get_stm() - ss_time > expire_time) {
//--重新生成会话id
ss_id = ar_get_random(10);
//--设置会话中的页面访问次数为0
ss_count = 0;
//--当前事件
ss_time = ar_get_stm();
}else{//--如果会话没有超时
//--会话id不变
//--设置会话中的页面方位次数+1
ss_count = ss_count + 1;
ss_time = ar_get_stm();
}
}
//--重新拼接cookie ar_stat_ss的值
value = ss_id+"_"+ss_count+"_"+ss_time;
ar_set_cookie("ar_stat_ss", value, 0);
//当前地址
var url = document.URL;
url = ar_encode(String(url));
//当前资源名
var urlname = document.URL.substring(document.URL.lastIndexOf("/")+1);
urlname = ar_encode(String(urlname));
//返回导航到当前网页的超链接所在网页的URL
var ref = document.referrer;
ref = ar_encode(String(ref));
//网页标题
var title = document.title;
title = ar_encode(String(title));
//网页字符集
var charset = document.charset;
charset = ar_encode(String(charset));
//屏幕信息
var screen = ar_get_screen();
screen = ar_encode(String(screen));
//颜色信息
var color =ar_get_color();
color =ar_encode(String(color));
//语言信息
var language = ar_get_language();
language = ar_encode(String(language));
//浏览器类型
var agent =ar_get_agent();
agent =ar_encode(String(agent));
//浏览器是否支持并启用了java
var jvm_enabled =ar_get_jvm_enabled();
jvm_enabled =ar_encode(String(jvm_enabled));
//浏览器是否支持并启用了cookie
var cookie_enabled =ar_get_cookie_enabled();
cookie_enabled =ar_encode(String(cookie_enabled));
//浏览器flash版本
var flash_ver = ar_get_flash_ver();
flash_ver = ar_encode(String(flash_ver));
//当前ss状态 格式为"会话id_会话次数_当前时间"
var stat_ss = ss_id+"_"+ss_count+"_"+ss_time;
//拼接访问地址 增加如上信息
dest=dest_path+"url="+url+"&urlname="+urlname+"&title="+title+"&chset="+charset+"&scr="+screen+"&col="+color+"&lg="+language+"&je="+jvm_enabled+"&ce="+cookie_enabled+"&fv="+flash_ver+"&cnv="+String(Math.random())+"&ref="+ref+"&uagent="+agent+"&stat_uv="+uv_id+"&stat_ss="+stat_ss;
//通过插入图片访问该地址
document.getElementsByTagName("body")[0].innerHTML += "+dest+"\" border=\"0\" width=\"1\" height=\"1\" />";
}
window.onload = function(){
//触发main方法
ar_main();
}
说明:
①var dest_path = "http://127.0.0.1:8081/log?"
, 此处要改成日志服务器的地址,并且这个地址是能够被访问的,最后一个 "?"不要忘加,用来拼后续参数使用的。
②埋点的原理:js代码会动态在页面中创建一个宽和高都是1px的图片,图片的地址指向了1中定义的日志服务器中的图片,
document.getElementsByTagName("body")[0].innerHTML += "+dest+"\" border=\"0\" width=\"1\" height=\"1\" />";
package com.logs.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
@Controller
public class LogController {
private Logger logger = LoggerFactory.getLogger(LogController.class);
@RequestMapping("/log")
public void log(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
//1.获取请求参数
String qs = request.getQueryString();
//2.对URL解码
String decode = URLDecoder.decode(qs, "utf-8");
//3.转换成需要处理的格式
StringBuilder sb = new StringBuilder();
String[] attrs = decode.split("&");
for (String attr : attrs) {
String[] kv = attr.split("=");
String val = kv.length >= 2 ? kv[1] : "";
sb.append(val+"|");
}
sb.append(request.getRemoteAddr());
String logStr = sb.toString();
// System.out.println(logStr);
logger.info(logStr);
}
}