Sukka's Blog

童话只美在真实却从不续写

使用 Cloudflare Workers 加速 Google Analytics

Sukka's Avatar 2019-08-26 创作集

  1. 1. 异步 Google Analytics
  2. 2. 前端数据收集和发送
  3. 3. 服务端逻辑
  4. 4. 过滤非真实访客数据

我一直以来都在使用 Google Analytics 统计自己的博客和几个网站访问情况。但是一个 gzip 以后都还有 45KB 大小的 analytics.jsCache-Control 还只有 7200 秒;Google 国内的数据中心会被抽风不说,www.google-analytics.com 域名早就上了各个广告屏蔽软件的黑名单。

异步 Google Analytics

避免用户直接给 Google Analytics 发起请求的思路已经有很多人提出并进行了实践。常见的思路有在 Web Server(一般是 Nginx)上实现统计请求并转发给 Google Analytics;一种是自己搭建一个后端程序,比如 Go 编写的 ga-proxy

Cloudflare 最近推出了他们的 Serverless 平台 Cloudflare Workers,每天有 10 万次请求的免费额度;考虑到我的 PV 一时半会是达不到每天 10 万的,我决定把就用 Cloudflare Workers 实现一个 Google Analytics 异步转发。

首先丢 GitHub,食用指南写在 README 里了,欢迎大家丢 star~

本文完(并没有)

前端数据收集和发送

我并不是十分在意详细访问情况,但是基本的数据还是要收集的:

  • dl: 当前页面 URL
  • uip: 用户的 IP 地址
  • ua: User Agent
  • dt: 当前页面的标题
  • dr: Referrer
  • ul: 浏览器的语言
  • sr: 当前屏幕分辨率

除此以外,我还通过 Performance Timing API 收集页面的加载性能:

  • plt: 页面加载时间
  • dns: DNS 解析用时
  • pdt: 页面下载用时
  • rrt: 重定向用时
  • tcp: TCP 连接用时
  • srt: 服务器响应用时
  • dit: DOM Interactive 用时
  • clt: Content Load 用时

以上数据分为两类,一类是当前页面的 Page View 信息,一类是 Timing 信息。大部分数据都可以通过 JS 获取到,当前页面的 URL 和 User-Agent 还可以通过 Request Headers 获取到,Cloudflare Worker 还可以通过 Cloudflare 的 cf-connecting-ip 的回源请求头获取到用户的真实 IP 。只要将这些数据通过 GET 的方式发送即可。

Google 的 analytics.jsga-proxy 都是让用户发送两个请求、一个请求绑定在 DOMContenLoaded 的 Event 上发送 Page View 信息,另一个请求绑定在 window.onload 事件上发送 Timing 信息。这样即使用户在页面加载完成之前就关闭了页面,只要 DOMContenLoaded 事件触发了,就可以提前发送 Page View 信息。考虑到 Cloudflare Workers 的免费额度是有限制的、我也不在乎数据的准确性,所以我只在 window.onload 事件上绑定了一个请求、将 Page View 和 Timing 信息通过这一个请求全部发送出去。

服务端逻辑

统计需要为每个用户生成唯一标识 UUID。虽然 Cloudflare 会为每个用户生成一个 _cfuid 的 cookie 可以直接使用,但是考虑到 Google Analytics 推荐使用 Version 4 的 UUID,所以我还是需要在远端实现一个 UUID 的逻辑。首先是检查 cookie 中有没有 uuid,如果没有就需要生成一个 UUID 并通过 set-cookie 下发给浏览器。

使用 JavaScript 生成 UUID 有很多种方法,在 JSPerf 上做了测试后,性能最好的是这个:

const createUuid = () => {
    let s = [];
    const hexDigits = '0123456789abcdef';
     for (let i = 0; i < 36; i++) {
        s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
    }
    s[14] = '4'; // bits 12-15 of the time_hi_and_version field to 0010
    s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
    s[8] = s[13] = s[18] = s[23] = '-';
    return s.join('');
};

浏览器采集的数据是通过 GET 方法发送给服务端的,服务端只需要在 URL 中把参数提取出来再发送给 Google Analytics 就好了。为了不影响性能、降低客户端统计请求的 TTFB,回传数据给 Google Analytics 是异步的,需要先为浏览器生成一个 Response。Google Analytics 会给浏览器返回一个 1px 的 GIF 图片保证兼容性和性能的平衡;不过对于更现代的浏览器,content-length 为 0 的 204 状态码显然性能开销更小。

在 Cloudflare Workers 上生成一个 204 请求的方法很简单,只需要设置状态码为 204,并将 Response Body 设置为 null 即可:

response = new Response(null, { status: 204, statusText: 'No Content' });

Cloudflare 也兼容 Response Body 为 Empty String 的 204 请求,但是 Cloudflare 推荐使用 null,Empty String 的方法不保证向后兼容。

Cloudflare Workers 支持向其他 URL 发送请求,写法和 Fetch API 类似,在 Cloudflare Workers 中被称为 Subrequest。Cloudflare Workers 在给浏览器返回 Response 后会停止运行、释放计算资源;对于需要异步的日志采集(比如回传数据给 Google Analytics),Cloudflare Workers 提供了一种 Service Worker 中的 ExtendableEvent.waitUntil() 写法,通过 waitUntil 调用的函数可以在 Response 发送给浏览器后继续持续运行。

async function senData(data, reqParameter) {
    await fetch(data, reqParameter);
}
...
event.waitUntil(senData(data, parameter));

过滤非真实访客数据

在 Nginx 等 Web Server 中实现统计请求的问题在于,爬虫发送的 HTTP 请求都会被统计,因此会包含许多非真实访客的数据。在前端使用 JS 采集和发送数据可以避免简单的 GET 请求的数据。

Cloudflare Workers 工作在 Cloudflare Cache Layer 前面,因此即使设置了 cache-control 和 Edge Cache TTL,Cloudflare Workers 依然是不可缓存的;但是 Firewall Rules、Cloudflare WAF、Cloudflare Rate Limit 都工作在 Cloudflare Workers 前面,因此你还可以通过设置 Cloudflare Firewall 来拦截不必要的请求。

本文作者 : Sukka
本文采用 CC BY-NC-SA 4.0 许可协议。转载和引用时请注意遵守协议!
本文链接 : https://blog.skk.moe/post/cloudflare-workers-cfga/

本文最后更新于 天前,文中所描述的信息可能已发生改变