Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
zmmbreeze opened this issue Sep 23, 2016 · 0 comments
Open

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

zmmbreeze opened this issue Sep 23, 2016 · 0 comments

Comments

@zmmbreeze
Copy link
Owner

zmmbreeze commented Sep 23, 2016

作者前段时间在做类似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。如下图:

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
@zmmbreeze zmmbreeze changed the title GA代码小技巧之cookie篇 GA源代码里的小技巧之cookie篇 Sep 24, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant