前言

埋点,是网站分析的一种常用的数据采集方法。我们主要用来采集用户行为数据(例如页面访问路径,点击了什么元素)进行数据分析,从而让运营同学更加合理的安排运营计划。现在市面上有很多第三方埋点服务商,百度统计,友盟,growingIO 等大家应该都不太陌生,大多情况下大家都只是使用,最近我研究了下 web 埋点,你要不要了解下。

现有埋点三大类型

用户行为分析是一个大系统,一个典型的数据平台。由用户数据采集,用户行为建模分析,可视化报表展示几个模块构成。现有的埋点采集方案可以大致被分为三种,手动埋点,可视化埋点,无埋点
  1. 手动埋点
    手动代码埋点比较常见,需要调用埋点的业务方在需要采集数据的地方调用埋点的方法。优点是流量可控,业务方可以根据需要在任意地点任意场景进行数据采集,采集信息也完全由业务方来控制。这样的有点也带来了一些弊端,需要业务方来写死方法,如果采集方案变了,业务方也需要重新修改代码,重新发布。
  2. 可视化埋点
    可是化埋点是近今年的埋点趋势,很多大厂自己的数据埋点部门也都开始做这块。优点是业务方工作量少,缺点则是技术上推广和实现起来有点难(业务方前端代码规范是个大前提)。阿里的活动页很多都是运营通过可视化的界面拖拽配置实现,这些活动控件元素都带有唯一标识。通过埋点配置后台,将元素与要采集事件关联起来,可以自动生成埋点代码嵌入到页面中。
  3. 无埋点
    无埋点则是前端自动采集全部事件,上报埋点数据,由后端来过滤和计算出有用的数据,优点是前端只要加载埋点脚本。缺点是流量和采集的数据过于庞大,服务器性能压力山大,主流的 GrowingIO 就是这种实现方案。

我们暂时放弃可视化埋点的实现,在 ​​手动埋点​​ 和 ​​无埋点​​ 上进行了尝试,为了便于描述,下文我会称采集脚本为 SDK。

思考几个问题

埋点开发需要考虑很多内容,贯穿着不轻易动手写代码的原则,我们在开发前先思考下面这几个问题
  1. 我们要采集什么内容,进行哪些采集接口的约定
  2. 业务方通过什么方式来调用我们的采集脚本
  3. 手动埋点:SDK 需要封装一个方法给业务方进行调用,传参方式业务方可控
  4. 无埋点:考虑到数据量对于服务器的压力,我们需要对无埋点进行开关配置,可以配置进行哪些元素进行无埋点采集
  5. 用户标识:游客用户和登录用户的采集数据怎么进行区分关联
  6. 设备Id:用户通过浏览器来访问 web 页面,设备Id需要存储在浏览器上,同一个用户访问不同的业务方网站,设备Id要保持一样,怎么实现
  7. 单页面应用:现在流行的单页面应用和普通 web 页面的数据采集是否有差异
  8. 混合应用:app 与 h5 的混合应用我们要怎么进行通讯

我们要采集什么内容,进行哪些采集接口的约定

第一期我们先实现对 PV(即页面浏览量或点击量) 、UV(一天内同个访客多次访问) 、点击量、用户的访问路径的基础指标的采集。精细化分析的流量转化需要和业务相关,需要和数据分析方做约定,我们预留扩展。所以我们的采集接口需要进行以下的约定

{
"header":{ // HTTP 头部
"X-Device-Id":" 550e8400-e29b-41d4-a716-446655440000", //设备ID,用来区分用户设备
"X-Source-Url":"https://www.baidu.com/", //源地址,关联用户的整个操作流程,用于用户行为路径分析,例如登录,到首页,进入商品详情,退出这一整个完整的路径
"X-Current-Url":"", //当前地址,用户行为发生的页面
"X-User-Id":"",//用户ID,统计登录用户行为
},
"body":[{ // HTTP Body体
"PageSessionID":"", //页面标识ID,用来区分页面事件,例如加载和离开我们会发两个事件,这个标识可以让我们知道这个事件是发生在一个页面上
"Event":"loaded", //事件类型,区分用户行为事件
"PageTitle": "埋点测试页", //页面标题,直观看到用户访问页面
"CurrentTime": “1517798922201”, //事件发生的时间
"ExtraInfo": {
} //扩展字段,对具体业务分析的传参
}]
}


以上就是我们现在约定好了的通用的事件采集的接口,所传的参数基本上会根据采集事件的不同而发生变化。但是在用户的整一个访问行为中,用户的设备是不会变化的,如果你想采集设备信息可以重新约定一个接口,在整个采集开始之前发送设备信息,这样可以避免在事件采集接口上重复采集固定数据。

{
"header":{ // HTTP 头部
"X-Device-Id" :"550e8400-e29b-41d4-a716-446655440000" , // 设备id
},
"body":{ // HTTP Body体
"DeviceType": "web" , //设备类型
"ScreenWide" : 768 , // 屏幕宽
"ScreenHigh": 1366 , // 屏幕高
"Language": "zh-cn" //语言
}
}


业务方通过什么方式来调用我们的采集脚本

埋点应该让调用的业务方,尽可能少有工作量,最好是什么都不用做,????,但是实现起来有点难额。我们采用的方案是让业务方在代码里通过 script 脚本来引用我们的 SDK ,业务方只要配置一些需要的参数进行埋点定制(????我们讲到过的无埋点的流量控制),然后什么都不做就可以进行基础数据的采集。

(function() {
var collect = document.createElement('script');
collect.type = 'text/javascript';
collect.async = true;
collect.src = 'http://collect.trc.com/index.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(collect, s);
})();


//用户自定义要进行无埋点采集的元素,如果不进行无埋点采集,可以不配置
var _XT = [];
_XT.push(['Target','div']);


手动埋点:SDK

如果业务方需要采集更多业务定制的数据,可以调用我们暴露出的方法进行采集


//自定义事件
sdk.dispatch('customEvent',{extraInfo:'自定义事件的额外信息'})


游客与用户关联

我们使用 userId 来做用户标识,同一个设备的用户,从游客用户切换到登录用户,如果我们要把他们关联起来,需要有一个设备Id 做关联

web 设备Id

用户通过浏览器来访问 web 页面,设备Id需要存储在浏览器上,同一个用户访问不同的业务方网站,设备Id要保持一样。web 变量存储,我们第一时间想到的就是 cookie,sessionStorage,localStorage,但是这3种存储方式都和访问资源的域名相关。我们总不能每次访问一个网站就新建一个设备指纹吧,所以我们需要通过一个方法来跨域共享设备指纹

我们想到的方案是,通过嵌套 iframe 加载一个静态页面,在 iframe 上加载的域名上存储设备id,通过跨域共享变量获取设备id,共享变量的原理是采用了iframe 的 contentWindow通讯,通过 postMessage 获取事件状态,调用封装好的回调函数进行数据处理具体的实现方式

//web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id,
collect.setIframe = function () {
var that = this
var iframe = document.createElement('iframe')
iframe.id = "frame",
iframe.src = 'http://collectiframe.trc.com' // 配置域名代理,目的是让开发测试生产环境代码一致
iframe.style.display='none' //iframe 设置的目的是用来生成固定的设备id,不展示
document.body.appendChild(iframe)

iframe.onload = function () {
iframe.contentWindow.postMessage('loaded','*');
}

//监听message事件,iframe 加载完成,获取设备id ,进行相关的数据采集
helper.on(window,"message",function(event){
that.deviceId = event.data.deviceId

if(event.data && event.data.type == 'loaded'){
that.sendDevice(that.getDevice(), that.deviceUrl);
setTimeout(function () {
that.send(that.beforeload)
that.send(that.loaded)
},1000)
}
})
}


iframe 与 SDK 通讯

function receiveMessageFromIndex ( event ) {
getDeviceInfo() // 获取设备信息
var data = {
deviceId: _deviceId,
type:event.data
}

event.source.postMessage(data, '*'); // 将设备信息发送给 SDK
}

//监听message事件
if(window.addEventListener){
window.addEventListener("message", receiveMessageFromIndex, false);
}else{
window.attachEvent("onmessage", receiveMessageFromIndex, false)

单页面应用:现在流行的单页面应用和普通 web 页面的数据采集是否有差异

我们知道单页面应用都是无刷新的页面加载,所以我们在页面​​跳转​​的处理和我们的普通的页面会有所不同。单页面应用的路由插件运用了 window 自带的无刷新修改用户浏览记录的方法,pushState 和 replaceState。

window 的 history 对象 提供了两个方法,能够无刷新的修改用户的浏览记录,pushSate,和 replaceState,区别的 pushState 在用户访问页面后面添加一个访问记录, replaceState 则是直接替换了当前访问记录,所以我们只要改写 history 的方法,在方法执行前执行我们的采集方法就能实现对单页面应用的页面跳转事件的采集了

 // 改写思路:拷贝 window 默认的 replaceState 函数,重写 history.replaceState 在方法里插入我们的采集行为,在重写的 replaceState 方法最后调用,window 默认的 replaceState 方法

collect = {}
collect.onPushStateCallback : function(){} // 自定义的采集方法

(function(history){
var replaceState = history.replaceState; // 存储原生 replaceState
history.replaceState = function(state, param) { // 改写 replaceState
var url = arguments[2];
if (typeof collect.onPushStateCallback == "function") {
collect.onPushStateCallback({state: state, param: param, url: url}); //自定义的采集行为方法
}
return replaceState.apply(history, arguments); // 调用原生的 replaceState
};
})(window.history);


混合应用:app 与 h5 的混合应用我们要怎么进行通讯

现在大部分的应用都不是纯原生的应用, app 与 h5 的混合的应用是现在的一种主流。

纯 web 数据采集我们考虑到前端存储数据容易丢失,我们在每一次事件触发的时候都用采集接口传输采集到的数据。考虑到现在很多用户的手机会有流量管家的软件监控,如果在 App 中 h5 还是采集到数据就传输给服务端,很有可能会让流量管家检测到,给用户报警,从而使得用户不再信任你的 App , 所以我们在用户操作的时候将数据传给 app 端,存储到 app。用户切换应用到后台的时候,通过 app 端的 SDK 打包传输到服务器,我们给 app 提供的方法封装了一个适配器

// app 与 h5 混合应用,直接将数信息发给 app
collect.saveEvent = function (jsonString) {

collect.dcpDeviceType && setTimeout(function () {
if(collect.dcpDeviceType=='android'){
android.saveEvent(jsonString)
} else {
window.webkit && window.webkit.messageHandlers ? window.webkit.messageHandlers.nativeBridge.postMessage(jsonString) : window.postBridgeMessage(jsonString)
}

},1000)
}


实现思路

通过上面几个问题的思考,我们对埋点的实现大致已经有了一些想法,我们使用思维导图来还原下我们即将要做的事情,图片记得放大看哦,太小了可能看不清。

我们需要暴露给业务方调用的方法

业务方通过什么方式来调用我们的采集脚本  美团点评前端无痕埋点实践_加载

 

我们需要处理的事件类型

业务方通过什么方式来调用我们的采集脚本  美团点评前端无痕埋点实践_控件_02

 

SDK 的基本实现思路

业务方通过什么方式来调用我们的采集脚本  美团点评前端无痕埋点实践_控件_03

 

 

我们来看下几个核心代码的实现

工具方法

我们定义了几个工具方法,提高开发的幸福指数 ????

    var helper = {};

// 生成一个唯一的标识,pageSessionId (用这个变量来关联开始加载、加载完成、离开页面的事件,计算出页面加菜时间,停留时间)
helper.uuid = function(){}

// 元素绑定事件监听,兼容浏览器到IE8
helper.on = function(){}

//元素移除事件监听的适配器函数,兼容浏览器到IE8
helper.remove = function(){}

//将json转为字符串,事件传输的参数类型转化
helper.changeJSON2Query = function(){}

//将相对路径解析成文档全路径
helper.normalize = function(){}


采集逻辑

    var collect = {
deviceUrl:'http://collect.trc.com/rest/collect/device/h5/v1',
eventUrl:'http://collect.trc.com/rest/collect/event/h5/v1',
isuploadUrl:'http://collect.trc.com/rest/collect/isupload/app/v1',
parmas:{ ExtraInfo:{} },
device:{}
};

//获取埋点配置
collect.setParames = function(){}

//更新访问路径及页面信息
collect.updatePageInfo = function(){}

//获取事件参数
collect.getParames = function(){}

//获取设备信息
collect.getDevice = function(){}

//事件采集
collect.send = function(){}

//设备采集
collect.sendDevice = function(){}

//判断才否采集,埋点采集的开关
collect.isupload = function(){

1. 判断是否采集,不采集就注销事件监听(项目中区分游客身份和用户身份的采集情况,这个方法会被判断两次)
2. 采集则判断是否已经采集过
a.已经采集过不做任何操作
b.没有采集过添加事件监听
3. 判断是 混合应用还是纯 web 应用
a.如果是web 应用,调用 collect.setIframe 设置 iframe
b.如果是混合应用 将开始加载和加载完成事件传输给 app
}

//点击事件处理函数
collect.clickHandler = function(){}

//离开页面的事件处理函数
collect.beforeUnloadHandler = function(){}

//页面回退事件处理函数
collect.onPopStateHandler = function(){}

//系统事件初始化,注册离开事件,浏览器后退事件
collect.event = function(){}

//获取记录开始加载数据信息
collect.getBeforeload = function(){}

//存储加载完成,获取设备类型,记录加载完成信息
collect.onload = function(){

1. 判断cookie是否有存设备类型信息,有表示混合应用
2. 采集加载完成时间等信息
3. 调用 collect.isupload 判断是否进行采集
}

//web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id
collect.setIframe = function(){}

//app 与 h5 混合应用,直接将数信息发给 app,判断设备类型做原生方法适配器
collect.saveEvent = function(){}

//采集自定义事件类型
collect.dispatch = function(){}

//将参数 userId 存入sessionStorage
collect.storeUserId = function(){}

//采集H5信息,如果是混合应用,将采集到的信息发送给 app 端
collect.saveEventInfo = function(){}

//页面初始化调用方法
collect.init = function(){

1. 获取开始加载的采集信息
2. 获取 SDK 配置信息,设备信息
3. 改写 history 两个方法,单页面应用页面跳转前调用我们自己的方法
4. 页面加载完成,调用 collect.onload 方法

}


collect.init(); // 初始化

//暴露给业务方调用的方法
return {
dispatch:collect.dispatch,
storeUserId:collect.storeUserId,
}





构建一个数据平台,大体上包括数据采集、数据上报、数据存储、数据计算以及数据可视化展示等几个重要的环节。其中,数据采集与上报是整个流程中重要的一环,只有确保前端数据生产的全面、准确、及时,最终产生的数据结果才是可靠的、有价值的。

为了解决前端埋点的准确性、及时性、开发效率等问题,业内各家公司从不同角度,提出了多种技术方案,这些方案大体上可以归为三类:

  1. 第一类是代码埋点,即在需要埋点的节点调用接口直接上传埋点数据,友盟、百度统计等第三方数据统计服务商大都采用这种方案;
  2. 第二类是可视化埋点,即通过可视化工具配置采集节点,在前端自动解析配置并上报埋点数据,从而实现所谓的“无痕埋点”, 代表方案是已经开源的Mixpanel
  3. 第三类是“无埋点”,它并不是真正的不需要埋点,而是前端自动采集全部事件并上报埋点数据,在后端数据计算时过滤出有用数据,代表方案是国内的GrowingIO。

美团点评对于前端埋点的要求很高,总结起来主要有三点需求:

  1. 数据的准确性和及时性,数据质量的好坏将直接影响依赖埋点数据的后端策略服务、与合作伙伴结算、以及运营数据报表等等。
  2. 埋点的效率,埋点的复杂度往往与业务需求相关,埋点效率会影响版本迭代的速度。
  3. 动态部署与修复埋点的能力,本质上这也是提升埋点效率的一种手段,并且使埋点不再依赖于客户端发版。

公司原有埋点主要采用手动代码埋点的方案,代码埋点虽然使用起来灵活,但是开发成本较高,并且一旦上线就很难修改。如果发生严重的数据问题,我们只能通过发热修复解决。如果直接改进为可视化埋点,开发成本较高,并且也不能解决所有埋点需求;改进为无埋点的话,带来的流量消耗和数据计算成本也是业务不能接受的。因此,我们在原有代码埋点方案的基础上,演化出了一套轻量的、声明式的前端埋点方案,并且在动态埋点、无痕埋点等方向做了进一步的探索和实践。

代码埋点

由于后面要介绍的声明式埋点和无痕埋点方案仍然依赖原有代码埋点的底层逻辑,这里有必要简单介绍下代码埋点。在实现代码埋点时,我们主要关注的是数据结构的规范性、埋点接口的易用性、上报策略的可靠性等问题。整体的模块划分如下图所示。

开发者需要手动在需要埋点的节点处(例如:点击事件的回调方法、列表元素的展示回调方法、页面的生命周期函数等等)插入这些埋点代码。

EventInfo eventInfo = new EventInfo();
eventInfo.nm = EventName.MGE; // 事件类型为MGE
eventInfo.val_bid = "xxx"; // 事件的唯一标标识
eventInfo.val_lab = new HashMap<>(); // 携带的业务数据
eventInfo.val_lab.put(Constants.Business.xx,"xxx");
Statistics.getChannel("hotel").writeEvent(eventInfo);


可以看出,代码埋点是一种典型的命令式编程,因此埋点代码常常要侵入具体的业务逻辑,这使埋点代码变得很繁琐并且容易出错。因此,最直接的做法就是将埋点代码与业务逻辑解耦,也就是“声明式编程”,从而降低埋点的难度。

声明式埋点

声明式埋点的思路是将埋点代码和具体的交互和业务逻辑解耦,开发者只用关心需要埋点的控件,并且为这些控件声明需要的埋点数据即可,从而降低埋点的成本。

Android

在Android中,我们自定义了常用的UI控件,例如TextView、LinearLayout、ListView、ViewPager等,重写了事件响应方法,在这些方法内部自动填写埋点代码。重写控件的好处在于可以拦截到更多的事件,执行效率高并且运行稳定。但其弊端也非常明显——移植成本很高!

为了解决这个问题,我们借鉴了Android v7支持库的思路,即通过AppCompatDelegate代理自动替换UI控件。

public class GAAppCompatDelegateV14 extends AppCompatDelegateImplV14 {
@Override
View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
switch (name) {
case "TextView":
return new NovaTextView(context, attrs);
}
return super.callActivityOnCreateView(parent, name, context, attrs);
}
}


这样,开发者只需要在自己的Activity基类中重写getDelegate方法,将方法的返回值替换为修改过的AppCompatDelegate,就可以实现自动替换UI控件了。

@Override
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = GAAppCompatUtil.create(this, this);
}
return mDelegate;
}


然而,新的问题又出现了。

如果引用的第三方库中重写了UI控件,上述方法是不生效的,也就是说我们需要一种替换UI控件类的父类方法。可是在运行时,我们没有找到可行的替换UI控件类的父类方法。因此,我们尝试在编译时修改父类,并开发了一个Gradle插件。事实上,这样做并不存在运行时效率的问题,只是会牺牲一些编译速度。这样开发者只需要运行这个插件,就可以实现自动将UI控件的父类替换为我们重写的UI控件了。

apply plugin: 'com.meituan.judasplugin'


采用了声明式埋点后,只需要在控件初始化时声明一下需要的埋点就可以了。我们不必再侵入程序的各种响应函数,降低了埋点的难度。

GAHelper.bindClick(view, bid, lab);


iOS

在iOS中,利用Objective-C关联属性和类别的语法特性,我们无需重写UI控件,就能实现声明式打点。对于UIControl,可以在声明埋点时添加新的action,并在事件发生时自动填写埋点代码。

- (void)nvja_setAnalyticsParams:(NVJAMGEParameter *)params mgeType:(SAKStatisticsEventMGEType)type
{
if (self.wmja_clickParams == nil && type == SAKStatisticsEventClick) {
[self addTarget:self action:@selector(wmja_controlDidTapped:) forControlEvents:UIControlEventTouchUpInside];
}
[super nvja_setAnalyticsParams:params mgeType:type];
}


对于UITableView,可以通过重写UITableViewDelegate,利用消息传递机制拦截事件,并在事件回调方法中自动填写埋点代码。

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL selector = [anInvocation selector];
if (self.originalDelegate && [self.originalDelegate respondsToSelector:selector]) {
[anInvocation invokeWithTarget:self.originalDelegate];
}
SEL nvjaSelector = [self nvjaSelector:selector];
if ([super respondsToSelector:nvjaSelector]) {
[anInvocation setSelector:nvjaSelector];
[anInvocation invokeWithTarget:self];
}
}


同样的,采用了声明式埋点后,埋点代码得到了简化。

NVJAMGEParameter *parameter = [[NVJAMGEParameter alloc] init];
parameter.bid = @"bid";
parameter.lab = @{@"poi_id":@"1"};
button.nvja_clickParams = parameter;


声明式埋点能够替代所有的代码埋点,并且能解决早期遇到的移植成本高等问题。但是其本质上还是一种代码埋点,只是埋点的代码减少了,并且不再侵入业务逻辑了。如果要满足动态部署与修复埋点的需求,就需要彻底消灭写死在前端的埋点代码。

无痕埋点

我们注意到,之所以声明式埋点还需要写死代码,主要有两个原因:第一是需要声明埋点控件的唯一事件标识,即bid;第二是有的业务字段需要在前端埋点时携带,而这些字段是在运行时才可获知的值。

对于第一点,我们可以尝试在前后端使用一致的规则自动生成事件标识,这样后端就可以配置前端的埋点行为,从而做到自动化埋点。对于第二点,可以尝试通过某种方式将业务数据自动与埋点数据关联,这种关联可以发生在前端,也可以发生在后端。

事件标识

为了自动生成事件标识,我们需要获取每个控件自身的ID、类名以及位于所属父组件的Index等特征信息,并逐级向上遍历找到根节点。根节点一般是手动标记的,如果没有标记则默认是视图层次树的顶层节点。最后,将遍历产生的路径上所有节点的特征信息组合在一起,就是这个事件的标识。考虑到在实际布局中有可能存在一些动态插入的控件,我们允许父组件的Index有一定的误差。

配置后台需要维护自动生成的事件标识和bid的映射关系,并且可以下发给前端一个配置文件。当前端控件事件触发时,自动和配置文件匹配就可以拿到对应的bid了。需要注意的是,配置后台维护事件标识的工作可不是一件轻松的事情,主要的复杂性在于不同版本之间布局变更导致的事件标识变更,这就是为什么还需要手动标记根节点的原因。所以,一般我们会选取不易变更的视图节点。

数据关联

为了实现业务数据与埋点数据的自动关联,我们起初尝试了前后端日志关联的方式。即在前端请求后端API的时机,由后端将业务数据写入日志,最后在数据清洗时将相对应的前后端日志合并。这种方式带来的问题是后端改造成本较高,并且数据清洗的开销较大,因此并不能广泛应用。但是在一些特殊场景下,例如某些业务数据只有后端可以获知,而前端不能获知时,这种关联是必要的。

更常见的数据关联发生在前端数据之间。当页面跳转时,通过传递规范的跳转URI Scheme,将业务数据传递给下个页面,并且自动填入这个页面的PV事件中。而该页面内产生的所有其他事件,都会携带与PV事件相同的业务数据。

这样,通过自动产生事件标识并进行数据关联,我们就能够实现“无痕埋点”了,并且埋点节点可以通过配置文件动态下发,从而具备了动态部署与修复埋点的能力。但需要注意的是,这种“无痕埋点”并不能解决所有问题,当业务字段无法通过数据关联获取时(这种情况比较常见),仍然需要开发者代码埋点或声明式埋点指定业务字段。就目前实践阶段的数据来看,业务中大约70%左右的埋点需求可以通过无痕埋点解决,而对于另外30%的埋点需求,仍然需要使用声明式埋点和代码埋点。

总结

前端数据采集与上报是构建数据平台过程中最重要的环节,美团点评前端每天上报的数据达到百亿次级别。为了更好的满足公司各业务日益复杂的埋点需求,以及对埋点准确性、及时性、开发效率的要求,我们在代码埋点方案的基础上演化出了一套轻量的、声明式的前端埋点方案,并且在动态埋点、无痕埋点等方向做了进一步的探索和实践。目前声明式埋点已经在部分业务上全量使用,从数据质量和开发者反馈来看,取得了预期的收益。而无痕埋点也正在一些业务上验证和持续优化中,后面也会在公司范围内进一步推广。

在实践中我们认识到,埋点问题不能通过单一一种技术方案来解决,在不同场景下我们需要选择不同的埋点方案。例如对于简单的用户行为类事件,可以使用无痕埋点解决;而对于需要携带大量运行时才可获知的业务字段的埋点需求,就需要声明式埋点来解决。从更高的层面来看,除了前端埋点技术的优化,埋点数据的规范化、前后端协同埋点、数据清洗和关联对于未来构建更加自动化、动态化的埋点体系同样非常重要。