之前看到屈屈同学的博客上面有提到 Google Analytics 支持通过服务端发送统计信息 这里,自己就了解了下,了解后发现优点还是挺多的,所以自己就用 ThinkJS 单独搭建了一套这样的服务。

通过服务端统计的优点

  1. 屈屈也提到了,这个方案可以解决 Google 统计经常被墙的问题,因为这个方案是通过「用户<==>我自己的服务器<==>Google 统计的服务器」这样的方式来通信的,而不是通常的「用户<==>Google 统计的服务器」这样的不可控,我这里只需要保证我的服务器可以联通到 Google 的服务器就可以了。

  2. 也是我自己使用这个方案的首要原因,就是因为 Adblock 等一类插件,对 Google 统计的脚本进行屏蔽,所以统计信息不准确。

第一个问题还可以通过使用国内的统计服务来做,但是第二个问题对于通用的统计服务都会做屏蔽。

具体我是怎么做的

因为要收集屏幕分辨率、设备像素比等信息,而且为了在一些静态文件中使用,我还是使用前端收集信息的方式,将收集到的信息发送到独立部署的一个服务上(部署在 DigitalOcean 上面),这个服务将收集到的信息过滤,组装后发送到 Google 的服务器;

1. 用户端收集信息

我会优先使用 HTTP 协议本身返回的信息,如果 HTTP 协议本身没有带有信息的话就会收集上来带到 url 上面,下面就是通过一个简单的脚本来将设备像素比,屏幕分辨率等信息收集起来:

var i, url = '//stat.alphadn.com/view.gif';
var screen = win.screen, doc = win.document;
var param = {
    p: 'alphatr-blog',
    r: doc.referrer,
    s: screen.width + 'x' + screen.height,
    v: win.innerWidth + 'x' + win.innerHeight,
    t: doc.title,
    dpr: win.devicePixelRatio || win.webkitDevicePixelRatio || win.mozDevicePixelRatio || 1,
    _: +new Date()
};

var query = [];
for (i in param) {
    query.push(i + '=' + encodeURIComponent(param[i]));
}

setTimeout(function () {
    var img = new Image();
    img.src = url + '?' + query.join('&');
    $('#monitor').append(img);
}, 10);

其中参数 P 是项目名,然后将信息发送到 url //stat.alphadn.com/view.gif 上面;

2. 服务器支持 HTTPS

因为我有使用 HTTPS 的网站,也有使用 HTTP 的网站,为了支持这些,我让 stat.alphadn.com 这个域名支持 HTTP 和 HTTPS,在 Nginx 上配置也比较简单,不用写两份配置:

server {
    listen       80;
    listen       443 ssl http2;
    server_name  stat.alphadn.com;

    ssl_certificate     /develop/alphatr-stat/config/certificate/stat.alphadn.com.pem;
    ssl_certificate_key /develop/alphatr-stat/config/certificate/stat.alphadn.com.key;
}

像这样子,添加两个 listen 就可以。

ssl 证书使用的是Let's Encrypt 的服务,使用 acme-tiny 这个工具来做自动签发,crontab 每个月进行一次更新,具体的使用方式参考屈屈的这篇文章 Let's Encrypt,免费好用的 HTTPS 证书

3. 服务器收集信息

服务端使用 ThinkJS,配置了 view.gif 这个路径到 viewAction 这个方法下,

/**
 * index action
 * @return {Promise} []
 */
viewAction: function () {
    var action = 'pageview';

    var data = {
        dr: this.get('r'), // referrer;
        dt: this.get('t'), // 文档标题
        sr: this.get('s'), // 屏幕分辨率
        vp: this.get('v'), // 视图大小
        cd1: 'dpr ' + this.get('dpr') // 自定义维度 设备像素比
    }

    this.model('analytics').send(this.baseData(action, data));
    return this.gif();
}

其中 baseData 这个方式定义在 Base Controller 中,是从 HTTP 协议中解析一些通用的信息

baseData: function (action, data) {
    var project = this.get('p');
    if (!project) {
        return this.gif();
    }

    var tid = this.config('project')[project];

    if (!tid) {
        return this.gif();
    }

    if (think.isString(tid)) {
        tid = {tid: tid};
    }

    var base = {
        cid: this.clientId(),
        t: action,

        uip: this.userIp(),
        ua: this.userAgent(),
        ul: this.lang(),
        dl: this.referrer()
    }

    stat.log(project + '-' + action, think.extend({}, base, data));
    return think.extend({}, tid, base, data);
},

clientId 方法调用 uuid 这个 Node.js 模块生成用户唯一 ID,并保存在 Cookie 中,因为 ThinkJS 原生的 controller 方法 ip() 返回的是一个 IP 的列表,所以这里通过 userIp() 这个方法将 IP 列表中的局域网 IP 过滤掉。

var clientIp = function (ip) {
    var isSpecialIp = function (str) {
        var ipBitAnd = function (mask, ip) {
            mask = mask.trim().split('.');

            return ip.trim().split('.').map(function (num, index) {
                return parseInt(num, 10) & parseInt(mask[index], 10);
            }).join('.');
        };

        var ipArea = [
            {mask: '255.0.0.0', expect: '127.0.0.0'},
            {mask: '255.0.0.0', expect: '10.0.0.0'},
            {mask: '255.240.0.0', expect: '172.16.0.0'},
            {mask: '255.255.0.0', expect: '192.168.0.0'}
        ];

        return ipArea.some(function (item) {
            return ipBitAnd(item.mask, str) === item.expect;
        });
    };

    var clientIp = '';

    ip.split(',').forEach(function (item) {
        item = item.trim();
        if (!net.isIPv4(item)) {
            return;
        }

        clientIp = item;
        if (!isSpecialIp(item)) {
            clientIp = item;
            return;
        }
    });

    return clientIp;
}

获取的信息会在服务器保存一份日志,这里使用 winston 这个 node 模块进行日志管理及按日期切分,接着调用 model analytics 将信息发送到 google 统计服务器,然后通过 this.gif() 返回一个 gif 图片。

gif: function () {
    this.type('gif', false);
    this.end('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64');
}

4. 服务器发送信息

这里使用 request 进行发送,首先在这里会将一些信息不完整的请求过滤掉,将爬虫数据也过滤掉,检查一些必要的数据是否完整,添加默认的一些参数。然后就是发送请求,最后对一些错误的信息进行记录;

5. Google Analytics 端的一些配置

基本的数据 Google 都会自动识别的,只是我这里统计了设备像素比,Google Analytics 并没有这个分类,所以需要自己手动添加,在「管理->媒体资源->自定义定义->自定义维度->新建自定义维度」中添加,范围选择用户,看到右侧示例代码中是 "dimension1" 就可以了,如果之前添加了自定义维度,那么这里可能是 "dimension2" 或者其他,那就需要更改 Index Controller 中 cd1 这个 key 的名称了,改为对应了 cd2 或者其他。然后在自定义报告中根据需求添加自定义报告就行了。

这就是我近 30 天的设备像素比访问情况

设备像素比

存在的问题

  1. google 搜索的 referer 在高级浏览器只返回域名,所以对搜索统计不到
  2. 用户连接 digitalocean 的服务器网络不好,会出现丢失,针对这个问题,我下一步可能会使用阿里云的服务器做一次反向代理,通过「用户<==>阿里云国内服务器<==>DigitalOcean 美国服务器<==>Google 统计的服务器」这样的方式来保证联通率。

完整的源码

整个项目我也放在了 Github 上面,地址在这里:https://github.com/AlphaTr/alphatr-stat,大家有什么建议可以在这里,也可以在 github 开 issues,欢迎提交 Pull Requests;

参考链接

Google Analytics 官方文档