好久沒寫博客了,大半年時間花費在了許多雜事上。
最近1個月專門為H5頁面的app開發了一些埋點功能,主要是考慮到以后的可復制性和通用型,由於不是前端開發出身,相對來說還是比較簡陋的。
正題開始:H5頁面的埋點主要涉及到的元素有a標簽,button按鈕,以及form表單的提交。
目前實現的功能基本還都是代碼埋點的方式,但是相對來說比較簡潔了,我這里主要是針對a標簽和button的事件埋點。
第一個js腳本 bigdataIndex.js
var _qjmap = _qjmap || []; var _bigdataDomain = "http://localhost:8082/" _qjmap.push(['tenantCode', 'xxxxx000001']); (function () { //監控a標簽的單擊事件 var a = document.getElementsByTagName("a"); for(var i =0; i<a.length; i++){ a[i].onclick = (function(i){ return function(){ var data = this.getAttribute('data-bigdata'); var href = this.getAttribute('href'); if(data){ var prevEvent = sessionStorage.getItem("event") || ''; sessionStorage.setItem("prevEvent",prevEvent); if(data.indexOf(':') != -1){ var event = data.split(':')[0] || ''; var eventData = data.split(':')[1]|| ''; sessionStorage.setItem("event", event); sessionStorage.setItem("eventData", eventData); }else { sessionStorage.setItem("event", data); } } if(href){ sessionStorage.setItem('href', href); } send(); } })(i); } function send(){ var ma = document.createElement('script'); ma.type = 'text/javascript'; ma.async = true; ma.src = _bigdataDomain + "js/qjdata.js?v=1"; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ma, s); } //在按鈕事件中調用該方法 function btnEventSend(event,data){ sessionStorage.setItem("prevEvent",sessionStorage.getItem("prevEvent")); sessionStorage.setItem("event", event); sessionStorage.setItem("eventData", data); send(); } })(); //為button時候,手工觸發 function bigdataBtnEventSend(event, data){ sessionStorage.setItem("prevEvent",sessionStorage.getItem("prevEvent")); sessionStorage.setItem("event", event); sessionStorage.setItem("eventData", data); var ma = document.createElement('script'); ma.type = 'text/javascript'; ma.async = true; ma.src = _bigdataDomain + "js/qjdata.js?v=1"; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ma, s); }
說明:
_qjmap為全局變量:添加的tenantCode為租戶的id,如果統計多個應用,可以通過這個字段來區分。
_bigdataDomain為第一個js腳本下載第二個js腳本的域名地址。
接下來在一個閉包函數中監聽了a標簽的單擊事件, 如果觸發,則獲取a標簽data-bigdata屬性、href屬性,
並且從sessionStorage中獲取對應的事件,保存到sessionStorage中作為上個事件,以便下個事件獲取。
如果該事件帶有具體的數據,則必須使用冒號放到事件類型后面,方便后面的分拆保存。
例如:用戶點擊商品列表中的某個商品,跳轉到商品詳情頁面中。
則設置a標簽的屬性為 <a href="item/123456.htm" data-bigdata="viewGoods:123456">蘋果</a>
經過如上設置,在該頁面中嵌入上面的bigdataIndex.js,則event為viewGoods, eventData為123456(這里123456假設為商品的id)。
接下來最重要的就是send()方法:
function send(){ var ma = document.createElement('script'); ma.type = 'text/javascript'; ma.async = true; ma.src = _bigdataDomain + "js/qjdata.js?v=1"; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ma, s); }
該方法中動態創建一個腳本,並且從遠程服務器上下載qjdata.js文件加載到頁面中,此處使用的是異步加載的方式。
qjdata.js
(function(){ function getOsInfo() { // 獲取當前操作系統 var os; if (navigator.userAgent.indexOf('Android') > -1 || navigator.userAgent.indexOf('Linux') > -1) { os = 'Android'; } else if (navigator.userAgent.indexOf('iPhone') > -1) { os = 'IOS'; } else if (navigator.userAgent.indexOf('Windows Phone') > -1) { os = 'WP'; } else { os = 'none'; //未知 } return os; } function getOSVersion() { // 獲取操作系統版本 var OSVision = '1.0'; var u = navigator.userAgent; var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; //Android var isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios終端 if (isAndroid) { OSVision = navigator.userAgent.split(';')[1].match(/\d+\.\d+/g)[0]; } if (isIOS) { OSVision = navigator.userAgent.split(';')[1].match(/(\d+)_(\d+)_?(\d+)?/)[0]; } return OSVision; } function getDeviceType() { // 獲取設備類型 var deviceType; var sUserAgent = navigator.userAgent.toLowerCase(); var bIsIpad = sUserAgent.match(/(ipad)/i) == "ipad"; var bIsIphoneOs = sUserAgent.match(/iphone os/i) == "iphone os"; var bIsMidp = sUserAgent.match(/midp/i) == "midp"; var bIsUc7 = sUserAgent.match(/rv:1.2.3.4/i) == "rv:1.2.3.4"; var bIsUc = sUserAgent.match(/ucweb/i) == "ucweb"; var bIsAndroid = sUserAgent.match(/android/i) == "android"; var bIsCE = sUserAgent.match(/windows ce/i) == "windows ce"; var bIsWM = sUserAgent.match(/windows mobile/i) == "windows mobile"; if (!(bIsIpad || bIsIphoneOs || bIsMidp || bIsUc7 || bIsUc || bIsAndroid || bIsCE || bIsWM)) { deviceType = 'PC'; //pc } else if (bIsIphoneOs || bIsMidp || bIsUc7 || bIsUc || bIsAndroid || bIsCE || bIsWM) { deviceType = 'phone'; //phone } else if (bIsIpad) { deviceType = 'ipad'; //ipad } else { deviceType = 'none'; //未知 } return deviceType; } function getOrientationStatus() { // 獲取橫豎屏狀態 var orientationStatus; if (window.screen.orientation.angle == 180 || window.screen.orientation.angle == 0) { // 豎屏 orientationStatus = '豎屏'; } if (window.screen.orientation.angle == 90 || window.screen.orientation.angle == -90) { // 橫屏 orientationStatus = '橫屏'; } return orientationStatus; } function getNetWork() { // 獲取網絡狀態 var netWork; switch (navigator.connection.effectiveType) { case 'wifi': netWork = 'wifi'; // wifi break; case '5g': netWork = '5G'; // 5g break; case '4g': netWork = '4G'; // 4g break; case '2g': netWork = '2G'; // 2g break; case '3g': netWork = '3G'; // 3g break; case 'ethernet': netWork = 'ethernet'; // 有線 break; case 'default': netWork = 'none'; // 未知 break; } return netWork; } //生成唯一Id function generateUUID() { var d = new Date().getTime(); if (window.performance && typeof window.performance.now === "function") { d += performance.now(); //use high-precision timer if available } var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = (d + Math.random() * 16) % 16 | 0; d = Math.floor(d / 16); return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); return uuid; } //判斷用戶是否存在,不存在則生成唯一號 function getUUID() { var uuid = localStorage.getItem("bigdata_uuid"); if(uuid == '' || uuid == null){ uuid = generateUUID(); localStorage.setItem("bigdata_uuid", uuid); } return uuid; } //獲取cookie function getCookie(sName) { var aCookie = document.cookie.split("; "); var returnValue = ""; for (var i=0; i < aCookie.length; i++) { var key = sName + '='; if(aCookie[i].indexOf(key) != -1){ returnValue = unescape(aCookie[i].substr(sName.length + 1)); } } return returnValue; } function setCookie(name,value) { var Days = 30; var exp = new Date(); exp.setTime(exp.getTime() + Days*24*60*60*1000); document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString(); } function clearCookie(name){ setCookie(name,''); } //頁面離開時觸發 // window.onbeforeunload = function(){ // params.intime = localStorage.getItem("bigdata_intime"); // params.duration = getDuration(); // params.outtime = Date.now(); // } //記錄的參數值 var params = {}; params.osInfo = getOsInfo(); params.osVersion = getOSVersion(); params.deviceType = getDeviceType(); params.webType = getNetWork(); params.orientationStatus = getOrientationStatus(); params.deviceId = getUUID(); params.actionTime = Date.now(); var intime = sessionStorage.getItem("bigdata_intime"); params.previousUrl_intime = intime || '' ; params.duration = intime == null ? 0 : Date.now() - intime; sessionStorage.setItem("bigdata_intime", Date.now().toString()); params.event = sessionStorage.getItem("event"); params.preEvent = sessionStorage.getItem("prevEvent"); params.eventData = sessionStorage.getItem("eventData"); sessionStorage.removeItem("eventData"); sessionStorage.removeItem('href'); params.loginName = getCookie("_mall_newMobile_username"); params.userCode = getCookie("userId"); params.targetUrl = sessionStorage.getItem('href'); // params.phoneType = getPhoneTypeAndVersion().split("#")[0]; // params.phoneVersion = getPhoneTypeAndVersion().split("#")[1]; //document對象元素 if(document){ params.currUrl = document.URL || ''; //當前URL地址 params.prevUrl = document.referrer || ''; //上一路徑 params.loginIp = document.domain || ''; //獲取域名 params.title = document.title || ''; //標題 } //window對象元素 if(window && window.screen){ params.height = window.screen.height || 0; //獲取顯示屏信息 params.width = window.screen.width || 0; params.colorDepth = window.screen.colorDepth || 0; } //navigator對象數據 if(navigator){ params.lang = navigator.language || ''; //獲取語言的種類 if(navigator.geolocation) { params.longitude = sessionStorage.getItem('longitude'); params.latitude = sessionStorage.getItem('latitude'); } } //解析_qjmap配置 if(_qjmap){ for(var i in _qjmap){ console.log(_qjmap[i]); switch (_qjmap[i][0]){ case 'tenantCode': params.tenantCode = _qjmap[i][1]; break; default: break; } } } //拼接字符串 var args = ''; for(var i in params){ if(args != ''){ args += '\x01'; } var p = params[i]; if(p != null ){ p = p.toString().replace(new RegExp("=",'g'),"%3D"); } args += i + '=' + p; //將所有獲取到的信息進行拼接 } //通過偽裝成Image對象,請求后端腳本 var img = new Image(1, 1); var src = 'http://localhost:8082/bigdata/qjdata.gif?args=' + encodeURIComponent(args); // alert("請求到的后端腳本為" + src); img.src = src; })();
qjdata.js說明:
在該js中主要是拼接數據,並通過構造虛擬的image的方式,發送到后台。
params對象包含了很多用戶訪問的設備、網絡、以及自定義的信息。這里不一一介紹了。
部分數據需要提前存放到sessionStorage中來獲取,比如經緯度等。
最后將params對象轉化為字符串,通過image的參數方式傳遞到后台。
后台java代碼實現:
package com.king; import org.apache.commons.lang3.StringUtils; import org.jboss.logging.Logger; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import javax.imageio.ImageIO; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.OutputStream; @Controller @RequestMapping("/bigdata") public class BigdataController { private static final Logger log = Logger.getLogger(BigdataController.class); @RequestMapping(value = "qjdata.gif") public void dataCollection(String args, HttpServletResponse response){ if(StringUtils.isNotBlank(args)){ String[] arr = args.split("\001"); for(String kv :arr){ String[] kvmap = kv.split("="); if(kvmap.length > 1 && !kv.split("=")[1].equals("null")){ String key = kv.split("=")[0]; String value = kv.split("=")[1]; //針對登錄賬號解密 if(key.equals("loginName")){ try { value = value.replaceAll("%3D","="); value = value.substring(1,value.length()-1); value = ThreeDES.decryptThreeDESECB(value, ThreeDES.LoginDesKey); }catch (Exception e){ log.error(e); } } System.out.println(key + "==>" + value); }else{ System.out.println(kv.split("=")[0] + "==>" + ""); } } } System.out.println("========================================="); } }
說明: 由於用戶的賬號保存在sessionStorage中時候進行了加密,所以只能在這里進行解密操作。沒有加密的可以不需要這段。
最后輸出的信息,即為用戶的訪問行為信息,下面為最終的輸出信息供參考:
用戶登錄:
從登錄頁(login)到商品分類頁面(pageClass):
從商品分類頁(pageClass)到首頁(pageHome)
瀏覽商品詳情頁(viewGoods),這里的201807271813151即為商品的id號。
⚠️最后注意點:
關於用戶的唯一id問題,由於設備的Id號現在很多被屏蔽了,不好獲取。
在用戶未登錄的時候,通過js自動生成了一串UUID,然后保存到localStorage中,這個除非手工清除了緩存,否則會一直保存在本地。
當用戶登錄后,可以查看到用戶登錄的賬號,所以在后續數據清洗時,可以根據有賬號的uuid去匹配無賬號的uuid,達到修復未登錄用戶的賬號。