GA analytics.js 发送请求中的 _u 参数


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 中查看参数的传值,这部分内容一般表现为如下形式:

GA 请求示例

可以看到,这里包含了,诸如 t / dl / ul / dt 等诸多参数,这些参数对应 GA 测量协议中的参数,一般可以从该链接中查看其详细含义。

Measurement Protocol 参数参考

但是仍有部分字段是没有标注在测试协议文档中的,属于谷歌内部使用的字段,例如 _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() ,可以获得如下结果:

GA 跟踪器示例

可以看到属性&方法基本一致,标注为 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 ^= expressionresult = 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