web 埋点实现原理了解一下
前言
埋點(diǎn),是網(wǎng)站分析的一種常用的數(shù)據(jù)采集方法。我們主要用來(lái)采集用戶行為數(shù)據(jù)(例如頁(yè)面訪問(wèn)路徑,點(diǎn)擊了什么元素)進(jìn)行數(shù)據(jù)分析,從而讓運(yùn)營(yíng)同學(xué)更加合理的安排運(yùn)營(yíng)計(jì)劃。現(xiàn)在市面上有很多第三方埋點(diǎn)服務(wù)商,百度統(tǒng)計(jì),友盟,growingIO 等大家應(yīng)該都不太陌生,大多情況下大家都只是使用,最近我研究了下 web 埋點(diǎn),你要不要了解下。
現(xiàn)有埋點(diǎn)三大類型
用戶行為分析是一個(gè)大系統(tǒng),一個(gè)典型的數(shù)據(jù)平臺(tái)。由用戶數(shù)據(jù)采集,用戶行為建模分析,可視化報(bào)表展示幾個(gè)模塊構(gòu)成。現(xiàn)有的埋點(diǎn)采集方案可以大致被分為三種,手動(dòng)埋點(diǎn),可視化埋點(diǎn),無(wú)埋點(diǎn)手動(dòng)代碼埋點(diǎn)比較常見(jiàn),需要調(diào)用埋點(diǎn)的業(yè)務(wù)方在需要采集數(shù)據(jù)的地方調(diào)用埋點(diǎn)的方法。優(yōu)點(diǎn)是流量可控,業(yè)務(wù)方可以根據(jù)需要在任意地點(diǎn)任意場(chǎng)景進(jìn)行數(shù)據(jù)采集,采集信息也完全由業(yè)務(wù)方來(lái)控制。這樣的有點(diǎn)也帶來(lái)了一些弊端,需要業(yè)務(wù)方來(lái)寫死方法,如果采集方案變了,業(yè)務(wù)方也需要重新修改代碼,重新發(fā)布。
可是化埋點(diǎn)是近今年的埋點(diǎn)趨勢(shì),很多大廠自己的數(shù)據(jù)埋點(diǎn)部門也都開(kāi)始做這塊。優(yōu)點(diǎn)是業(yè)務(wù)方工作量少,缺點(diǎn)則是技術(shù)上推廣和實(shí)現(xiàn)起來(lái)有點(diǎn)難(業(yè)務(wù)方前端代碼規(guī)范是個(gè)大前提)。阿里的活動(dòng)頁(yè)很多都是運(yùn)營(yíng)通過(guò)可視化的界面拖拽配置實(shí)現(xiàn),這些活動(dòng)控件元素都帶有唯一標(biāo)識(shí)。通過(guò)埋點(diǎn)配置后臺(tái),將元素與要采集事件關(guān)聯(lián)起來(lái),可以自動(dòng)生成埋點(diǎn)代碼嵌入到頁(yè)面中。
無(wú)埋點(diǎn)則是前端自動(dòng)采集全部事件,上報(bào)埋點(diǎn)數(shù)據(jù),由后端來(lái)過(guò)濾和計(jì)算出有用的數(shù)據(jù),優(yōu)點(diǎn)是前端只要加載埋點(diǎn)腳本。缺點(diǎn)是流量和采集的數(shù)據(jù)過(guò)于龐大,服務(wù)器性能壓力山大,主流的 GrowingIO 就是這種實(shí)現(xiàn)方案。
我們暫時(shí)放棄可視化埋點(diǎn)的實(shí)現(xiàn),在 手動(dòng)埋點(diǎn) 和 無(wú)埋點(diǎn) 上進(jìn)行了嘗試,為了便于描述,下文我會(huì)稱采集腳本為 SDK。
思考幾個(gè)問(wèn)題
埋點(diǎn)開(kāi)發(fā)需要考慮很多內(nèi)容,貫穿著不輕易動(dòng)手寫代碼的原則,我們?cè)陂_(kāi)發(fā)前先思考下面這幾個(gè)問(wèn)題我們要采集什么內(nèi)容,進(jìn)行哪些采集接口的約定
第一期我們先實(shí)現(xiàn)對(duì) PV(即頁(yè)面瀏覽量或點(diǎn)擊量) 、UV(一天內(nèi)同個(gè)訪客多次訪問(wèn)) 、點(diǎn)擊量、用戶的訪問(wèn)路徑的基礎(chǔ)指標(biāo)的采集。精細(xì)化分析的流量轉(zhuǎn)化需要和業(yè)務(wù)相關(guān),需要和數(shù)據(jù)分析方做約定,我們預(yù)留擴(kuò)展。所以我們的采集接口需要進(jìn)行以下的約定
{"header":{ // HTTP 頭部"X-Device-Id":" 550e8400-e29b-41d4-a716-446655440000", //設(shè)備ID,用來(lái)區(qū)分用戶設(shè)備"X-Source-Url":"https://www.baidu.com/", //源地址,關(guān)聯(lián)用戶的整個(gè)操作流程,用于用戶行為路徑分析,例如登錄,到首頁(yè),進(jìn)入商品詳情,退出這一整個(gè)完整的路徑"X-Current-Url":"", //當(dāng)前地址,用戶行為發(fā)生的頁(yè)面"X-User-Id":"",//用戶ID,統(tǒng)計(jì)登錄用戶行為},"body":[{ // HTTP Body體"PageSessionID":"", //頁(yè)面標(biāo)識(shí)ID,用來(lái)區(qū)分頁(yè)面事件,例如加載和離開(kāi)我們會(huì)發(fā)兩個(gè)事件,這個(gè)標(biāo)識(shí)可以讓我們知道這個(gè)事件是發(fā)生在一個(gè)頁(yè)面上"Event":"loaded", //事件類型,區(qū)分用戶行為事件"PageTitle": "埋點(diǎn)測(cè)試頁(yè)", //頁(yè)面標(biāo)題,直觀看到用戶訪問(wèn)頁(yè)面"CurrentTime": “1517798922201”, //事件發(fā)生的時(shí)間"ExtraInfo": {} //擴(kuò)展字段,對(duì)具體業(yè)務(wù)分析的傳參}] }以上就是我們現(xiàn)在約定好了的通用的事件采集的接口,所傳的參數(shù)基本上會(huì)根據(jù)采集事件的不同而發(fā)生變化。但是在用戶的整一個(gè)訪問(wèn)行為中,用戶的設(shè)備是不會(huì)變化的,如果你想采集設(shè)備信息可以重新約定一個(gè)接口,在整個(gè)采集開(kāi)始之前發(fā)送設(shè)備信息,這樣可以避免在事件采集接口上重復(fù)采集固定數(shù)據(jù)。
{"header":{ // HTTP 頭部"X-Device-Id" :"550e8400-e29b-41d4-a716-446655440000" , // 設(shè)備id},"body":{ // HTTP Body體"DeviceType": "web" , //設(shè)備類型"ScreenWide" : 768 , // 屏幕寬"ScreenHigh": 1366 , // 屏幕高"Language": "zh-cn" //語(yǔ)言} }業(yè)務(wù)方通過(guò)什么方式來(lái)調(diào)用我們的采集腳本
埋點(diǎn)應(yīng)該讓調(diào)用的業(yè)務(wù)方,盡可能少有工作量,最好是什么都不用做,?,但是實(shí)現(xiàn)起來(lái)有點(diǎn)難額。我們采用的方案是讓業(yè)務(wù)方在代碼里通過(guò) script 腳本來(lái)引用我們的 SDK ,業(yè)務(wù)方只要配置一些需要的參數(shù)進(jìn)行埋點(diǎn)定制(?我們講到過(guò)的無(wú)埋點(diǎn)的流量控制),然后什么都不做就可以進(jìn)行基礎(chǔ)數(shù)據(jù)的采集。
(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);})();//用戶自定義要進(jìn)行無(wú)埋點(diǎn)采集的元素,如果不進(jìn)行無(wú)埋點(diǎn)采集,可以不配置var _XT = [];_XT.push(['Target','div']);手動(dòng)埋點(diǎn):SDK
如果業(yè)務(wù)方需要采集更多業(yè)務(wù)定制的數(shù)據(jù),可以調(diào)用我們暴露出的方法進(jìn)行采集
游客與用戶關(guān)聯(lián)
我們使用 userId 來(lái)做用戶標(biāo)識(shí),同一個(gè)設(shè)備的用戶,從游客用戶切換到登錄用戶,如果我們要把他們關(guān)聯(lián)起來(lái),需要有一個(gè)設(shè)備Id 做關(guān)聯(lián)
web 設(shè)備Id
用戶通過(guò)瀏覽器來(lái)訪問(wèn) web 頁(yè)面,設(shè)備Id需要存儲(chǔ)在瀏覽器上,同一個(gè)用戶訪問(wèn)不同的業(yè)務(wù)方網(wǎng)站,設(shè)備Id要保持一樣。web 變量存儲(chǔ),我們第一時(shí)間想到的就是 cookie,sessionStorage,localStorage,但是這3種存儲(chǔ)方式都和訪問(wèn)資源的域名相關(guān)。我們總不能每次訪問(wèn)一個(gè)網(wǎng)站就新建一個(gè)設(shè)備指紋吧,所以我們需要通過(guò)一個(gè)方法來(lái)跨域共享設(shè)備指紋
我們想到的方案是,通過(guò)嵌套 iframe 加載一個(gè)靜態(tài)頁(yè)面,在 iframe 上加載的域名上存儲(chǔ)設(shè)備id,通過(guò)跨域共享變量獲取設(shè)備id,共享變量的原理是采用了iframe 的 contentWindow通訊,通過(guò) postMessage 獲取事件狀態(tài),調(diào)用封裝好的回調(diào)函數(shù)進(jìn)行數(shù)據(jù)處理具體的實(shí)現(xiàn)方式
//web 應(yīng)用,通過(guò)嵌入 iframe 進(jìn)行跨域 cookie 通訊,設(shè)置設(shè)備id,collect.setIframe = function () {var that = thisvar iframe = document.createElement('iframe')iframe.id = "frame",iframe.src = 'http://collectiframe.trc.com' // 配置域名代理,目的是讓開(kāi)發(fā)測(cè)試生產(chǎn)環(huán)境代碼一致iframe.style.display='none' //iframe 設(shè)置的目的是用來(lái)生成固定的設(shè)備id,不展示document.body.appendChild(iframe)iframe.onload = function () {iframe.contentWindow.postMessage('loaded','*');}//監(jiān)聽(tīng)message事件,iframe 加載完成,獲取設(shè)備id ,進(jìn)行相關(guān)的數(shù)據(jù)采集helper.on(window,"message",function(event){that.deviceId = event.data.deviceIdif(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() // 獲取設(shè)備信息var data = {deviceId: _deviceId,type:event.data}event.source.postMessage(data, '*'); // 將設(shè)備信息發(fā)送給 SDK }//監(jiān)聽(tīng)message事件 if(window.addEventListener){window.addEventListener("message", receiveMessageFromIndex, false); }else{window.attachEvent("onmessage", receiveMessageFromIndex, false)如果你想知道可以看我的另一篇博客 web 瀏覽器指紋跨域共享
單頁(yè)面應(yīng)用:現(xiàn)在流行的單頁(yè)面應(yīng)用和普通 web 頁(yè)面的數(shù)據(jù)采集是否有差異
我們知道單頁(yè)面應(yīng)用都是無(wú)刷新的頁(yè)面加載,所以我們?cè)陧?yè)面跳轉(zhuǎn)的處理和我們的普通的頁(yè)面會(huì)有所不同。單頁(yè)面應(yīng)用的路由插件運(yùn)用了 window 自帶的無(wú)刷新修改用戶瀏覽記錄的方法,pushState 和 replaceState。window 的 history 對(duì)象 提供了兩個(gè)方法,能夠無(wú)刷新的修改用戶的瀏覽記錄,pushSate,和 replaceState,區(qū)別的 pushState 在用戶訪問(wèn)頁(yè)面后面添加一個(gè)訪問(wèn)記錄, replaceState 則是直接替換了當(dāng)前訪問(wèn)記錄,所以我們只要改寫 history 的方法,在方法執(zhí)行前執(zhí)行我們的采集方法就能實(shí)現(xiàn)對(duì)單頁(yè)面應(yīng)用的頁(yè)面跳轉(zhuǎn)事件的采集了
// 改寫思路:拷貝 window 默認(rèn)的 replaceState 函數(shù),重寫 history.replaceState 在方法里插入我們的采集行為,在重寫的 replaceState 方法最后調(diào)用,window 默認(rèn)的 replaceState 方法collect = {}collect.onPushStateCallback : function(){} // 自定義的采集方法(function(history){var replaceState = history.replaceState; // 存儲(chǔ)原生 replaceStatehistory.replaceState = function(state, param) { // 改寫 replaceStatevar url = arguments[2];if (typeof collect.onPushStateCallback == "function") {collect.onPushStateCallback({state: state, param: param, url: url}); //自定義的采集行為方法}return replaceState.apply(history, arguments); // 調(diào)用原生的 replaceState};})(window.history);這塊介紹起來(lái)也比較的復(fù)雜,如果你想了解更多,可以看我的另一篇博客你需要知道的單頁(yè)面路由實(shí)現(xiàn)原理
混合應(yīng)用:app 與 h5 的混合應(yīng)用我們要怎么進(jìn)行通訊
現(xiàn)在大部分的應(yīng)用都不是純?cè)膽?yīng)用, app 與 h5 的混合的應(yīng)用是現(xiàn)在的一種主流。純 web 數(shù)據(jù)采集我們考慮到前端存儲(chǔ)數(shù)據(jù)容易丟失,我們?cè)诿恳淮问录|發(fā)的時(shí)候都用采集接口傳輸采集到的數(shù)據(jù)。考慮到現(xiàn)在很多用戶的手機(jī)會(huì)有流量管家的軟件監(jiān)控,如果在 App 中 h5 還是采集到數(shù)據(jù)就傳輸給服務(wù)端,很有可能會(huì)讓流量管家檢測(cè)到,給用戶報(bào)警,從而使得用戶不再信任你的 App , 所以我們?cè)谟脩舨僮鞯臅r(shí)候?qū)?shù)據(jù)傳給 app 端,存儲(chǔ)到 app。用戶切換應(yīng)用到后臺(tái)的時(shí)候,通過(guò) app 端的 SDK 打包傳輸?shù)椒?wù)器,我們給 app 提供的方法封裝了一個(gè)適配器
// app 與 h5 混合應(yīng)用,直接將數(shù)信息發(fā)給 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)}實(shí)現(xiàn)思路
通過(guò)上面幾個(gè)問(wèn)題的思考,我們對(duì)埋點(diǎn)的實(shí)現(xiàn)大致已經(jīng)有了一些想法,我們使用思維導(dǎo)圖來(lái)還原下我們即將要做的事情,圖片記得放大看哦,太小了可能看不清。我們需要暴露給業(yè)務(wù)方調(diào)用的方法
我們需要處理的事件類型
SDK 的基本實(shí)現(xiàn)思路
我們來(lái)看下幾個(gè)核心代碼的實(shí)現(xiàn)
工具方法
我們定義了幾個(gè)工具方法,提高開(kāi)發(fā)的幸福指數(shù) ?
var helper = {};// 生成一個(gè)唯一的標(biāo)識(shí),pageSessionId (用這個(gè)變量來(lái)關(guān)聯(lián)開(kāi)始加載、加載完成、離開(kāi)頁(yè)面的事件,計(jì)算出頁(yè)面加菜時(shí)間,停留時(shí)間)helper.uuid = function(){}// 元素綁定事件監(jiān)聽(tīng),兼容瀏覽器到IE8helper.on = function(){}//元素移除事件監(jiān)聽(tīng)的適配器函數(shù),兼容瀏覽器到IE8helper.remove = function(){}//將json轉(zhuǎn)為字符串,事件傳輸?shù)膮?shù)類型轉(zhuǎn)化helper.changeJSON2Query = function(){}//將相對(duì)路徑解析成文檔全路徑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:{}};//獲取埋點(diǎn)配置collect.setParames = function(){}//更新訪問(wèn)路徑及頁(yè)面信息collect.updatePageInfo = function(){}//獲取事件參數(shù)collect.getParames = function(){}//獲取設(shè)備信息collect.getDevice = function(){}//事件采集collect.send = function(){}//設(shè)備采集collect.sendDevice = function(){}//判斷才否采集,埋點(diǎn)采集的開(kāi)關(guān)collect.isupload = function(){1. 判斷是否采集,不采集就注銷事件監(jiān)聽(tīng)(項(xiàng)目中區(qū)分游客身份和用戶身份的采集情況,這個(gè)方法會(huì)被判斷兩次)2. 采集則判斷是否已經(jīng)采集過(guò)a.已經(jīng)采集過(guò)不做任何操作b.沒(méi)有采集過(guò)添加事件監(jiān)聽(tīng)3. 判斷是 混合應(yīng)用還是純 web 應(yīng)用a.如果是web 應(yīng)用,調(diào)用 collect.setIframe 設(shè)置 iframeb.如果是混合應(yīng)用 將開(kāi)始加載和加載完成事件傳輸給 app}//點(diǎn)擊事件處理函數(shù)collect.clickHandler = function(){}//離開(kāi)頁(yè)面的事件處理函數(shù)collect.beforeUnloadHandler = function(){}//頁(yè)面回退事件處理函數(shù)collect.onPopStateHandler = function(){}//系統(tǒng)事件初始化,注冊(cè)離開(kāi)事件,瀏覽器后退事件collect.event = function(){}//獲取記錄開(kāi)始加載數(shù)據(jù)信息collect.getBeforeload = function(){}//存儲(chǔ)加載完成,獲取設(shè)備類型,記錄加載完成信息collect.onload = function(){1. 判斷cookie是否有存設(shè)備類型信息,有表示混合應(yīng)用2. 采集加載完成時(shí)間等信息3. 調(diào)用 collect.isupload 判斷是否進(jìn)行采集}//web 應(yīng)用,通過(guò)嵌入 iframe 進(jìn)行跨域 cookie 通訊,設(shè)置設(shè)備idcollect.setIframe = function(){}//app 與 h5 混合應(yīng)用,直接將數(shù)信息發(fā)給 app,判斷設(shè)備類型做原生方法適配器collect.saveEvent = function(){}//采集自定義事件類型collect.dispatch = function(){}//將參數(shù) userId 存入sessionStoragecollect.storeUserId = function(){}//采集H5信息,如果是混合應(yīng)用,將采集到的信息發(fā)送給 app 端collect.saveEventInfo = function(){}//頁(yè)面初始化調(diào)用方法collect.init = function(){1. 獲取開(kāi)始加載的采集信息2. 獲取 SDK 配置信息,設(shè)備信息3. 改寫 history 兩個(gè)方法,單頁(yè)面應(yīng)用頁(yè)面跳轉(zhuǎn)前調(diào)用我們自己的方法4. 頁(yè)面加載完成,調(diào)用 collect.onload 方法}collect.init(); // 初始化//暴露給業(yè)務(wù)方調(diào)用的方法return {dispatch:collect.dispatch,storeUserId:collect.storeUserId,}擴(kuò)展
?就是我這段時(shí)間研究的成果了,代碼的篇幅比較長(zhǎng),就不放在博客里了,感興趣的同學(xué)可以加我微信進(jìn)行交流,或則在文章下面留言,也歡迎大家給我提意見(jiàn),幫忙優(yōu)化 ?。web 瀏覽器指紋跨域共享
你需要知道的單頁(yè)面路由實(shí)現(xiàn)原理
數(shù)據(jù)埋點(diǎn)是什么?設(shè)置埋點(diǎn)的意義是什么?
數(shù)據(jù)采集與埋點(diǎn)
美團(tuán)點(diǎn)評(píng)前端無(wú)痕埋點(diǎn)實(shí)踐
如何清楚易懂的解釋“UV和PV"的定義
總結(jié)
以上是生活随笔為你收集整理的web 埋点实现原理了解一下的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 基于工程经验的『RESTful接口设计规
- 下一篇: LOJ6354 洛谷4366:[Cod