很久没写博客了,大半年时间花费在了许多琐事上。javascript
最近1个月专门为H5页面的app开发了一些埋点功能,主要是考虑到之后的可复制性和通用型,因为不是前端开发出身,相对来讲仍是比较简陋的。前端
正题开始:H5页面的埋点主要涉及到的元素有a标签,button按钮,以及form表单的提交。java
目前实现的功能基本还都是代码埋点的方式,可是相对来讲比较简洁了,我这里主要是针对a标签和button的事件埋点。android
第一个js脚本 bigdataIndex.jsios
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); }
说明:git
_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文件加载到页面中,此处使用的是异步加载的方式。web
qjdata.jsspring
(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说明:apache
在该js中主要是拼接数据,并经过构造虚拟的image的方式,发送到后台。windows
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,达到修复未登陆用户的帐号。