app嵌入的H5页面的数据埋点总结
好久没写博客了,大半年时间花费在了许多杂事上。
最近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,达到修复未登录用户的账号。
app嵌入的H5页面的数据埋点总结的更多相关文章
- 客户端相关知识学习(一)之混合开发,为什么要在App中使用H5页面以及应用场景、注意事项
混合开发 随着移动互联网的高速发展,常规的开发速度已经渐渐不能满足市场需求.原生H5混合开发应运而生,目前,市场上许多主流应用都有用到混合开发,例如支付宝.美团等.下面,结合我本人的开发经验,简单谈一 ...
- 利用浏览器调试APP中的H5页面
安卓手机的情况下,可以用chrome浏览器来调试. 打开地址: chrome://inspect/#devices 手机用USB数据线连接电脑,并启动USB调试模式. 只要在APP中打开H5页面,界面 ...
- APP分享视频H5页面
男左女右中国APP需要做一个APP分享视频H5页面,效果图见下面的图. 出现的问题: (1)URL参数为中文的时候乱码: (2)vedio点击默认是QQ,微信的播放器: (3)给视频添加一个默认的封面 ...
- 混合app开发,h5页面调用ios原生APP的接口
混合APP开发中,前端开发H5页面,不免会把兼容性拉进来,在做页面的兼容性同事,会与原生app产生一些数据交互: 混合APP开发,安卓的兼容性倒是好说,安卓使用是chrome浏览器核心,已经很好兼容H ...
- APP内的H5页面测试方法, 移动端的浏览器(例如UC浏览器)测试方法
前言: 用appium做UI自动化,测试APP里面的H5和测试手机浏览器打开的H5的操作流程上是有所区别的.比如要测试APP内嵌的H5需要先操作appium启动APP,然后通过context切到web ...
- ios下app内嵌h5页面是video适配问题
ios下做新闻详情用h5页面实现然后打包到app中,其中新闻详情页会有视频,安卓下video的poster可以做到适应video大小,但是ios下会按照poster图片大小将video等比撑大,但是视 ...
- APP端有原生态的控件,但嵌入了H5页面,怎么定位到H5页面的元素
appium 通常有很多种定位元素方法,例如xpath,driver.find_element_by_accessibility_id等,安卓sdk自带的uiautomatorviewer但是对于H5 ...
- 安卓app中嵌入一个H5页面,当手机系统设置字体变大时,如何使H5页面的字体不会随用户自己调整的系统字体变化而变化?
webview.getSettings().setTextZoom(100);WebView加上这个设置后,WebView里的字体就不会随系统字体大小设置发生变化了. https://segmentf ...
- 嵌套移动APP端的H5页面meta标签
<meta charset="utf-8"> <meta content="width=device-width, initial-scale=1.0, ...
随机推荐
- JAVA "GMT+10" 和 "GMT+0010"
可以使用 getAvailableIDs 方法来对所有受支持的时区 ID 进行迭代.可以选择受支持的 ID 来获得 TimeZone.如果想要的时区无法用受支持的 ID 之一表示,那么可以指定自定义时 ...
- Linux shell中处理
awk是行处理器: 相比较屏幕处理的优点,在处理庞大文件时不会出现内存溢出或是处理缓慢的问题,通常用来格式化文本信息 awk处理过程: 依次对每一行进行处理,然后输出 awk命令形式: awk ...
- 使用OPCNetAPI连接OPCServer
OPCServer KepServer; OPCGroup KepGroup; bool opc_connected; string remoteServerName = "KEPware. ...
- android studio 汉化
the modules below are not imported from Gradle anymore. Check those to be removed from the ide proje ...
- PlantUML windows android
dot执行程序. 渲染 url 连接(花费大量时间) 错误 和 语法注释 (是还在实验的) 缓存大小 5 在键入和渲染之间的延迟 (毫秒) 100
- BZOJ.2437.[NOI2011]兔兔与蛋蛋游戏(二分图博弈 匈牙利)
题目链接 首先空格的移动等价于棋子在黑白格交替移动(设起点移向白格就是黑色),且不会走到到起点距离为奇数的黑格.到起点距离为偶数的白格(删掉就行了),且不会重复走一个格子. (然后策略就同上题了,只不 ...
- C语言字符串操作详细总结
1)字符串操作 strcpy(p, p1) 复制字符串 strncpy(p, p1, n) 复制指定长度字符串 strcat(p, p1) 附加字符串 strncat(p, p1, n) 附加指定长度 ...
- java中线程安全的map是ConcurrentHashMap
原理:http://www.cnblogs.com/ITtangtang/p/3948786.html 与hashtable的区别: http://blog.csdn.net/songfeihu08 ...
- Java中的ReentrantLock和synchronized两种锁定
原文:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html 多线程和并发性并不是什么新内容,但是 Java 语言设计中的创新之 ...
- [Android Pro] https://blog.csdn.net/gaugamela/article/details/79143309
原文地址:https://blog.csdn.net/gaugamela/article/details/79143309 最近遇到这样一个问题: 第三方的SDK除了Jar包外,还提供了对应的so文件 ...