好久没写博客了,大半年时间花费在了许多杂事上。

最近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页面的数据埋点总结的更多相关文章

  1. 客户端相关知识学习(一)之混合开发,为什么要在App中使用H5页面以及应用场景、注意事项

    混合开发 随着移动互联网的高速发展,常规的开发速度已经渐渐不能满足市场需求.原生H5混合开发应运而生,目前,市场上许多主流应用都有用到混合开发,例如支付宝.美团等.下面,结合我本人的开发经验,简单谈一 ...

  2. 利用浏览器调试APP中的H5页面

    安卓手机的情况下,可以用chrome浏览器来调试. 打开地址: chrome://inspect/#devices 手机用USB数据线连接电脑,并启动USB调试模式. 只要在APP中打开H5页面,界面 ...

  3. APP分享视频H5页面

    男左女右中国APP需要做一个APP分享视频H5页面,效果图见下面的图. 出现的问题: (1)URL参数为中文的时候乱码: (2)vedio点击默认是QQ,微信的播放器: (3)给视频添加一个默认的封面 ...

  4. 混合app开发,h5页面调用ios原生APP的接口

    混合APP开发中,前端开发H5页面,不免会把兼容性拉进来,在做页面的兼容性同事,会与原生app产生一些数据交互: 混合APP开发,安卓的兼容性倒是好说,安卓使用是chrome浏览器核心,已经很好兼容H ...

  5. APP内的H5页面测试方法, 移动端的浏览器(例如UC浏览器)测试方法

    前言: 用appium做UI自动化,测试APP里面的H5和测试手机浏览器打开的H5的操作流程上是有所区别的.比如要测试APP内嵌的H5需要先操作appium启动APP,然后通过context切到web ...

  6. ios下app内嵌h5页面是video适配问题

    ios下做新闻详情用h5页面实现然后打包到app中,其中新闻详情页会有视频,安卓下video的poster可以做到适应video大小,但是ios下会按照poster图片大小将video等比撑大,但是视 ...

  7. APP端有原生态的控件,但嵌入了H5页面,怎么定位到H5页面的元素

    appium 通常有很多种定位元素方法,例如xpath,driver.find_element_by_accessibility_id等,安卓sdk自带的uiautomatorviewer但是对于H5 ...

  8. 安卓app中嵌入一个H5页面,当手机系统设置字体变大时,如何使H5页面的字体不会随用户自己调整的系统字体变化而变化?

    webview.getSettings().setTextZoom(100);WebView加上这个设置后,WebView里的字体就不会随系统字体大小设置发生变化了. https://segmentf ...

  9. 嵌套移动APP端的H5页面meta标签

    <meta charset="utf-8"> <meta content="width=device-width, initial-scale=1.0, ...

随机推荐

  1. JAVA "GMT+10" 和 "GMT+0010"

    可以使用 getAvailableIDs 方法来对所有受支持的时区 ID 进行迭代.可以选择受支持的 ID 来获得 TimeZone.如果想要的时区无法用受支持的 ID 之一表示,那么可以指定自定义时 ...

  2. Linux shell中处理

        awk是行处理器: 相比较屏幕处理的优点,在处理庞大文件时不会出现内存溢出或是处理缓慢的问题,通常用来格式化文本信息 awk处理过程: 依次对每一行进行处理,然后输出 awk命令形式: awk ...

  3. 使用OPCNetAPI连接OPCServer

    OPCServer KepServer; OPCGroup KepGroup; bool opc_connected; string remoteServerName = "KEPware. ...

  4. android studio 汉化

    the modules below are not imported from Gradle anymore. Check those to be removed from the ide proje ...

  5. PlantUML windows android

    dot执行程序. 渲染 url 连接(花费大量时间) 错误 和 语法注释 (是还在实验的) 缓存大小 5 在键入和渲染之间的延迟 (毫秒) 100

  6. BZOJ.2437.[NOI2011]兔兔与蛋蛋游戏(二分图博弈 匈牙利)

    题目链接 首先空格的移动等价于棋子在黑白格交替移动(设起点移向白格就是黑色),且不会走到到起点距离为奇数的黑格.到起点距离为偶数的白格(删掉就行了),且不会重复走一个格子. (然后策略就同上题了,只不 ...

  7. C语言字符串操作详细总结

    1)字符串操作 strcpy(p, p1) 复制字符串 strncpy(p, p1, n) 复制指定长度字符串 strcat(p, p1) 附加字符串 strncat(p, p1, n) 附加指定长度 ...

  8. java中线程安全的map是ConcurrentHashMap

    原理:http://www.cnblogs.com/ITtangtang/p/3948786.html 与hashtable的区别:  http://blog.csdn.net/songfeihu08 ...

  9. Java中的ReentrantLock和synchronized两种锁定

    原文:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html 多线程和并发性并不是什么新内容,但是 Java 语言设计中的创新之 ...

  10. [Android Pro] https://blog.csdn.net/gaugamela/article/details/79143309

    原文地址:https://blog.csdn.net/gaugamela/article/details/79143309 最近遇到这样一个问题: 第三方的SDK除了Jar包外,还提供了对应的so文件 ...