GA源代码里的小技巧之cookie篇

GA源代码里的小技巧之cookie篇

作者前段时间在做类似Google Analytics(以下简称GA)的第三方监控脚本。所以对GA的前端代码做过调研,对GA的压缩后代码做了一定程度上的人肉美化。这里美化的是analytics.js的j41版本,本文提到的小技巧也是基于这个版本的js。

cookie的本质是存储在浏览器端的一段简单数据(多个键值对),浏览器会从服务器接受或者发送给服务器cookie。这样便可以为没有状态的HTTP协议提供了记录状态信息的方法,知道多个不同的HTTP请求是否来自同一个浏览器。

在浏览器中cookie是支持范围最广的存储数据的手段了,前端工程师一般都曾经或多或少的使用过cookie来存储数据或变量。浏览器提供了document.cookie这个接口来增删改查cookie。但是只能一次设置一个cookie。使用方法如下:

// 设置名为key的cookie,值为value
document.cookie = 'key=value';

代码中省略了其他可选配置,具体使用方法可以参考MDN文档。通过domainpath参数,可以将cookie设置在不同的域名或者路径下,例如:

// 设置名为key的cookie,值为value
document.cookie = 'A=1;path=/;domain=map.baidu.com;';

上面的代码在域名map.baidu.com下的根路径(/)设置了cookie A的值为1。我们可以通过document.cookie来获取当前域名和路径下的所有cookie。当我们访问http://map.baidu.com页面时,执行document.cookie获得的结果是A=1

大家知道域名是有父域名和子域名的区别的,键名相同的两个cookie可以分别设置在父子域名上。例如如下代码:

document.cookie = 'A=1;path=/;domain=.example.com;';
document.cookie = 'A=2;path=/;domain=a.example.com;';

顺带提到一个冷门的知识点:以.开头的域名表示cookie设置在此域名及其子域上,否则不适用于其子域名。不过从RFC 2965标准开始浏览器便会自动为domain属性值自动添加一个.前缀,所以在设置domain时加不加.前缀已经没有区别了。但是为了兼容一些旧浏览还是加.为好。另外如果不设置domain浏览器会默认用当前页面的域名,这时浏览器不会自动添加.前缀,自然也就不会包含子域了。

.example.coma.example.com域名下面分别设置了A=1A=2两个cookie。此时我们在http://a.example.com页面下执行document.cookie得到的结果是:A=1;A=2。如下图:

开发者是没办法从结果中知道这个cookie是设置在哪个域名上的。同样这个情况也适用于不同的父子路径上。

这在一般情况下对开发者不会有影响,但是对于GA来说确实致命的。GA会在当前网站域名下面设置一个全局唯一的cookie _ga,用于标志相同的用户。现在大型的公司都会分不同的网站域名,例如:baidu.comditu.baidu.com。假设百度使用了GA~ 那么GA会分别在两个域名下设置不同的_ga cookie值,这样在baidu.com下GA便会拿到两个_ga值。不知道该用哪个,傻傻分不清楚。

为了解决这个问题,GA在cookie的值上面做文章。可以看到冲突只会发生在父子域名和父子路径上。因为cookie本身的特殊性:所有http请求会带着该域名下的所有cookie。如果cookie值太长太多会消耗太多带宽。GA通过计算域名和路径的“长度”来唯一标示这个cookie所设置在的域名和路径。计算“长度”的方法如下:

/**
 * normalize domain.
 * remove the first '.' if exist.
 * @param {string} domain domain String
 * @return {string} normalized domain
 */
var normalizeDomain = function (domain) {
    return domain.indexOf('.') === 0 ? domain.substr(1) : domain;
};

/**
 * get count of domain.
 * getDomainCount('qq.com') === 2
 * getDomainCount('.qq.com') === 2
 * getDomainCount('b.qq.com') === 3
 * getDomainCount('e.qidian.qq.com') === 4
 *
 * @param {string} domain domain String
 * @return {string} normalized domain
 */
var getDomainCount = function (domain) {
    return normalizeDomain(domain).split('.').length;
};

/**
 * normalize path
 * normalizePath('') === '/'
 * normalizePath('/') === '/'
 * normalizePath('/ping') === '/ping'
 * normalizePath('/ping/pv') === '/ping/pv'
 * normalizePath('ping/pv') === '/ping/pv'
 * normalizePath('ping/pv/') === '/ping/pv'
 *
 * @param {string} path path
 * @return {string} path
 */
var normalizePath = function (path) {
    if (!path) {
        return '/';
    }

    if (path.length > 1 && path.lastIndexOf('/') === path.length - 1) {
        // remove last '/'
        path = path.substr(0, path.length - 1);
    }

    if (path.indexOf('/') !== 0) {
        // fill up first '/'
        path = '/' + path;
    }

    return path;
};

/**
 * get count of path
 * getPathCount('') === 1
 * getPathCount('/') === 1
 * getPathCount('/ping') === 2
 * getPathCount('/ping/pv') === 3
 * getPathCount('ping/pv') === 3
 * getPathCount('ping/pv/') === 3
 *
 * @param {string} path path
 * @return {number} count of path
 */
var getPathCount = function (path) {
    path = normalizePath(path);
    return path === '/' ? 1 : path.split('/').length;
};

/**
 * Get domain and path count string.
 *      domainCount + '-' + pathCount + '$'
 *
 * @param {Window=} win window context.
 * @param {string=} domain cookie domain.
 * @param {string=} path cookie path.
 * @return {string} count string.
 */
var getDomainAndPathCount = function (win, domain, path) {
    win = win || window;

    var location = win.location;
    var pathCount = getPathCount(path != null ? path : location.pathname);
    var domainCount = getDomainCount(domain != null ? domain : location.hostname);
    return domainCount + (pathCount > 1 ? '-' + pathCount : '') + '-';
};

GA在设置cookie时加上domainpath的长度前缀,然后在取出cookie时遍历所有的名字相同的cookie找到指定域名和路径下的cookie。示例代码如下:

/**
 * Set cookie.
 * @param {string} key cookie name.
 * @param {string|number} value cookie value.
 * @param {Window=} win window context.
 * @param {number=} expires cookie expired time in milliseconds.
 * @param {string=} domain cookie domain.
 * @param {string=} path cookie path.
 * @return {boolean} success or not.
 */
var setCookie = function (key, value, win, expires, domain, path) {
    var domainAndPathCount = getDomainAndPathCount(win, domain, path);
    value = value + '';
    return setCookieRaw(key, domainAndPathCount + value.replace(/\-/g, '%2d'), win, expires, domain, path);
};
/**
 * Get cookie value.
 *
 * @param {string} key key name.
 * @param {Window=} win window context.
 * @param {string=} domain cookie domain.
 * @param {string=} path cookie path.
 * @return {string} cookie value.
 */
var getCookie = function (key, win, domain, path) {
    var results = getCookieRaw(key, win);
    var domainAndPathCount = getDomainAndPathCount(win, domain, path);
    for (var i = 0; i < results.length; i++) {
        var r = results[i];
        if (r.indexOf(domainAndPathCount) === 0) {
            return r.slice(domainAndPathCount.length).replace(/%2d/g, '-');
        }
    }
    return '';
};

魔鬼?藏于细节。GA对细节的追求并没有止步于此。cookie名_ga虽然一般很少有开发者会用到,但是不怕一万就怕万一。如果cookie名_ga冲突了并被改写成了别的值,那么GA很可能会发送一个不符合要求的值过去。这对服务器端解析也非常不利。

GA在cookie的值之前又加了前缀GA1.,下次使用这个cookie时都会检查是否带有GA1.前缀。如果不存在前缀则直接覆盖生成新的,如果存在则继续复用原cookie值。

除了防止_ga被开发者覆盖之外,猜想还有一个作用,就是“版本号”。多个GA版本之间如果_ga的cookie值生成算法不一样需要特殊处理,那么可以依据GA1.前缀来判断应该是哪个版本,采取正确的操作。

总结下_ga cookie值的格式如下:

// GA{version}.{domainCount}[-{pathCount}].{randomNumber}.{time}
// path是'/'时
document.cookie = '_ga=GA1.3.494346849.1446193077';
// 没有path时
document.cookie = '_ga=GA1.3-2.494346849.1446193077';

细微之处见真章:+1:

参考文档:

  1. RFC 2109
  2. RFC 2965
  3. RFC 6265
  4. What does the dot prefix in the cookie domain mean?
  5. MDN document.cookie

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏李鹏的专栏

Dubbo 发布恢复维护后的第一个版本 2.5.4

Dubbo 发布了恢复维护后的第一个版本 2.5.4,主要是解决 issues 和依赖升级。

1481
来自专栏Java技术栈

分布式 | Dubbo 架构设计详解

Dubbo是Alibaba开源的分布式服务框架,它最大的特点是按照分层的方式来架构,使用这种方式可以使各个层之间解耦合(或者最大限度地松耦合)。从服务模型的角度...

1612
来自专栏周明礼的专栏

一步一步带你搭建一个“摩登”的前端开发环境

最近几年也陆续推出了多种不同的js类型系统用于增强js的健壮性,其中像 typescript 就是其中的佼佼者。当然我今天要讲的并不是typescript,而是...

1.5K0
来自专栏后台全栈之路

基于汇编的 C/C++ 协程 - 切换上下文

既然本系列讲的是基于汇编的 C/C++ 协程,那么这篇文章我们就来讲讲使用汇编来进行上下文切换的原理。

3936
来自专栏AndroidTv

讲讲断点续传那点儿事提问理论基础代码示例

这次想来讲讲断点续传,以前没相关需求,所以一直没去接触,近阶段了解了之后,其实并不复杂,那么也便来写一篇记录一下,分享给大伙,也方便自己后续查阅。

942
来自专栏学习有记

学委助手

学委除了要收作业,最烦的就是统计谁没有交作业啦,还有就是大家的命名不统一造成文件排序混乱,更加大了学委统计的难度。所以,写这个应用的目的就是查交和格式化文件命名...

1982
来自专栏阿杜的世界

【转】Dubbo架构设计详解总体架构核心要点参考资料

Dubbo是Alibaba开源的分布式服务框架,它最大的特点是按照分层的方式来架构,使用这种方式可以使各个层之间解耦合(或者最大限度地松耦合)。从服务模型的角度...

1145
来自专栏跟着阿笨一起玩NET

Microsoft SyncToy 文件同步工具

SyncToy 是由 微软 推出的一款免费的文件夹同步工具。虽然名字中有一个 Toy,但是大家可千万不要误以为它的功能弱爆了。实际上,我感觉这款软件还真是摆脱了...

1252
来自专栏数据之美

玩转 Nginx 之:使用 Lua 扩展 Nginx 功能

1、Nginx 简介 Nginx 作为一款面向性能设计的HTTP服务器,相较于Apache、lighttpd具有占有内存少,稳定性高等优势。其流行度越来越高,应...

1K7
来自专栏菩提树下的杨过

scala + intellij idea 环境搭建及编译、打包

大数据生态圈中风头正旺的Spark项目完全是采用Scala语言开发的,不懂Scala的话,基本上就没法玩下去了。Scala与Java编译后的class均可以运行...

7927

扫码关注云+社区

领取腾讯云代金券