前言
首先在介紹這套方案前,咱們還是簡單地普及一下“埋點”這個名詞。
埋點是指在各個終端(如網頁、小程序)中收集一些關鍵訪問數據並將數據發送到日志服務器,以供后續的數據分析。
如下筆者在寫這篇文章之前對公司內的一些業務做的訪談調研記錄,可以發現埋點在實際業務中大概會有這些作用:
- “采集並針對性做些投放調整,比如會員權益的展現、影院場次的優先露出、用戶想看和看過的互動等”
- “做新春大盤活動的時候,某些模塊曝光次數不夠高,運營會調整相應策略”
- “個性化推薦,根據曝光和點擊情況推薦用戶數據”
- “我們這邊埋點數據對算法開發、模型訓練、效果評估起決定性作用”
- “觀察用戶逛會場深度的分布,做相應決策”
在簡單介紹今天的主角——埋點的定義后,接下來,我們一起來研究一下自動曝光這件事情。
什么是自動曝光?
自動曝光是指按照埋點規范在頁面上進行一個簡單的聲明式埋點,第三方采集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