背景
互聯網發展到現在,數據的重要性已經不需要再多的強調,那如何做好數據搜集的工作則是每一家公司都要面臨的問題。尤其是像天貓、京東、寺庫這樣的電商公司,數據的統計可以提升用戶購買的用戶體驗,可以方便運營和產品調整銷售策略等等。可見頁面埋點多么重要。今天就讓我們從無到有制作一個埋點上報工具。
主要內容:
- 什么是埋點
- 埋點原理
- 埋點的種類
- 電商頁面前端埋點規范
- 封裝一個異步請求
- IntersectionObserver -新一代元素觀察接口
- 基於VUE從零開始封裝一個前端數據埋點工具
- 后端日志格式(前端理解即可)
- 后端nginx配置(前端理解即可)
- 需要改進的地方

什么是埋點
所謂“埋點”,是數據采集領域(尤其是用戶行為數據采集領域)的術語,指的是針對特定用戶行為或事件進行捕獲、處理和發送的相關技術及其實施過程。比如用戶某個icon點擊次數、觀看某個視頻的時長等等。
埋點原理分析和流程概述
簡單來說,網站統計分析工具需要收集到用戶瀏覽目標網站的行為(如打開某網頁、點擊某按鈕、將商品加入購物車等)及行為附加數據(如某下單行為產生的訂單金額等)。早期的網站統計往往只收集一種用戶行為:頁面的打開。而后用戶在頁面中的行為均無法收集。這種收集策略能滿足基本的流量分析、來源分析、內容分析及訪客屬性等常用分析視角,但是,隨着ajax技術的廣泛使用及電子商務網站對於電子商務目標的統計分析的需求越來越強烈,這種傳統的收集策略已經顯得力不能及。
后來,Google在其產品谷歌分析中創新性的引入了可定制的數據收集腳本,用戶通過谷歌分析定義好的可擴展接口,只需編寫少量的javascript代碼就可以實現自定義事件和自定義指標的跟蹤和分析。目前百度統計、搜狗分析等產品均照搬了谷歌分析的模式。
其實說起來兩種數據收集模式的基本原理和流程是一致的,只是后一種通過javascript收集到了更多的信息。下面看一下現在各種網站統計工具的數據收集基本原理。

首先,用戶的行為會---這里姑且先認為行為就是打開網頁。當網頁被打開,頁面中的埋點javascript片段會被執行,一般網站統計工具都會要求用戶在網頁中加入一小段javascript代碼,這個代碼片段一般會動態創建一個script標簽,並將src指向一個單獨的js文件,例子中為dot.js.此時這個單獨的js文件(圖中綠色節點)會被瀏覽器請求到並執行,這個js往往就是真正的數據收集腳本。數據收集完成后,js會請求一個后端的數據收集腳本(圖中的backend),這個腳本一般是一個偽裝成圖片的動態腳本程序,可能由php、python或其它服務端語言編寫,js會將收集到的數據通過http參數的方式傳遞給后端腳本,后端腳本解析參數並按固定格式記錄到訪問日志。
上面是一個數據收集的大概流程,下面以寺庫商城為例,對每一個階段進行一個相對詳細的分析。
1.1 埋點腳本執行階段
技術棧為vue,當頁面中的資源加載完成后,執行埋點的腳本,如下圖

1.2 數據收集腳本執行階段
數據收集腳本(dot.js)被請求后當頁面展示會被執行,這個腳本一般要做如下幾件事:
(1)通過瀏覽器內置javascript對象收集頁面基本信息,如頁面title(通過document.title)、url(頁面鏈接)、用戶顯示器分辨率(通過windows.screen)、cookie信息(通過document.cookie)等等一些信息。
(2)收集曝光樓層信息。
(3)將上面兩步收集的數據進行拼接。
(4)請求一個后端腳本,將信息放在http request參數中攜帶給后端腳本。
這里唯一的問題是步驟4,javascript請求后端腳本常用的方法是ajax,但是ajax是不能跨域請求的。這里dot.js在被統計網站的域內執行,而后端腳本在另外的域,ajax行不通。一種通用的方法是js腳本創建一個Image對象(log.gif),將Image對象的src屬性指向后端腳本並攜帶參數,此時即實現了跨域請求后端。這也是后端腳本為什么通常偽裝成gif文件的原因。通過http抓包可以看到dot.js對log.gif的請求:

1.3 后端腳本執行階段
log.gif是一個偽裝成gif的腳本。這種后端腳本一般要完成以下幾件事情:
(1)解析http請求參數的到信息。
(2)從服務器(WebServer)中獲取一些客戶端無法獲取的信息,如訪客ip等。
(3)將信息按格式寫入log。
(4)生成一副1×1的空gif圖片作為響應內容並將響應頭的Content-type設為image/gif。
(5)在響應頭中通過Set-cookie設置一些需要的cookie信息。
之所以要設置cookie是因為如果要跟蹤唯一訪客,通常做法是如果在請求時發現客戶端沒有指定的跟蹤cookie,則根據規則生成一個全局唯一的cookie並種植給用戶,否則Set-cookie中放置獲取到的跟蹤cookie以保持同一用戶cookie不變(見圖4)。

埋點的種類
業界的埋點方案主要分為以下三類:
代碼埋點
代碼埋點就是在需要數據統計的地方植入數據上報的代碼,統計用戶行為。
優點:可以非常精確的選擇什么時候發送數據。
缺點:維護代價較大,每一次更新都要對埋點代碼進行維護,否則大概率搜集不到舊版本的數據。
可視化埋點
利用可視化交互手段,數據產品/數據分析師可以通過可視化界面(管理后台連接設備) 配置事件,如下是騰訊移動分析的可視化埋點界面。可視化埋點仍需要先配置相關事件,再采集。


- 優點:埋點只需業務同學接入,無需開發支持;
- 缺點:僅支持客戶端行為。
無埋點
無埋點是指開發人員集成采集 SDK 后,SDK 便直接開始捕捉和監測用戶在應用里的所有行為,並全部上報,不需要開發人員添加額外代碼。
數據分析師/數據產品 通過管理后台的圈選功能來選出自己關注的用戶行為,並給出事件命名。之后就可以結合時間屬性、用戶屬性、事件進行分析了。所以無埋點並不是真的不用埋點了。
優點:
無需開發,業務人員埋點即可;
支持先上報數據,后進行埋點。
缺點:
數據量大;
僅僅支持客戶端。
無埋點和可視化埋點均不需要開發支持,僅數據業務同學進行設置即可。但兩者數據上報-埋點設置存在加大的差異:無埋點支持在數據上報之后再進行埋點設置,因而數據采集/上報的量遠大於可視化埋點。
因而無埋點的數據大都有清空機制,例如growingIO,允許版本發布后7天內設置埋點,超過7天數據清空,無法追溯。
這次主要講代碼埋點
代碼埋點分為 命令式埋點 與 聲明式埋點 :
命令式埋點,顧名思義,開發者需要手動在需要埋點的節點處進行埋點。如點擊按鈕或鏈接后的回調函數、頁面ready時進行請求的發送。大家肯定都很熟悉這樣的代碼:
// 頁面加載時發送埋點請求 $(document).ready(function(){ // ... 這里存在一些業務邏輯 sendRequest(params); }); // 按鈕點擊時發送埋點請求 $('button').click(function(){ // ... 這里存在一些業務邏輯 sendRequest(params); });
可以很容易發現,這樣的做法很有可能會將埋點代碼侵入業務代碼,這使整體業務代碼變得繁瑣,容易出錯,且后續代碼會愈加膨脹,難以維護。所以,我們需要讓埋點的代碼與具體的業務邏輯解耦,即 聲明式埋點 ,從而提高埋點的效率和代碼的可維護性。
聲明式埋點理論上,只需要關注兩個問題:
- 需要埋點的DOM節點;
- 所需攜帶的數據
因此,可以很快想出一個聲明式埋點的方法:
// key表示埋點的唯一標識;act表示埋點方式
<span v-clstag-dot = "{'act':'thumbs', 'key': 'details_product_1_dot_thumbs', 'productId': product.productId}">點贊</span>
稍后會詳細講聲明式埋點的實現原理
電商頁面前端埋點規范
建立一個好的規范非常重要,包括命名規范、上報規范、數據規范和使用規范*。
1.埋點命名規范
埋點名稱為上報日志中的key字段,第三條會講到,我們當前的做法是埋點名稱只能是由字母、數字、下划線組成,並保證在應用內唯一。
常用規則的舉例如下:
比如行為埋點:{頁面名稱}+{組件名稱}+{組件id}+{功能}+{動作}
組件名稱和動作最為重要,它決定着后端收到埋點后要進行哪種操作,開發過程中要和后端嚴格制定好名稱,比如點擊的是商品列表。我們約定好了商品為product,那么組件名稱就必須為product.比如點擊了收藏,和后端約定好的是thumbs,動作就必須為:thumbs。另外,id從0開始。
組件名稱列表:
廣告:ad
商品:product
購物車:car
其他:可和后端協商
動作列表:
點擊:click
收藏:collection
評論:comment
點贊; thumbs
加入購物車: add
其他:可和后端協商
示例:
// key表示埋點的唯一標識;act表示埋點方式
<span v-clstag-dot = "{'act':'thumbs', 'key': 'details_product_1_dot_thumbs', 'productId': product.productId}">點贊</span>
埋點啟動日志和錯誤上報日志:{頁面名稱}+{頁面id}+{動作}
示例:
'key': 'details_123_show'
'key':'details_123_error'
2.埋點上報規范
(1)針對曝光埋點數據的上報策略一般如下:
-
基於時間間隔:每隔 n秒(時間間隔可以根據公司的業務情況自定義)
-
基於數據條數:每累積 n條數據(條數可以自定義)
-
不間斷實時上報,如果是低頻率,數據量小,實時性要求高的數據可以不設限制
-
為以防用戶卸載 App或者關閉瀏覽器造成本地數據的丟失,會將未上報的埋點存儲在localstorage,瀏覽器關閉埋點數據並不會被刪除,如果用戶再次訪問,會啟動上報。基於Native提供的bridge,讓Native幫忙持久化數據,並在再次進入時,啟動上報。這里也可以創建一個單獨的串行隊列,來實現對本地持久化數據的逐個上報。
(2)事件埋點和錯誤埋點的上報策略
- 事件發生后及時上報
-
數據規范
每個公司都有自己的埋點數據規范,里面匯總了需要上報的埋點數據,例如
image.png

- 使用規范
(1)引入埋點腳本一定要在頁面資源加載完,例如:
import { dot } from './assets/js/dot' // 中央事件總線封裝 Vue.use(VueBus) Vue.config.productionTip = false /* eslint-disable no-new */ // Vue.directive() 這個方法寫在new Vue之前 dot.clickExpDot(Vue) window.onload = function () { dot.postError() dot.dotPageReadyData() dot.show() } new Vue({ el: '#app', router, store, // 使用store components: { App }, template: '<App/>' })
(2)聲明式埋點在html中引入的規范,例如:
#曝光埋點的用法 <div class="exposure-statistics" show-dot="{'act':'show',' key':'details_ad_1_flowtab_show'}">
#事件埋點的用法 <span v-clstag-dot = "{'act':'thumbs', 'key': 'details_product_1_dot_thumbs', 'productId': product.productId}">點贊</span>
封裝一個異步請求
1. axios(考慮到跨域問題,本次沒有使用)
在vue項目中,和后台交互獲取數據這塊,我們通常使用的是axios庫,它是基於promise的http庫,可運行在瀏覽器端和node.js中。他有很多優秀的特性,例如攔截請求和響應、取消請求、轉換json、客戶端防御cSRF等。所以我們的尤大大也是果斷放棄了對其官方庫vue-resource的維護,直接推薦我們使用axios庫。如果還對axios不了解的,可以移步axios文檔。
安裝
npm install axios; // 安裝axios復制代碼
引入
一般我會在項目的src目錄中,新建一個request文件夾,然后在里面新建一個http.js和一個api.js文件。http.js文件用來封裝我們的axios,api.js用來統一管理我們的接口。
代碼如下:
import axios from 'axios' // import QS from 'qs' import { Toast } from 'vant' // 環境的切換 if (process.env.NODE_ENV === 'development') { axios.defaults.baseURL = 'http://localhost:8080' } else if (process.env.NODE_ENV === 'production') { axios.defaults.baseURL = 'http://localhost:8080' } // 請求超時時間 axios.defaults.timeout = 10000 // post請求頭 axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8' // 請求攔截器 axios.interceptors.request.use(config => { // 請求處理 return config }, err => { // 處理請求錯誤 return Promise.reject(err) }) // 響應攔截器 axios.interceptors.response.use( response => { if (response.status === 200) { return Promise.resolve(response.data) } else { return Promise.reject(response) } }, // 服務器狀態碼不是200的情況 error => { if (error.response.status) { switch (error.response.status) { case 500: Toast({ message: '系統錯誤', duration: 1000, forbidClick: true }) break case 201: Toast({ message: '業務失敗!', duration: 1000, forbidClick: true }) break // 其他錯誤,直接拋出錯誤提示 default: Toast({ message: '失敗', duration: 1500, forbidClick: true }) } return Promise.reject(error.response) } } ) /** * get方法,對應get請求 * @param {String} url [請求的url地址] * @param {Object} params [請求時攜帶的參數] */ export function get (url, params) { return new Promise((resolve, reject) => { axios.get(url, { params: params }).then(res => { resolve(res) }).catch(err => { reject(err.data)