文章目錄
一、前言
所謂的埋點就是在應用中特定的流程收集一些信息,用來跟蹤應用使用的狀況,後續用來進一步優化產品或是提供運營的數據支撐,包括訪問數(Visits),訪客數(Visitor),停留時長(Time On Site),頁面瀏覽數(Page Views)和跳出率(Bounce Rate)。這樣的信息收集可以大致分爲兩種:頁面統計(track this virtual page view),統計操作行爲(track this button by an event),在爲一些網頁和App接下來進行流量分析以及描繪用戶畫像提供支持,是進行數據分析和挖掘的“第一步”。
二、“埋點”知多少
數據埋點作爲數據採集的一種重要方式,主要用來記錄和收集終端用戶的操作行爲,其基本原理是在App/H5/PC等終端部署採集的SDK代碼,當用戶的行爲滿足某種條件的時候,比如進入某個頁面、點擊某個按鈕等,會自動觸發記錄和存儲,然後這些數據會被收集並被傳輸到終端提供商,或者是通過後端採集用戶使用服務過程中的請求數據。
一個典型的埋點採集處理流程如下圖所示:
三、“埋點”有何用
終端提供商在收集到埋點數據之後,通過大數據處理、數據統計、數據分析、數據挖掘等加工處理,可以得到衡量產品狀態的一些基本指標,比如活躍、留存、新增等大盤數據,從而洞察產品的狀態。此外更重要的是隨着數據挖掘等技術的興起,埋點採集到的數據在以下方面的作用也越來越凸顯:
驅動決策:ABtest、漏斗優化、用戶增長、bug修復、精準營銷、流失用戶預警
驅動產品智能:智能推薦(千人千面)、場景化提示(私人助理)等
驅動安全:風險識別
四、幾種埋點“姿勢”
爲了將海量數據採集得更加精準,爲後續營造“純淨”的數據分析環境,埋點技術應運而生。數據基礎夯實與否,取決於數據的採集方式。
埋點方式多種多樣,按照埋點位置不同,可以分爲前端(客戶端)埋點與後端(服務器端)埋點,其中前端埋點包括:代碼埋點、全埋點、可視化埋點。
4.1 前端埋點
前端埋點是在用戶端(APP、Web、客戶端)等嵌入數據採集代碼,比如友盟等均採用的是前端埋點,比如通過嵌入一段代碼就就可以對網頁數據的訪問數據進行採集。相比於後端埋點,前端埋點能方便收集到用戶在界面上的行爲數據,比如用戶點了哪個按鈕、頁面之間的跳轉次序、停留時長等,這些數據是後面進行數據分析的主要來源。
前端埋點技術有以下三類:
4.1.1 代碼埋點
代碼埋點是直接將採集SDK集成在終端,然後不斷在此基礎上添加調整採集方案,是目前主流的埋點採集方案,其優缺點如下:
優點:
高度定製、控制精準、採集的數據豐富準確
缺點:
首先是每當有采集需求,需要開發人員不斷添加採集代碼,工作量大;
其次變更採集策略,需要發佈新版本,代價巨大,存在滯後效應;
最後由於採集代碼常駐終端,不斷將採集的用戶行爲數據進行記錄和上報,對於終端尤其是移動終端來說還有耗電、消耗數據流量等負載,此外在數據上報傳輸的過程中也存在丟失數據的風險。
4.1.2 可視化埋點
由於代碼埋點需要終端開發人員來執行採集方案,對業務的功能開發侵入性較高。有的公司開發出了可視化埋點技術,只需要產品與運營人員通過GUI界面進行鼠標簡單點擊,就可以隨時增加、取消、調整採集數據的位置和方式,此種埋點方式避開了終端開發人員的介入,由需求人員直接執行採集,減輕了需求傳遞過程中的信息損耗和誤解,另外可視化埋點技術往往由服務端直接下發採集的配置文件,而不用跟隨版本發佈,從而加快了數據採集的流程。
具體實現方式參考:
具體實現是SDK定時做界面截圖,在截圖的同時從界面UI的根對象開始遍歷所有的可視化子對象,得到其層級關係。根據截圖和UI元素的可視化信息重新渲染頁面,識別可埋點的控件。當產品人員在後臺管理端的截屏畫面上點擊可埋點控件,設置事件關聯方面的配置,服務器保存這些配置,客戶端在獲取到這些配置信息以後,按照新配置採集數據。
4.1.3 無埋點
無埋點與可視化埋點原理基本一致,區別在於無埋點是先遍歷所有的控件和操作行爲的組合情況,然後將這些組合情況交給埋點後臺,由數據分析人員選擇對哪些組合的埋點數據進行分析,其優缺點如下:
優點:
收集數據全面,無漏報
缺點:
採集數據量巨大,增加了終端流量消耗和服務器存儲負擔。
埋點的上報時機相對呆板,不能靈活的根據特定的場景進行特殊設置
前端埋點的注意事項:
頁面和控件標示上報要從頂層進行合理的設計,層次感要明顯
埋點數據的漏報和重複上報如何衡量
前端埋點不僅可以處理不需要和服務器交互的曝光和點擊事件,也可以將與服務器交互的結果,比如關注成功、分享成功、優惠券領取成功等原屬於後端埋點裏的事件放在前端來上報。
4.2 後端埋點
後端埋點爲了避免前端埋點的以下問題:
前端埋點需要對採集的數據壓縮、暫存,爲減少移動端的數據流量,除一些需要實時上報的重要事件不限制網絡環境,其它事件一般只在wifi情況下上報,因此數據會有延遲,丟數據等弊端,而在後端採集數據,由於數據是在內網傳輸,數據傳輸的即時性強,丟失數據的風險小。
前端埋點採集程序由於需要常駐,監測實時和延遲埋點上報,不可避免的帶來額外的耗電。
前端埋點若要新增或調整採集方案,需要開發人員修改客戶端代碼,然後發版之後才能解決,受發佈週期的影響較大,而且通常用戶的版本更新並不會及時,這將導致新方案不能及時覆蓋所有用戶。雖然現在部分埋點管理後臺也支持熱配置更新,但功能一般都很弱,只支持一些基礎的埋點事件熱更新部署,
注意:
很多時候並不把後端埋點獨立出來,而是混合在前端埋點中,等用戶和服務器端的交互返回結果之後,將結果進行上報。
對一下需要精確採集的數據,比如代金券發放等,實施的時候儘量採用後端埋點,除非後端無法採集到所需要的數據,前端埋點只是用來參考。此外也可以將業務數據庫代金券領取數據同步到數據倉庫中進行分析。
4.3 其它埋點
路徑埋點和獨立埋點:
這部分的埋點根據業務對路徑的追蹤需求和SDK的開發能力,可爲每個事件設計上下文的路徑信息,路徑信息的組成一般由頁面、控件、行爲三部分組成,而路徑的深度也不宜太深,一般小於五層。
顯性埋點和隱性埋點:
顯性和隱性是從用戶有感和無感來區分的,有感事件是用戶的主動事件,比如展示和點擊事件;無感事件主要用來處理後臺的數據請求和拉取,用以監控和服務器的數據交互是否正常等,無感事件中常用的是掃描採集,比如app啓動之後,掃描各設置開關的狀態信息進行上報等
業務埋點和監測埋點:
業務埋點是從業務需求的角度而言,比如產品需要統計某個頁面的曝光和點擊,算法人員需要的推薦項點擊率等;而監測埋點是從業務的流程上來講的,一般是指隱性的(比如服務器交互的內容拉取情況、本地潛在信息的生成情況等),此外業務埋點中的關鍵部分也可以用作監測埋點。
五、最理想的埋點方式?
回到一開始的問題:何種埋點方式最理想呢?
正如同硬幣有兩面,任何單一的埋點方式都存在優點與缺點,企圖通過簡單粗暴的幾行代碼/一次部署、甚至犧牲用戶體驗的埋點方式,都不是企業所期望的。要滿足精細化、精準化的數據分析需求,可根據實際需要的分析場景,選擇一種或多種組合的採集方式,畢竟採集全量數據不是目的,實現有效的數據分析,從數據中找到關鍵決策信息實現增長才是重中之重。
因此,數據採集只是數據分析的第一步,數據分析的目的是洞察用戶行爲,挖掘用戶價值,進而促進業務增長,故最理想的埋點方案是根據根據不同的業務和場景以及行業特性和自身實際需求,將埋點通過優劣互補方式進行組合,比如:
1、代碼埋點+全埋點:在需要對落地頁進行整體點擊分析時,細節位置逐一埋點的工作量相對較大,且在頻繁優化調整落地頁時,更新埋點的工作量更加不容小覷,但複雜的頁面存在着全埋點不能採集的死角,因此,可將代碼埋點作爲輔助,將用戶核心行爲進行採集,從而實現精準的可交叉的用戶行爲分析;
2、代碼埋點+服務端埋點:以電商平臺爲例, 用戶在支付環節,由於中途會跳轉到第三方支付平臺,是否支付成功需要通過服務器中的交易數據來驗證,此時可通過代碼埋點和服務端埋點相結合的方式,提升數據的準確性;
3、代碼埋點+可視化埋點:因代碼埋點的工作量大,可通過核心事件代碼埋點,可視化埋點用於追加和補充的方式採集數據。
六、流量分析系統中日誌埋點
6.1 選擇客戶端埋點
客戶端埋點:支持 iOS、安卓、Web/H5、微信小程序,主要用於分析 UV、PV、點擊量等基本指標。例:下圖是Web端的埋點技術圖:
6.2 服務器規劃
七、日誌埋點的實現
7.1 客戶端埋點
在網頁流量分析系統中,採用客戶端網頁埋點實現,在其中需要埋點的頁面中的<head></head>
中加入如下代碼:
<script src="tj.js"></script>
注:tj.js 就是需埋點的 js 文件
/**函數可對字符串進行編碼,這樣就可以在所有的計算機上讀取該字符串。*/
function ar_encode(str)
{
//進行URL編碼
return encodeURI(str);
}
/**屏幕分辨率*/
function ar_get_screen()
{
var c = "";
if (self.screen) {
c = screen.width+"x"+screen.height;
}
return c;
}
/**顏色質量*/
function ar_get_color()
{
var c = "";
if (self.screen) {
c = screen.colorDepth+"-bit";
}
return c;
}
/**返回當前的瀏覽器語言*/
function ar_get_language()
{
var l = "";
var n = navigator;
if (n.language) {
l = n.language.toLowerCase();
}
else
if (n.browserLanguage) {
l = n.browserLanguage.toLowerCase();
}
return l;
}
/**返回瀏覽器類型IE,Firefox*/
function ar_get_agent()
{
var a = "";
var n = navigator;
if (n.userAgent) {
a = n.userAgent;
}
return a;
}
/**方法可返回一個布爾值,該值指示瀏覽器是否支持並啓用了Java*/
function ar_get_jvm_enabled()
{
var j = "";
var n = navigator;
j = n.javaEnabled() ? 1 : 0;
return j;
}
/**返回瀏覽器是否支持(啓用)cookie */
function ar_get_cookie_enabled()
{
var c = "";
var n = navigator;
c = n.cookieEnabled ? 1 : 0;
return c;
}
/**檢測瀏覽器是否支持Flash或有Flash插件*/
function ar_get_flash_ver()
{
var f="",n=navigator;
if (n.plugins && n.plugins.length) {
for (var ii=0;ii<n.plugins.length;ii++) {
if (n.plugins[ii].name.indexOf('Shockwave Flash')!=-1) {
f=n.plugins[ii].description.split('Shockwave Flash ')[1];
break;
}
}
}
else
if (window.ActiveXObject) {
for (var ii=10;ii>=2;ii--) {
try {
var fl=eval("new ActiveXObject('ShockwaveFlash.ShockwaveFlash."+ii+"');");
if (fl) {
f=ii + '.0';
break;
}
}
catch(e) {}
}
}
return f;
}
/**匹配頂級域名*/
function ar_c_ctry_top_domain(str)
{
var pattern = "/^aero$|^cat$|^coop$|^int$|^museum$|^pro$|^travel$|^xxx$|^com$|^net$|^gov$|^org$|^mil$|^edu$|^biz$|^info$|^name$|^ac$|^mil$|^co$|^ed$|^gv$|^nt$|^bj$|^hz$|^sh$|^tj$|^cq$|^he$|^nm$|^ln$|^jl$|^hl$|^js$|^zj$|^ah$|^hb$|^hn$|^gd$|^gx$|^hi$|^sc$|^gz$|^yn$|^xz$|^sn$|^gs$|^qh$|^nx$|^xj$|^tw$|^hk$|^mo$|^fj$|^ha$|^jx$|^sd$|^sx$/i";
if(str.match(pattern)){ return 1; }
return 0;
}
/**處理域名地址*/
function ar_get_domain(host)
{
//如果存在則截去域名開頭的 "www."
var d=host.replace(/^www\./, "");
//剩餘部分按照"."進行split操作,獲取長度
var ss=d.split(".");
var l=ss.length;
//如果長度爲3,則爲xxx.yyy.zz格式
if(l == 3){
//如果yyy爲頂級域名,zz爲次級域名,保留所有
if(ar_c_ctry_top_domain(ss[1]) && ar_c_ctry_domain(ss[2])){
}
//否則只保留後兩節
else{
d = ss[1]+"."+ss[2];
}
}
//如果長度大於3
else if(l >= 3){
//如果host本身是個ip地址,則直接返回該ip地址爲完整域名
var ip_pat = "^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$";
if(host.match(ip_pat)){
return d;
}
//如果host後兩節爲頂級域名及次級域名,則保留後三節
if(ar_c_ctry_top_domain(ss[l-2]) && ar_c_ctry_domain(ss[l-1])) {
d = ss[l-3]+"."+ss[l-2]+"."+ss[l-1];
}
//否則保留後兩節
else{
d = ss[l-2]+"."+ss[l-1];
}
}
return d;
}
/**返回cookie信息*/
function ar_get_cookie(name)
{
//獲取所有cookie信息
var co=document.cookie;
//如果名字是個空 返回所有cookie信息
if (name == "") {
return co;
}
//名字不爲空 則在所有的cookie中查找這個名字的cookie
var mn=name+"=";
var b,e;
b=co.indexOf(mn);
//沒有找到這個名字的cookie 則返回空
if (b < 0) {
return "";
}
//找到了這個名字的cookie 獲取cookie的值返回
e=co.indexOf(";", b+name.length);
if (e < 0) {
return co.substring(b+name.length + 1);
}
else {
return co.substring(b+name.length + 1, e);
}
}
/**
設置cookie信息
操作符:
0 表示不設置超時時間 cookie是一個會話級別的cookie cookie信息保存在瀏覽器內存當中 瀏覽器關閉時cookie消失
1 表示設置超時時間爲10年以後 cookie會一直保存在瀏覽器的臨時文件夾裏 直到超時時間到來 或用戶手動清空cookie爲止
2 表示設置超時時間爲1個小時以後 cookie會一直保存在瀏覽器的臨時文件夾裏 直到超時時間到來 或用戶手動清空cookie爲止
* */
function ar_set_cookie(name, val, cotp)
{
var date=new Date;
var year=date.getFullYear();
var hour=date.getHours();
var cookie="";
if (cotp == 0) {
cookie=name+"="+val+";";
}
else if (cotp == 1) {
year=year+10;
date.setYear(year);
cookie=name+"="+val+";expires="+date.toGMTString()+";";
}
else if (cotp == 2) {
hour=hour+1;
date.setHours(hour);
cookie=name+"="+val+";expires="+date.toGMTString()+";";
}
var d=ar_get_domain(document.domain);
if(d != ""){
cookie +="domain="+d+";";
}
cookie +="path="+"/;";
document.cookie=cookie;
}
/**返回客戶端時間*/
function ar_get_stm()
{
return new Date().getTime();
}
/**返回指定個數的隨機數字串*/
function ar_get_random(n) {
var str = "";
for (var i = 0; i < n; i ++) {
str += String(parseInt(Math.random() * 10));
}
return str;
}
/* main function */
function ar_main() {
//收集完日誌 提交到的路徑
var dest_path = "http://127.0.0.1:8081/log?";
var expire_time = 30 * 60 * 1000;//會話超時時長
//處理uv
//--獲取cookie ar_stat_uv的值
var uv_str = ar_get_cookie("ar_stat_uv");
var uv_id = "";
//--如果cookie ar_stat_uv的值爲空
if (uv_str == ""){
//--爲這個新uv配置id,爲一個長度20的隨機數字
uv_id = ar_get_random(20);
//--設置cookie ar_stat_uv 保存時間爲10年
ar_set_cookie("ar_stat_uv", uv_id, 1);
}
//--如果cookie ar_stat_uv的值不爲空
else{
//--獲取uv_id
uv_id = uv_str;
}
//處理ss
//--獲取cookie ar_stat_ss
var ss_stat = ar_get_cookie("ar_stat_ss");
var ss_id = ""; //sessin id
var ss_count = 0; //session有效期內訪問頁面的次數
var ss_time = "";
//--如果cookie中不存在ar_stat_ss 說明是一次新的會話
if (ss_stat == ""){
//--隨機生成長度爲10的session id
ss_id = ar_get_random(10);
//--session有效期內頁面訪問次數爲0
ss_count = 0;
//--當前事件
ss_time = ar_get_stm()
} else { //--如果cookie中存在ar_stat_ss
//獲取ss相關信息
var items = ss_stat.split("_");
//--ss_id
ss_id = items[0];
//--ss_count
ss_count = parseInt(items[1]);
//--ss_stm
ss_time = items[2];
//如果當前時間-當前會話上一次訪問頁面的時間>30分鐘,雖然cookie還存在,但是其實已經超時了!仍然需要重新生成cookie
if (ar_get_stm() - ss_time > expire_time) {
//--重新生成會話id
ss_id = ar_get_random(10);
//--設置會話中的頁面訪問次數爲0
ss_count = 0;
//--當前事件
ss_time = ar_get_stm();
}else{//--如果會話沒有超時
//--會話id不變
//--設置會話中的頁面方位次數+1
ss_count = ss_count + 1;
ss_time = ar_get_stm();
}
}
//--重新拼接cookie ar_stat_ss的值
value = ss_id+"_"+ss_count+"_"+ss_time;
ar_set_cookie("ar_stat_ss", value, 0);
//當前地址
var url = document.URL;
url = ar_encode(String(url));
//當前資源名
var urlname = document.URL.substring(document.URL.lastIndexOf("/")+1);
urlname = ar_encode(String(urlname));
//返回導航到當前網頁的超鏈接所在網頁的URL
var ref = document.referrer;
ref = ar_encode(String(ref));
//網頁標題
var title = document.title;
title = ar_encode(String(title));
//網頁字符集
var charset = document.charset;
charset = ar_encode(String(charset));
//屏幕信息
var screen = ar_get_screen();
screen = ar_encode(String(screen));
//顏色信息
var color =ar_get_color();
color =ar_encode(String(color));
//語言信息
var language = ar_get_language();
language = ar_encode(String(language));
//瀏覽器類型
var agent =ar_get_agent();
agent =ar_encode(String(agent));
//瀏覽器是否支持並啓用了java
var jvm_enabled =ar_get_jvm_enabled();
jvm_enabled =ar_encode(String(jvm_enabled));
//瀏覽器是否支持並啓用了cookie
var cookie_enabled =ar_get_cookie_enabled();
cookie_enabled =ar_encode(String(cookie_enabled));
//瀏覽器flash版本
var flash_ver = ar_get_flash_ver();
flash_ver = ar_encode(String(flash_ver));
//當前ss狀態 格式爲"會話id_會話次數_當前時間"
var stat_ss = ss_id+"_"+ss_count+"_"+ss_time;
//拼接訪問地址 增加如上信息
dest=dest_path+"url="+url+"&urlname="+urlname+"&title="+title+"&chset="+charset+"&scr="+screen+"&col="+color+"&lg="+language+"&je="+jvm_enabled+"&ce="+cookie_enabled+"&fv="+flash_ver+"&cnv="+String(Math.random())+"&ref="+ref+"&uagent="+agent+"&stat_uv="+uv_id+"&stat_ss="+stat_ss;
//通過插入圖片訪問該地址
document.getElementsByTagName("body")[0].innerHTML += "<img src=\""+dest+"\" border=\"0\" width=\"1\" height=\"1\" />";
}
window.onload = function(){
//觸發main方法
ar_main();
}
說明:
①var dest_path = "http://127.0.0.1:8081/log?"
, 此處要改成日誌服務器的地址,並且這個地址是能夠被訪問的,最後一個 "?"不要忘加,用來拼後續參數使用的。
②埋點的原理:js代碼會動態在頁面中創建一個寬和高都是1px的圖片,圖片的地址指向了1中定義的日誌服務器中的圖片,
document.getElementsByTagName("body")[0].innerHTML += "<img src=\""+dest+"\" border=\"0\" width=\"1\" height=\"1\" />";
7.2 服務器端開發(關鍵代碼)
package com.logs.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
@Controller
public class LogController {
private Logger logger = LoggerFactory.getLogger(LogController.class);
@RequestMapping("/log")
public void log(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
//1.獲取請求參數
String qs = request.getQueryString();
//2.對URL解碼
String decode = URLDecoder.decode(qs, "utf-8");
//3.轉換成需要處理的格式
StringBuilder sb = new StringBuilder();
String[] attrs = decode.split("&");
for (String attr : attrs) {
String[] kv = attr.split("=");
String val = kv.length >= 2 ? kv[1] : "";
sb.append(val+"|");
}
sb.append(request.getRemoteAddr());
String logStr = sb.toString();
// System.out.println(logStr);
logger.info(logStr);
}
}