配合 Measurement Protocol 优化 Google Analytics

我使用 Google Analytics 来统计访问情况,按照官方说明,直接引入 js 文件就可以了,但后来因为使用了 React 这些 SPA ,官方提供的方法不再起作用,就用了别人做好的专门用于这些框架的库。最近了解到 Measurement Protocol 这么一个东西,正好可以替代目前的引入方式。

我对 Google Analytics 的需求并不高,只看他的访客,页面,来源这些数据,使用频率也不高,算是可有可无的这么一个需求吧,但还是想尝试下这种新的方式,因为对我来说还是有些好处的:

  • 无需使用官方 js ,减少加载
  • 使用自已的域名,不用担心被拦截
  • 不用担心个别地区官方 js 加载有问题

Google Analytics(分析)Measurement Protocol 可让开发者通过 HTTP 请求直接向 Google Analytics(分析)服务器发送原始用户互动数据。这样,开发者就可以衡量在各种环境中用户与商家互动的情况。

以上是官方的介绍,我们可以利用 Measurement Protocol 做一个中间代理,浏览器收集的信息先发送至代理,再有代理通过 Measurement Protocol 向 Google Analytics 提交访客信息

收集信息

获取浏览器标题,URL,分辨率,语言... 可以直接读取 window document location 相关属性,但如何把数据送到自己的后端,倒是需要考虑下。使用比较多的方案是新建一个 img 对象,设置 src 为后端 url ,将数据放在 params 里。还有一种是使用 navigator.sendBeacon

navigator.sendBeacon(url, data);

这个方法主要用于满足统计和诊断代码的需要,这些代码通常尝试在卸载(unload)文档之前向 web 服务器发送数据。

看描述信息,似乎这个方法就是专门用来做这种事的,而且使用也简单,只需传入目标 URL 和数据,就会发起 POST 请求,数据类型可以是 ArrayBufferView 或 Blob, DOMString 或者 FormData

具体的参数名称可以从 Measurement Protocol 参数参考 上找到。我自己也简单的把这些逻辑封装成了 ga-beacon

接受信息

需要有一个后端来接数据,我选择了阿里云的函数计算和 API 网关,使用 Serverless Framework 进行部署。但在部署的时候,发现有一个问题:如果 API 网关要能与函数通信,必须在同一实例类型(经典网络或 VPC),但 Serverless Framework 默认创建的 API 实例类型是经典网络,函数又是 VPC,然后我也没找到相关配置的地方,只好手动创建 API 网关。

对于每个不同的访客,需要用 UUID 来区分,我将这个信息放在 cookie 里,由于 Chrome 改了策略,我又使用了不同的域名,在加上 navigator.sendBeacon 使用的是 POST 请求,必须给 Set-Cookie 加上 SameSite=None; Secure; 这也导致后端必须使用 HTTPS

以下是我的函数计算主文件:

const axios = require("axios");
const cookie = require("cookie");
const uuidv4 = require("uuid").v4;

exports.proxy = async (event, context, callback) => {
  try {
    const request = JSON.parse(event);

    const requestBody =
      request.isBase64Encoded === true
        ? Buffer.from(request.body, "base64").toString()
        : request.body;

    const requestBodyJson = JSON.parse(requestBody);

    const requestHeader = Object.keys(request.headers).reduce((header, key) => {
      header[key.toLowerCase()] = request.headers[key];
      return header;
    }, {});

    const requestCookie = cookie.parse(requestHeader["cookie"] || "");

    const requestUUID = requestCookie["uuid"];

    const responseUUID = requestUUID || uuidv4();

    const gaBody = Object.assign({}, requestBodyJson, {
      v: 1,
      cid: responseUUID,
      uip: request.headers.ClientIP || "",
    });

    const gaBodyString = Object.keys(gaBody)
      .map((x) => {
        return `${x}=${encodeURIComponent(gaBody[x] || "")}`;
      })
      .join("&");

    await axios({
      url: "https://www.google-analytics.com/collect",
      method: "post",
      data: gaBodyString,
    });

    const response = {
      isBase64Encoded: false,
      statusCode: 204,
      headers: {
        "Content-Type": undefined,
        "Set-Cookie": requestUUID
          ? undefined
          : cookie.serialize("uuid", responseUUID, {
              maxAge: 63072000,
              httpOnly: true,
              sameSite: "none",
              secure: true,
            }),
      },
    };

    callback(null, response);
  } catch (error) {
    callback(error);
  }
};