南琴浪博客

Google_Analytics 异步化方案 via Openresty

06/09/2018

网站配置 Google_Analytics 的常见方式,是在网站前端引用 Google_Analytics 的 tracker_code 和 analytics.js 用以统计用户行为。而这种方式会成为访问缓慢的原因:访客对 analytics.js 的下载请求会拖慢页面加载完毕,并需要向 Google 发送访客数据,用户行为的统计提交全部在客户端由客户完成,加重了客户端的负担。并且也有 analytics.js 会被 ublock 等屏蔽掉而导致无法收集用户行为的弊端。

如果将 Google_Analytics 的统计工作从前端转移到后端完成,就可以有效避免上述问题。

在 2016 年时就已经出现了这种方式(via),该文章使用 ngx_http_userid_module 实现 userid 的生成。而基于本文所述,使用 Openresty 的好处在于 id 的生成方案由网站所有者决定,且能够享受 lua code 带来的便(kuai)利(gan)。

基本思路

通过 Nginx proxy_pass 就能实现该目的,根据用户访问的 location,按照 Mea­sure­ment Pro­to­col 向 https://www.google-analytics.com/ 提交数据,就能将统计工作放在后端完成:

    location @google_analytics {
        proxy_pass  https://www.google-analytics.com/collect?v=1&t=pageview&tid=UA-111111111-1&cid=$cookie_cid&uip=$remote_addr&dh=$host&dp=$uri&dr=$http_referer&z=$msec;
    }

配置 Nginx

在上面已提出了基本思路,完整的 Nginx 实现应该是这样的框架:

    location / {
        ...

        post_action @google_analytics;
    }

    location @google_analytics {
        internal;
        resolver                    8.8.8.8 [2001:4860:4860::8888];
        proxy_http_version          1.1;
        proxy_method                GET;
        proxy_set_header            User-Agent $http_user_agent;
        proxy_pass_request_headers  off;
        proxy_pass_request_body     off;
        proxy_pass                  https://www.google-analytics.com/collect?v=1&t=pageview&tid=UA-111111111-1&cid=$cookie_cid&uip=$remote_addr&dh=$host&dp=$uri&dr=$http_referer&z=$msec;
    }

这个时候,以上配置中有两个需要注意的地方:

  • tid=UA-111111111-1:自行修改为 Google_Analytics 分配给你网站的
  • cid=$cookie_cid:这个是用户标识,按照标准应该为 uuid 格式,为每位用户分配的标识应该独一无二。

其中,这里的变量 $cookie_cid 通过 Openresty 实现,是作为分配给每次访问的客户 id,生成方式会在下文进行说明。

配置 Openresty

要通过 Openresty 生成 uuid 分配给访客,可以通过 $remote_addr 生成,这是基于 client ip 生成的客户标识:

    local str_random = ngx.var.binary_remote_addr
    local str_md5 = ngx.md5(str_random)
    local str_sub = string.sub(str_md5, 1, 8) .. "-" .. string.sub(str_md5, 9, 12) .. "-" .. string.sub(str_md5, 13, 16) .. "-" .. string.sub(str_md5, 17, 20) .. "-" .. string.sub(str_md5, 21, 32)
    cid = str_sub

或者也可以通过访问时间生成,将 str_random 换一种定义方式,其中 ngx.time() 函数提供秒级的当前 unix 时间戳:

    ngx.update_time()
    local str_random = ngx.time()

然后需要做的就是把 Openresty 中的这个变量 $cid 传递到 Nginx,即在 location 中所需要的 $cookie_cid 变量。使用 cookie 传递是最简单的方法:

    local cookie_cid = nil
    if (ngx.var.cookie_cid == nil) then
        cid = userid.generate()
    end

    ngx.header["Set-Cookie"] = {"cid=" .. cookie_cid}

然后 Nginx 中 proxy_pass 时就会读取 cookie 中的 cid 变量(即 $cookie_cid 变量),以此作为访客标识发送至 Google_Analytics 服务。

不发送的行为

“不发送的行为”包括并不限于 spider 和 request_file_not_exist 两种常见情况,爬虫和 404 我们是不会想提交给 Google_Analytics 服务的。可以加入是否执行 post_action 的判断。

通过 Openresty 判断筛选不希望行为统计被提交的情况:

    -- 这里把 ".../" 替换成你的网站文件的绝对路径
    local request_filename = ".../" .. ngx.var.uri
    -- 判断请求文件是否存在
    local file = io.open(request_filename, "r")

    if (file and ngx.re.find(ngx.var.http_user_agent, "qihoobot|Baidu", "jo") == nil) then
        ngx.header["Set-Cookie"] = {"stats=" .. "1"}
    end

然后在 Nginx 中进行判断,仅满足 $cookie_stats = 1 的访问提交给统计服务:

    location / {
        ...

        if ($cookie_stats = "1") {
            post_action @google_analytics;
        }
    }

Lib 实现

本文仅提供最基本的思路介绍,完整的实现方案我已封装为 lib 并发布于 Github

本站已使用本文所述方案,效果可参见本站 cookie。

Reference

感谢以下作者提供的思路:

  • https://darknode.in/network/nginx-google-analytics/
  • https://eason-yang.com/2016/11/04/google-analytics-via-nginx/
  • https://blog.nfz.moe/archives/google-analytics-optimize.html