analytics.js
是谷歌官方提供的,用于 Google Analytics 监测埋码的 JavaScript 库。通过部署该工具,我们可以非常方便地构建发送至谷歌服务器的监测请求。
analytics.js
构建的监测请求,一般表现为如下形式:https://www.google-analytics.com/collect?v=1&_v=j81&......
熟悉 GA 的朋友在对该请求进行检查时,通过会关注该请求所携带的数据(payload - post请求)或参数(query string parameters)。
analytics.js
默认采用的 GET 请求,所以我们一般在 query string parameters 中查看参数的传值,这部分内容一般表现为如下形式:
可以看到,这里包含了,诸如 t / dl / ul / dt 等诸多参数,这些参数对应 GA 测量协议中的参数,一般可以从该链接中查看其详细含义。
但是仍有部分字段是没有标注在测试协议文档中的,属于谷歌内部使用的字段,例如 _v _s _u
此类前缀带有下划线 _
的参数。
今天我们主要研究一下 _u
这个参数是做什么用的。
参数 _u
参数 _u
由 analytics.js 构建,我们进入 analytics.js 直接查看与此部分相关的源码。(注意,由于 analytics.js 进行了加密混淆,下列演示的示例,相应方法名称并不固定。)
首先,我们下载 analytics.js 源代码文件,注意,这块可以使用 GA Debugger Chrome 拓展插件获取可以在 Console 打印 Debug 日志的 analytics_debug.js 文件,这个版本的文件中,存在部分注释,便于我们理解。
然后我们搜索 _u
关键字,可以搜索到如下部分内容。
var Sf = X("_cd2l", void 0, !1)
, oc = W("usage", "_u")
, af = W("_um");
注意,这里 Sf / oc / af / X() 等都是经过了混淆后的内容,不具有实际含义。
注意到这里出现了 usage。
继续搜索 oc 关键字,出现如下部分内容:
function mc(a) {
try {
Q.navigator.sendBeacon ? F(42) : Q.XMLHttpRequest && "withCredentials"in new Q.XMLHttpRequest && F(40)
} catch (c) {}
a.set(oc, cf(a), !0);
a.set(md, jc(a, md) + 1);
var b = [];
nf.map(function(c, d) {
d.i && (c = a.get(c),
void 0 != c && c != d.defaultValue && ("boolean" == typeof c && (c *= 1),
b.push(d.i + "=" + P("" + c))))
});
!1 === a.get(qf) && b.push("npa=1");
b.push("z=" + be());
a.set(Na, b.join("&"), !0)
}
可以看到 d 是 ef 的一个实例,然后在 d 中执行了 set 命令,set 的参数为 yc(oc).i
以及 cf(b)
这里面 oc = W("usage", "_u");
继续查询 ef ,可以找到如下代码段:
var ef = function() {
this.keys = [];
this.values = {};
this.u = {};
this.debug = !1
};
ef.prototype.set = function(a, b, c) {
this.debug && L(" " + a + "=" + Fa(b) + (c ? " (temp)" : ""));
this.keys.push(a);
c ? this.u[":" + a] = b : this.values[":" + a] = b
}
;
ef.prototype.get = function(a) {
return this.u.hasOwnProperty(":" + a) ? this.u[":" + a] : this.values[":" + a]
}
;
ef.prototype.map = function(a) {
for (var b = 0; b < this.keys.length; b++) {
var c = this.keys[b]
, d = this.get(c);
d && a(c, d)
}
}
由于我们知道,analytics.js 代码其核心是为了构建跟踪器&跟踪器中的参数&相关的方法。
进入部署了 anlytics.js 的页面,在 Devtool 中的 Console 中,输入 ga.getAll()
,可以获得如下结果:
可以看到属性&方法基本一致,标注为 ef,可以说这里的 set 就对应了跟踪器的 set 方法,所以这里的执行相当于在 ef -> keys 中 push 了 yc(oc).i
由于仅传了两个参数,即 形参 c 未传,则 ef -> values 中 :usage
应该为 形参 b 的值,即为:cf(b)
所以我们接下来需要关注的就是 cf(b)
,即 cf()
& b
先看 cf()
cf()
搜索 cf 可以找到如下代码段:
var Ne = new ec;
function F(a) {
Ne.set(a)
}
var cf = function(a) {
a = $e(a);
a = new ec(a);
for (var b = Ne.B.slice(), c = 0; c < a.B.length; c++)
b[c] = b[c] || a.B[c];
return (new ec(b)).encode()
}
, $e = function(a) {
a = a.get(af);
sa(a) || (a = []);
return a
};
这里面有一些相关内容,如下所示:
var af = W("_um");
var sa = function(a) {
return "[object Array]" == Object.prototype.toString.call(Object(a))
}
这里,构造比较精妙的是 "[object Array]" == Object.prototype.toString.call(Object(a))
Object 构造函数,创建一个对象包装器,
Object.prototype
属性表示 Object 的原型对象,Object.prototype.toString()
方法返回一个表示该对象的字符串。call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
也就是说,可以推断,这里的 a 预期应该是一个 Array Object. 进而说明 cf(b) 中的 b 预期是一个 Array Object.
接下来,我们继续看 ec 这个对象,搜索 ec 可得:
var ec = function(a) {
this.B = a || []
};
ec.prototype.set = function(a) {
this.B[a] = !0
}
;
ec.prototype.encode = function() {
for (var a = [], b = 0; b < this.B.length; b++)
this.B[b] && (a[Math.floor(b / 6)] ^= 1 << b % 6);
for (b = 0; b < a.length; b++)
a[b] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".charAt(a[b] || 0);
return a.join("") + "~"
};
可以看到,ec 构造了两个方法,set / encode,而其中 encode 也正是 cf 的返回对象中所涉及的。return (new ec(b)).encode()
我们重点关注一下 .encode()
方法.
从 return 开始分析,return 了一个 a.join("") + "~"
这块正好对象 _u
的值,例如:AACAAUAB~
,继续往上看,
for (b = 0; b < a.length; b++)
a[b] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".charAt(a[b] || 0);
这里面 String.prototype.charAt(index)
方法从一个字符串中返回指定的字符,index 对应数字索引。
如果 a.join("") + "~"
的结果对应 AACAAUAB~
,则相当于原来的输入 array 为 [0, 0, 2, 0, 0, 20, 0, 1]
,继续往上看
for (var a = [], b = 0; b < this.B.length; b++)
this.B[b] && (a[Math.floor(b / 6)] ^= 1 << b % 6);
this.B
由 set 决定,标明了设置与其中的参数。
且根据前面的推论,可知 ec 的参数是一个 Array Object.
然后再看这句 this.B[b] && (a[Math.floor(b / 6)] ^= 1 << b % 6);
,这句代码很有意思。
this.B
通过初始化和 .set 方法获得,猜测里面是一个 array 对象。如果是使用 .set 方法置入的则 this.B[a] = !0 也就是为 True,则可以继续 &&
Math.floor
用于向下取整,b 为从0开始 - 最大不得超过 B 内包含的元素数。
^=
是按位 “异或” 赋值运算符,
result ^= expression
与 result = result ^ expression
意义相同,用于查看两个表达式的二进制表示的值,并执行按位异或。
按位异或,表现为下种情况:
0101 (expression1)
1100 (expression2)
expression1 ^ expression2
result = 1001
当且仅当只有一个表达式的某位为 1 时,结果的该位才为 1。否则,结果的该位为 0。
<<
为左移运算符,例如:
var bar = 5; // (00000000000000000000000000000101)
bar <<= 2; // 20 (00000000000000000000000000010100)
a << b 等同于 a * (2**b)
%
是求余赋值,例如:
bar = 5
bar %= 2 // 1
除了各运算符的说明外,还有一点在于运算符的优先级,查 运算符优先级表
可知,(a[Math.floor(b / 6)] ^= 1 << b % 6);
中, % 的优先级高于 << 则,先进行取模,然后做 1 的左移,然后赋值给 a[Math.floor(b / 6)]
b % 6 结果只可能为 0/1/2/3/4/5, 1 << b % 6 结果只有 1/2/4/8/16/32。
此时由于实际运算中情况十分复杂,此时直接分析代码已经到头,需要进一步在代码中进行打点,以继续分析。
我们在 ec.prototype.encode 中打印一下 this.B 的值,得到如下结果:
[empty × 13, true, empty × 18, true, empty × 9, true]
TODO