阅读 1494

从零开始写一个前端数据埋点工具(BuryingPoint)

背景

互联网发展到现在,数据的重要性已经不需要再多的强调,那如何做好数据搜集的工作则是每一家公司都要面临的问题。

数据搜集可以有不同的选择。有的公司选择使用第三方统计的SDK,比如友盟、神策等;有的公司选择自己在产品中注入统计代码,搭建查询系统,当然后者的代价会比较大,但优点就是更贴近公司的业务。

数据埋点技术

代码埋点

代码埋点就是在需要数据统计的地方植入数据上报的代码,统计用户行为。

优点:可以非常精确的选择什么时候发送数据。 缺点:维护代价较大,每一次更新都要对埋点代码进行维护,否则大概率搜集不到旧版本的数据。

可视化埋点

使用可视化交互手段代替写代码,把核心代码和配置、资源分开,每次打开都通过网络更新配置和资源。

优点:解决埋点代价大和维护代价大的问题。 缺点:覆盖的功能有限,不是所有的控件都可以通过这种方案定制。

无埋点

也就是全埋点的意思,无埋点尽可能收集所有控件的操作数据,然后再在系统里进行数据分析。

优点:对页面所有元素进行埋点,可以获取页面元素点击概率,并进一步分析。 缺点:数据传输和服务器压力相对较大。

构建思路与核心代码

依然是先从一个思维导图开始。

思维导图

自执行方法

确保埋点工具可以即插即用,只要加载完毕就可以自动上报部分数据。具体实现方式如下:

(function (win, doc) {
    var BP = {
        // 开放接口代码
    };
    
    win.BP = BP;
})(window, document);
复制代码

这样在js文件加载完毕时,就可以直接在全局使用BP来调用埋点工具的方法了。

埋点方式

使用代码埋点的方式来上报数据,在工具中定义:

    var BP = {
        send: function () {
            // 发送数据方法
        }
    };
复制代码

在页面的关键操作方法中通过BP.send()调用。

同时,考虑到服务端渲染的情况,页面可能直接由后端输出。后端开发者也可以直接在标签中添加属性bp-data,来实现用户有交互操作时进行数据上报。

    /**
     * 埋点,捕获带有bp-data属性的节点点击事件
     */
    var buryingPoint = function () {
        var attr = 'bp-data';
        var evtType = utils.mobile ? 'touchstart' : 'mousedown';
        utils.addEvent(doc, evtType, function (evt) {
            var target = evt.srcElement || evt.target;
            while (target && target.parentNode) {
                if (target.hasAttribute(attr)) {
                    BP.send();
                    break;
                }
                target = target.parentNode;
            }
        });
    };
复制代码

为了兼顾PC与移动端浏览器,将utils.addEvent设计为一个可以跨浏览器侦听事件的方法,具体实现方法如下:

    var utils = {
        /**
         * 跨浏览器事件侦听
         */
        addEvent: function () {
            if (doc.attachEvent) {
                return function (ele, type, func) {
                    ele.attachEvent('on' + type, func);
                };
            } else if (doc.addEventListener) {
                return function (ele, type, func) {
                    ele.addEventListener(type, func, false);
                };
            }
        }()
    }
复制代码

数据搜集

抛开业务来讲,通常需要统计的数据往往是uv和pv,有时需要统计页面停留的时长。基于这些基础需求,整理了如下需要搜集的数据:

  • 客户端信息

客户端信息对于前端开发来说属于相对比较头疼的问题,各种魔改UserAgent严重影响开发者们的情绪。相信各大公司对于UserAgent判断也有一个较为成熟的处理,作为个人开发来说推荐一个代码库ua-device,可以减少很多这方面的工作。唯一的不足,是引用这个库,会使打包出来的js文件体积增加150KB左右,个人认为在当前网络环境下这点无需顾虑。

    // 浏览器信息
    var CI = {
        size: function () {
            return scr.width + 'x' + scr.height;
        }(),
        // 网络类型
        network: function () {
            return (nav.connection && nav.connection.type) ? nav.connection.type : '-';
        }(),
        // 语言
        language: function () {
            return nav.language || '';
        }(),
        timezone: function () {
            return new Date().getTimezoneOffset() / 60 || '';
        }(),
        ua: function () {
            return encodeURIComponent(ua);
        }(),
        os: function () {
            var o = uaOutput.os;
            return encodeURIComponent(o.name + '_' + o.version.original);
        }(),
        browser: function () {
            var b = uaOutput.browser;
            return b.name + '_' + b.version.original;
        }(),
        engine: function () {
            var e = uaOutput.engine;
            return e.name + '_' + e.version.original;
        }()
    };
复制代码
  • 流量来源

  • 页面地址

  • 会话id

会话id用于计算uv,在工具初始化的时候生成一个uuid。由于会话id在打开页面后不会更新,所以使用类vue计算属性的方式来实现。

    var BP = {
        /**
         * 会话id,刷新页面会更新
         */
        sessionId: function () {
            return UUID.create();
        }()
    }
复制代码
  • 设备id

设备id用于串联用户的行为。比如用户浏览了若干个页面,上报了数条数据,就可以用设备id将这些行为串联起来。由于前端无法真正获取到所用设备的唯一标识,所以与会话id一样,采用不会重复的uuid。同样也是类vue的计算属性。

    BP = {
        /**
         * 设备id,读取cookie,不存在则种入cookie
         */
        deviceId: function () {
            var did = utils.getCookie(cookieName);
            if (!did) {
                did = UUID.create();
                utils.setCookie(cookieName, did, year);
            }
            return did;
        }()
    }
复制代码
  • 时间戳

  • 页面停留时长

记录页面停留时长成本最低的方法就是使用轮询上报数据,请求间隔可以根据业务需求来定。毕竟间隔越小,服务器承载的压力就会更大一点,但获取的数据就更准确。

    /**
     * Ticker钩子函数,用于上报页面停留时长
     * @param dt 间隔时间
     */
    var calStayTime = function (dt) {
        totalTime += dt;
        if(totalTime >= stayTime) {
            BP.send();
            totalTime -= stayTime;
        }
    };
    
    // 启动ticker
    ticker.start();
    ticker.register(calStayTime);
    
    // 页面离开时不再计时
    utils.addEvent(doc, 'visibilitychange', function () {
        if (doc.visibilityState === 'hidden') {
            ticker.stop();
        } else {
            ticker.start();
        }
    });
复制代码

为了能够上报尽可能准确的停留时间,当离开页面时(比如最小化或切换标签)应当停止计时。这里用一个独立的,简易版本的Ticker来维护时间线,更多关于维护时间线的问题可以看下面的链接。

使用TypeScript实现一个Ticker

数据存储与读取

采用cookie存储一些需要持久保存的数据,比如设备id。

    var utils = {
        /**
         * 设置cookie
         * @param name 名称
         * @param value 值
         * @param days 保存时间
         * @param domain 域
         */
        setCookie: function (name, value, days, domain) {
            if (value === null) {
                return;
            }
            if (domain === undefined || domain === null) {
                // 去除host中的端口部分
                domain = utils.stringSplice(win.location.host, '', ':', '');
            }
            if (days === undefined || days === null || days === '') {
                doc.cookie = name + '=' + value + ';domain=' + domain + ';path=/';
            } else {
                var now = new Date();
                var time = now.getTime() + DAY * days;
                now.setTime(time);
                doc.cookie = name + '=' + value + ';domain=' + domain + ';expires=' + now.toUTCString() + ';path/';
            }
        },
        /**
         * 读取cookie
         * @param name 名称
         */
        getCookie: function (name) {
            if (name === undefined || name === null) {
                return;
            }
            var reg = RegExp(name);
            if (reg.test(doc.cookie)) {
                return utils.stringSplice(doc.cookie, name, ';', '');
            }
        }
    }
复制代码

上报数据

埋点数据上报本质上可以看作是一种单向请求,即不需要关心服务器反馈,可以采用image标签的方式向服务器发送数据,同时还可以避免额外的跨域问题。

    var utils = {
        /**
         * 发送请求,使用image标签跨域
         * @param url 接口地址
         */
        sendRequest: function (url) {
            if (page.length === 0) {
                console.error('请配置有效的page参数', '@burying-point');
                return;
            }
            var img = new Image();
            img.src = url;
        }
    }
复制代码

扩展

  • 白名单

添加一个白名单过滤,规定只有白名单内的域名才可以发送请求。这只是一个小把戏,避免在开发过程中上报过多的脏数据,增加数据分析的工作量。

    var utils = {
        /**
         * 白名单校验
         */
        checkWhiteList: function () {
            if (whiteList.length === 0) {
                return true;
            }
            var href = win.location.href;
            var flag = false;
            for (var i = 0; i < whiteList.length; i++) {
                if (href.indexOf(whiteList[i]) > -1) {
                    flag = true;
                    break;
                }
            }
            return flag;
        }
    }
复制代码

使用方式

构建的埋点工具可以通过两种不同的方式进行数据上报。

第一种,通过代码直接上报:

    <script>
        BP.send();
    </script>
复制代码

第二种,在DOM标签中添加bp-data属性:

    <div bp-data>点击我会上报一条数据</div>
复制代码

总结

开发一个前端数据埋点工具,不需要特别复杂的技术,更多是基于业务的思考。这里把其中比较关键的部分列举出来,作为一个参考:

  • 自执行方法
  • 跨浏览器事件侦听
  • 维护时间线
  • cookie读写
  • 单向跨域请求
  • 类vue计算属性

完整代码与使用方法,请移步GitHub,感谢。

关注下面的标签,发现更多相似文章
评论