借助 Measurement Protocol 防 GA 拦截
上次的问题:由于网络不畅,导致 gtm.js 长时间加载,最终影响整个页面的响应。
今天尝试去解决它,方案的出发点很简单:既然依托于 Google 的 gtm.js 被污染劣化,那索性不加载额外的 js,把自己当个离线设备,自己采集、整理、发送数据总可以了吧。这个想法的依据就是 GA 提供的:Measurement Protocol。
什么是 Measurement Protocol
使用 Google Analytics 做 Web 数据采集,通常的做法是在网页上加载 GA 的采集 SDK,通过这一 SDK 完成一系列自动以及自定义的数据采集需求。这一方案兼顾部署效率以及兼容宽广度,是被推荐的通行办法,沿用多年。直到 2012 年,GA 进步到 Universal Analytics 版本时,引入了 Measurement Protocol,用于实现各种在线离线环境的数据采集和整合,延伸 GA 的应用范畴。
什么是 Measurement Protocol?顾名思义,它是一个度量协议,背靠 GA API,约束了如何传递、以及可以传递哪些数据到 GA 服务器。至于怎么采集这些需要传递的数据,则需要你根据自己的情况,实现自己的需求。注意上图,平时 ga.js、analytics.js 帮助我们实现了数据的自动采集,但如果我们无法加载这些 js,也可以自己把数据按规范采集好,通过 Measurement Protocol 发往 GA 进行处理。
好比说,GA 提供了一条 A 到 B 的物流管道,可以帮你快速运载特定物资从 A 到 B,但物资的清理、打包、装车则需要你自己想办法。要想这个办法,也不难,我们只需继续追问自己如下问题👇:
- GA 平时采集些什么数据
- 我自己如何采集这些数据
- 如何通过 Measurement Protocol 上传数据
Google Analytics 平时采集些什么数据
这个问题,GA 有详细的帮助和开发文档可供参考,Measurement Protocol Parameter Reference 内更有各个参数的门类梳理,在此,仅以此篇文章的 GA 采集请求为例,梳理一下我自己的需求。
上图 21 个参数,乍一看不少,结合 Measurement Protocol Parameter Reference 稍作梳理,即可发现,关键内容只有 dl、dt、cid 等几项。因为这是最普通的内容页面浏览行为,并不涉及其他的事件交互、交易环节等,且我也只想关注用户来源、浏览内容、以及环境信息等 3 个方面的指标,即:
- 用户识别(什么人)
- 环境信息(什么时间、设备)
- 浏览内容(浏览什么文章)
如何采集这些数据
假如我们自己想设计一套 Web 数据采集工具,最初的起点,应该也就是上文这些东西:用 cookie 来做用户标注,环境信息通过 user agent 来获得,浏览内容等通过 DOM API 去读取。
1. 用户识别
这里仅讨论匿名 cookie,思路简单:如果还不存在 cid
cookie,则生成一个;如果已经有了 cid
则使用现有的,说明是老访客。对于自定义 Client ID,GA 有现成的规范,需要符合 UUID v4 标准。我也不是专业的技术人员,所以以下代码多来自 stackoverflow。
它做 3 件事:生成 UUID v4 规格的 cid
,提取现有 cookie 中的 cid
,有则用之,无则赋值。
// https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}
// https://stackoverflow.com/questions/10730362/get-cookie-by-name
function getcookie(name) {
var value = '; ' + document.cookie
var parts = value.split('; ' + name + '=')
if (parts.length == 2) { return parts.pop().split(';').shift() }
}
var cid = getcookie('cid') || uuidv4()
if (!document.cookie) { document.cookie = 'cid=' + cid }
2. 环境属性
这里所说的环境属性,我是指用户使用的什么浏览器、从什么来源过来等信息。这些都是常规客户端信息,要获得也很简单,按照浏览器提供的各项 API 来读取即可。API 文档可参考 MDN,专业、详实又丰富。
'&dr': document.referrer
'&ul': (navigator.language || navigator.userLanguage)
'&dp': (window.location.pathname + window.location.search)
'&sd': screen.colorDepth
'&sr': (screen.width + 'x' + screen.height)
'&vp': (Math.max(document.documentElement.clientWidth, window.innerWidth || 0) + 'x' + Math.max(document.documentElement.clientHeight, window.innerHeight || 0))
3. 浏览内容
通上边的环境信息一样,调用相应的 DOM API 即可。如果是 Event Tracking、EC Tracking 等,具体的实现方式可能不同,但基本思路一致。
'&dl': document.location
'&dt': document.title
通过 Measurement Protocol 上传数据
这里可以分为三个部分:
- 数据的整合(Payload)
- 传输方式的实现(接口,GET/POST 的选取,以及如何发起)
基本条件(User Agent String 以及 IP,这里不用考虑)
数据的整合比较直观,按照文档来即可,所谓 Payload Data,它形如平常做 QA 时见到的 URL,只是字符串的连接。
?v=1&_v=j64&a=2079646992&t=pageview&_s=1&dl=http%3A%2F%2Flocalhost%2F2017%2F10%2F14%2Facross-the-firewall-with-measurement-protocol.html&ul=en-us&de=UTF-8&dt=%E5%80%9F%E5%8A%A9%20Measurement%20Protocol%20%E9%98%B2%20GA%20%E6%8B%A6%E6%88%AA&sd=24-bit&sr=1680x1050&vp=1035x257&je=0&_u=IEBAAcABI~&jid=1764491897&gjid=787771514&cid=1155599406.1508063074&tid=UA-101491967-2&_gid=1136739196.1508063074&_r=1>m=uab&z=1632362829
传输方式的实现,其实就是如何在客户端向 GA 发起 HTTP 请求,并带上上面的数据。文档提供了接口地址 https://www.google-analytics.com/collect
,并给出了可接受的请求方式 POST 和 GET。推荐 POST,因为它能够传输更多数据。
发起 POST 还需要另两个个条件:user_agent_string 和 IP 地址。因为我们正好是从浏览器端发起,user agent 和 IP 就不用管了,只需如何发起 POST 请求即可。如何发起,Ajax (Wikipedia)。
全部的范例代码如下。onreadystatechange
、readyState
与 XMLHttpRequest
的用法,请参考 MDN 文档。实际范例可打开浏览器开发工具,看本页的网络请求。
// https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}
// https://stackoverflow.com/questions/10730362/get-cookie-by-name
function getcookie(name) {
var value = '; ' + document.cookie
var parts = value.split('; ' + name + '=')
if (parts.length == 2) { return parts.pop().split(';').shift() }
}
function cachebuster() {
return Math.ceil(Math.random() * 1000000)
}
function setcookie() {
var d = new Date()
d.setTime(d.getTime() + 1000*60*60*24*365*2)
var expires = 'expires=' + d.toGMTString()
var cid = getcookie('cid') || uuidv4()
if (!document.cookie) { document.cookie = 'cid=' + cid + '; ' + expires + '; path=/' }
}
document.onreadystatechange = function() {
if (document.readyState === 'complete') {
setcookie()
var data = encodeURI(
'v=1&tid=UA-xxxxxxxx-y'
+ '&cid=' + getcookie('cid')
+ '&t=pageview'
+ '&dl=' + document.location
+ '&dr=' + document.referrer
+ '&ul=' + (navigator.language || navigator.userLanguage)
+ '&dp=' + (window.location.pathname + window.location.search)
+ '&dt=' + document.title
+ '&sd=' + screen.colorDepth
+ '&sr=' + (screen.width + 'x' + screen.height)
+ '&vp=' + (Math.max(document.documentElement.clientWidth, window.innerWidth || 0) + 'x' + Math.max(document.documentElement.clientHeight, window.innerHeight || 0))
+ '&z=' + cachebuster()
)
var xhr = new XMLHttpRequest()
xhr.open('POST', 'https://www.google-analytics.com/collect')
xhr.send(data)
}
}
总结
以上,只是 Measurement Protocol 的一个最为简单的范例,它的可玩性非常高。几乎可以在任何环境去按照规范构造数据,然后通过 Measurement Protocol 推送到 GA,进行数据整合,抛开对官方 SDK 的下载依赖,完全剥离了 GA/GTM SDK 与用户端页面的加载关系。
而 GA 的接收接口,这几年一直有通过监控宝进行可用性的监测,它被 GFW 拦截的时候并不多,而且有自己再做一层代理的玩法来进一步规避。
对于更复杂的数据采集需求,接下来打算细读一下 Segment 的开发文档,学习他们抽象出来的数据结构,学习他们的连接能力。