使用Cloudflare Workers執行Google Analytics

原因:

我一直以来都在使用Google Analytics统计自己的博客和几个网站访问情况。但是一个gzip以后都还有45KB大小的analytics.jsCache-Control还只有7200秒;Google国内的数据中心会被抽风不说,www.google-analytics.com域名早就上了各个广告屏蔽软件的黑名单。

說明:

  • 該方案所收集的數據有限,如需更詳細數據請用Google Analytics提供的程式碼。
  • Cloudflare Workers免費方案額度為一天/10萬個請求,對於流量很小的個人網站是綽綽有餘。大流量網站不適用──且收集的數據需要很詳細吧。

參照其方法,到Cloudflare新建一個Workers,在Script頁面將work.js內容複製貼上(因為是即時頁面讀取較慢,瀏覽器會一直提示停止執行):

//const AllowedReferrer = 'skk.moe'; // ['skk.moe', 'suka.js.org'] multiple domains is supported in array format

addEventListener('fetch', (event) => {
    event.respondWith(response(event));
});

async function senData(event, url, uuid, user_agent, page_url) {
    const encode = (data) => encodeURIComponent(decodeURIComponent(data));

    const getReqHeader = (key) => event.request.headers.get(key);
    const getQueryString = (name) => {
        const pattern = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
        const r = url.search.substr(1).match(pattern);
        return (r !== null) ? unescape(r[2]) : null;
    };

    const reqParameter = {
        headers: {
            'Host': 'www.google-analytics.com',
            'User-Agent': user_agent,
            'Accept': getReqHeader('Accept'),
            'Accept-Language': getReqHeader('Accept-Language'),
            'Accept-Encoding': getReqHeader('Accept-Encoding'),
            'Cache-Control': 'max-age=0'
        }
    };

    const pvData = `tid=${encode(getQueryString('ga'))}&cid=${uuid}&dl=${encode(page_url)}&uip=${getReqHeader('CF-Connecting-IP')}&ua=${user_agent}&dt=${encode(getQueryString('dt'))}&de=${encode(getQueryString('de'))}&dr=${encode(getQueryString('dr'))}&ul=${getQueryString('ul')}&sd=${getQueryString('sd')}&sr=${getQueryString('sr')}&vp=${getQueryString('vp')}`;

    const perfData = `plt=${getQueryString('plt')}&dns=${getQueryString('dns')}&pdt=${getQueryString('pdt')}&rrt=${getQueryString('rrt')}&tcp=${getQueryString('tcp')}&srt=${getQueryString('srt')}&dit=${getQueryString('dit')}&clt=${getQueryString('clt')}`

    const pvUrl = `https://www.google-analytics.com/collect?v=1&t=pageview&${pvData}&z=${getQueryString('z')}`;
    const perfUrl = `https://www.google-analytics.com/collect?v=1&t=timing&${pvData}&${perfData}&z=${getQueryString('z')}`

    await fetch(pvUrl, reqParameter);
    await fetch(perfUrl, reqParameter);
}

async function response(event) {
    const url = new URL(event.request.url);

    const getReqHeader = (key) => event.request.headers.get(key);

    const Referer = getReqHeader('Referer');
    const user_agent = getReqHeader('User-Agent');
    const ref_host = (() => {	
        try {
            return new URL(Referer).hostname;
        } catch (e) {
            return ""
        }
    })();

    let needBlock = false;

    needBlock = (!ref_host || ref_host === '' || !user_agent || !url.search.includes('ga=UA-')) ? true : false;

    if (typeof AllowedReferrer !== 'undefined' && AllowedReferrer !== null && AllowedReferrer) {
      let _AllowedReferrer = AllowedReferrer;

      if (!Array.isArray(AllowedReferrer)) _AllowedReferrer = [_AllowedReferrer];
    
      const rAllowedReferrer = new RegExp(_AllowedReferrer.join('|'), 'g');

      needBlock = (!rAllowedReferrer.test(ref_host)) ? true : false;
      console.log(_AllowedReferrer, rAllowedReferrer, ref_host);
    }

    if (needBlock) {
        return new Response('403 Forbidden', {
            headers: { 'Content-Type': 'text/html' },
            status: 403,
            statusText: 'Forbidden'
        });
    }

    const getCookie = (name) => {
        const pattern = new RegExp('(^| )' + name + '=([^;]*)(;|$)');
        const r = (getReqHeader('cookie') || '').match(pattern);
        return (r !== null) ? unescape(r[2]) : null;
    };

    const createUuid = () => {
        let s = [];
        const hexDigits = '0123456789abcdef';
        for (let i = 0; i < 36; i++) {
            s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
        }
        s[14] = '4'; // bits 12-15 of the time_hi_and_version field to 0010
        s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
        s[8] = s[13] = s[18] = s[23] = '-';

        return s.join('');
    };

    const _uuid = getCookie('uuid');
    const uuid = (_uuid) ? _uuid : createUuid();

    // To sent data to google analytics after response id finished
    event.waitUntil(senData(event, url, uuid, user_agent, Referer));

    // Return an 204 to speed up: No need to download a gif
    let response = new Response(null, {
        status: 204,
        statusText: 'No Content'
    });

    if (!_uuid) response.headers.set('Set-Cookie', `uuid=${uuid}; Expires=${new Date((new Date().getTime() + 365 * 86400 * 30 * 1000)).toGMTString()}; Path='/';`);

    return response
}

修改網站<head>位置,將Google Analytics程式碼換成:

<script>
window.ga_tid = "UA-XXXXX-Y"; // {String} The trackerID of your site.
window.ga_api = "https://example.com/xxx/"; // {String} The route of your cloudflare workers you just registered before.
</script>
<script src="https://cdn.jsdelivr.net/npm/cfga@1.0.1" async></script>

"UA-XXXXX-Y":Google Analytics的追蹤ID。
"https://example.com/xxx/":Cloudflare Workers新建好的網址,例如https://xxx-xxx-xxx.yourname.workers.dev/

以上步驟完成後,到Google Analytics網站的即時頁面檢查是否有變化。

參考網站: