一套 Web 自动曝光埋点技术方案(转)
前言
首先在介绍这套方案前,咱们还是简单地普及一下“埋点”这个名词。
埋点是指在各个终端(如网页、小程序)中收集一些关键访问数据并将数据发送到日志服务器,以供后续的数据分析。
如下笔者在写这篇文章之前对公司内的一些业务做的访谈调研记录,可以发现埋点在实际业务中大概会有这些作用:
- “采集并针对性做些投放调整,比如会员权益的展现、影院场次的优先露出、用户想看和看过的互动等”
- “做新春大盘活动的时候,某些模块曝光次数不够高,运营会调整相应策略”
- “个性化推荐,根据曝光和点击情况推荐用户数据”
- “我们这边埋点数据对算法开发、模型训练、效果评估起决定性作用”
- “观察用户逛会场深度的分布,做相应决策”
在简单介绍今天的主角——埋点的定义后,接下来,我们一起来研究一下自动曝光这件事情。
什么是自动曝光?
自动曝光是指按照埋点规范在页面上进行一个简单的声明式埋点,第三方采集SDK会根据埋点信息自动的采集元素曝光信息的一种方式。
如下图,页面滑动过程中A、B、C、D模块出现在视口内采集SDK会自动上报埋点日志:
典型轮播图场景,图片滚动出现后需要打曝光日志:
自动曝光的实现难点?
1、一般而言产品上会要求页面上某个模块一定面积连续一段时间出现在视口才是有效曝光(如30%、300ms)
2、性能,几乎所有的第三方采集平台都会在曝光埋点的说明文档里注明:“请不要配置过多的曝光埋点,这会严重影响你的页面性能”
两个埋点方式
HTML如下:
<title>轮播图自动曝光埋点demo</title>
<style type="text/css">
ul {
padding: 0;
}
.clear{
clear:both;
zoom: 1;
}
*, :after, :before {
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.clearfix:after {
clear: both;
}
.clearfix:after,
.clearfix:before {
display: table;
content: "";
}
.promo-bd {
margin: 0 auto;
overflow: hidden;
width: 520px;
}
.promo-bd .items-container {
list-style: none;
overflow: hidden;
width: 2280px;
left: 0px;
opacity: 1;
height: 280px;
}
.promo-bd .items-container .item {
display: list-item;
float: left;
overflow: hidden;
display: block;
visibility: visible;
height: 100%;
}
.promo-bd .items-container .item a {
display: inline-block;
height: 100%;
}
.sld-ft-nav {
text-align: center;
}
.sld-ft-nav li {
display: inline-block;
margin-left: 8px;
border-radius: 10px;
width: 20px;
height: 20px;
line-height: 20px;
background-color: #ccc;
color: #fff;
font-size: 12px;
cursor: pointer;
}
.sld-ft-nav li:first-child {
margin-left: 0px;
}
.sld-ft-nav li.selector {
background-color: #ff7300;
}
<div class="container">
<div class="promo-bd">
<div class="items-container clear">
<div class="item" data-id="111">
<a href="#">
<img src="https://img.alicdn.com/tfs/TB1fEOLCrr1gK0jSZFDXXb9yVXa-520-280.jpg">
</a>
</div>
<div class="item" data-id="112">
<a href="#">
<img src="//img.alicdn.com/tfs/TB1BrwUFuL2gK0jSZPhXXahvXXa-520-280.jpg_q90_.webp">
</a>
</div>
<div class="item" data-id="113">
<a href="#">
<img border="0" src="//aecpm.alicdn.com/simba/img/TB183NQapLM8KJjSZFBSutJHVXa.jpg">
</a>
</div>
<div class="item" data-id="114">
<a href="#">
<img border="0" src="//aecpm.alicdn.com/simba/img/TB1JNHwKFXXXXafXVXXSutbFXXX.jpg">
</a>
</div>
</div>
<ul class="promo-nav sld-ft-nav">
<li class="dot selector" onclick="handlerClick(0)">1</li>
<li class="dot" onclick="handlerClick(1)">2</li>
<li class="dot" onclick="handlerClick(2)">3</li>
<li class="dot" onclick="handlerClick(3)">4</li>
</ul>
</div>
</div>
<script type="text/javascript">
let handlerLoop;
let width = 520;
function transform (num) {
document.querySelector('.items-container').setAttribute('style', `
transition-duration: 0.3s;
transform: translate3d(-${width * num}px, 0px, 0px);
backface-visibility: hidden;
left: 0px;
opacity: 1;
`);
document.querySelectorAll('li.dot').forEach(function(ele, i){
ele.setAttribute('class', 'dot');
if (i === num) {
ele.setAttribute('class', 'dot selector');
}
});
}
function loop (n) {
let num = n;
handlerLoop = setInterval(function(){
if (num === 3) {
num = 0;
} else {
num++;
}
transform(num);
}, 1500);
}
loop(0);
function handlerClick (index) {
clearInterval(handlerLoop);
transform(index);
loop(index);
}
</script>
方式1:在head头部声明式埋点
...... <meta name="auto-exp-track" content='[{"logkey":"/banner.item.image","cssSelector":".item","props":["data-id"]}]' />
......
方式2:JS注入式埋点
...... <script> var q = (window.tracksdk_queue.push || (window.tracksdk_queue.push = [])); q.push({ action: 'trackSdk.setMetaInfo', arguments: ['auto-exp-track',[{ "logkey":"/banner.item.image", "cssSelector":".item", "props":["data-id"] }
......
以上两种方式,埋点SDK均会判断cssSelector=".name"的所有元素被曝光时,自动采集这个标签的曝光信息,及当前元素上"data-id"等props参数打包在一起,以logkey=/banner.item.image的形式发出去。
如:https://tracker.xxx.com/banner.item.image
技术原理
生命周期
技术细节
1、初始化:DomReady后做监听埋点配置变化(watchConfig)
2、watchConfig首次拿到埋点配置
①监听dom变化(watchDOM)
- 步骤一、使用MutationObserver或轮询(浏览器最小化、浏览器后台运行、tab未激活均会暂停轮询,直到浏览器窗口再次激活)。MutationObserver监听除['IFRAME', 'BODY', 'OBJECT', 'SCRIPT', 'NOSCRIPT','#text', 'LINK', 'STYLE']之外的所有节点增加监听['class', 'style']属性变化;
- 步骤二、监听到有dom变化后判断当前元素的track-ae属性是否有值,有则跳过,无则组装参数对象并生成元素唯一HASH,同时设置符合条件的节点状态:“status=init”,再push到_aeElementsHashMap中;
- 步骤三、分发一个内部事件消息“_AE_DOM_CHANGE”,用于通知watchExposure。
②监听曝光(watchExposureByIntersectionObserver)
- 步骤一、监听来自watchDOM分发的内部消息“_AE_DOM_CHANGE”,转至步骤二;
- 步骤二、使用IntersectionObserver包装埋点配置,拿到回调后将节点带入步骤三;
- 步骤三、遍历aeElementsHashMap,拿到“status===init && element from IntersectionObserver”后判断被曝光的元素是否符合要求(默认按可视面积30%),符合条件的节点设置“status=exposure_start”并更新aeElementsHashMap;
- 步骤四、在步骤三的基础上setTimeout 300ms后用getBoundingClientRect拿到节点坐标宽高信息再使用自定义交叉计算方法重新计算一遍交叉面积,如果依然超过30%的面积在视口内,那么将符合条件的节点设置“status=exposure_complete”并更新aeElementsHashMap。分发日志发送命令“AE_EXPOSURE_COMPLETE”,用于通知watchRecord;
- 步骤五、修改已曝光元素dom锚点:track-ae="${HASH}"。
③监听曝光(watchExposureByCustomIntersection)
- 步骤一、监听来自watchDOM分发的内部消息“_AE_DOM_CHANGE”,转至步骤三;
- 步骤二、监听touchmove、scroll、resize三种事件回调函数做成throttle_handler_exposure,拿到回调进入步骤三;
- 步骤三、遍历aeElementsHashMap,拿到“status===init”的节点,然后用getBoundingClientRect获取节点坐标宽高计算出交叉面积,符合可视面积超过30%的节点设置“status=exposure_start”并更新aeElementsHashMap;
- 步骤四、在步骤三的基础上setTimeout 300ms后用getBoundingClientRect拿到节点坐标宽高信息再使用自定义交叉计算方法重新计算一遍交叉面积,如- 果依然超过30%的面积在视口内,那么 将符合条件的节点设置“status=exposure_complete”并更新aeElementsHashMap。分发日志发送命令AE_EXPOSURE_COMPLETE,用于通知watchRecord;
- 步骤五、修改已曝光元素dom锚点:track-ae="${HASH}"。
④监听日志发送命令(watchRecord)
- 步骤一、监听到“_AE_EXPOSURE_COMPLETE”消息后最多10个元素打包在一起发送日志;
- 步骤二、清理_aeElementsHashMap上下文;
- 步骤三、给待曝光元素设置dom锚点:track-ae="${index}";
⑤watchConfig非首次拿到埋点配置
1、配置不为空时do_reset,做两件事:1.1、重置_aeElementsHashMap上下文;1.2、将符合条件的元素的track-ae属性重置成同类节点索引值,以触发下一轮曝光监听。
2、配置为空值时do_destroy,做三件事:2.1、销毁_aeElementsHashMap上下文;2.2、移除所有监听事件;2.3、清空所有track-ae锚点属性。
效果
性能
笔者通过大量测试和线上优化,拿目前这套架构方案与GA做了一个对比,即对208个元素做了曝光埋点,连续来回滚动,直到所有元素都完成曝光日志上报的性能对比:
额外收益
有了这个基础,做如下两件事会显得比较轻松了
1、可视化埋点
2、可视化分析
原文链接>>
参考文献:
IntersectionObserver
IntersectionObserverPolyfill