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,达到修复未登录用户的账号。

 

 



 

posted @ 2019-02-27 17:58  硅谷工具人  阅读(4840)  评论(0编辑  收藏  举报
成功之道,在于每个人生阶段都要有不同的目标,并且通过努力实现自己的目标,毕竟人生不过百年! 所有奋斗的意义在于为个人目标实现和提升家庭幸福,同时能推进社会进步和国家目标! 正如古人讲的正心诚意格物致知,修身齐家治国平天下。