自从 CNZZ 砍掉了免费服务,百度统计免费版除基础数据以外的功能也关闭了,51.la 刚重构的 v6 突然承接到这两家的「难民」,也饱受巨大的流量冲击。于是尝试过自建统计服务 Umami 来统计博客流量数据(使用 Vercel 部署 Umami,从零开始搭建一个免费的个人博客数据统计),但是,自己搭建的服务总归不如大厂的公共服务来的稳定,而且自建还需要占用资源,于是又开始研究 Google Analytics。
Google Analytics 是一款优秀的流量分析服务,集成方便,使用简单。
但是,众所周知,国内网络环境使用 Google Analytics 页面访问速度肉眼可见的变慢,同时还面临着各种广告屏蔽插件拦截,所以 Google Analytics 是一个很大的优化点。
本文就记录一下借助 Cloudflare Worker 实现 Google Analytics 反代加速的过程,顺便还能更换采集路由规避广告屏蔽插件的拦截。
强调一下,反代 Google Analytics 目前的问题是,Measurement Protocol 现在不能手动设置来源 IP,这意味着无法获取访客地理位置、运营商等信息. 不过如果在 Workers 使用如 lib-qqwry 去查询 IP 库后修改上报内容,或许不失为一种权宜之策。
Cloudflare 基础使用就不赘述了,在 Dashboard 里找到 Workers 开通就好. 创建服务那里,「启动器」选择初始模板,随便选即可。
新建的 Worker,代码如下,保存后部署。
addEventListener('fetch', (event) => {
// 这里可以加 filter
return event.respondWith(handleRequest(event));
});
// worker应用的路由地址,末尾不加 '/',比如:ga.wangtwothree.workers.dev
const DOMAIN = 'xxx.workers.dev';
// 插入的js地址文件名,可自定义
const JS_FILE = '自定义ga.js'
// 响应上报的接口路径,可自定义,规避广告屏蔽插件拦截
const COLLECT_PATH = 'collect_path';
// 原 gtag 地址,将G-XXX改为你的id
const JS_URL = 'https://www.googletagmanager.com/gtag/js?id=G-XXX'
// 下面不需要改
const G_DOMAIN = 'google-analytics.com';
const G_COLLECT_PATH = 'g\/collect';
async function handleRequest(event) {
const url = event.request.url;
if (url.match(`${DOMAIN}/${JS_FILE}`)) {
const requestJs = await (await fetch(JS_URL)).text();
const jsText = requestJs.replaceAll('\"www\"', '\"\"').replaceAll('.' + G_DOMAIN, DOMAIN).replaceAll(G_COLLECT_PATH, COLLECT_PATH);
return new Response(jsText, {
status: 200,
statusText: 'OK',
headers: {
'Content-Type': 'application/javascript',
},
});
} else if (url.match(`${DOMAIN}/${COLLECT_PATH}`)) {
const newReq = await readRequest(event.request);
event.waitUntil(fetch(newReq));
}
return new Response(null, {
status: 204,
statusText: 'No Content',
});
}
async function readRequest(request) {
const { url, headers } = request;
const body = await request.text();
const ga_url = url.replace(`${DOMAIN}/${COLLECT_PATH}`, `www.${G_DOMAIN}/${G_COLLECT_PATH}`);
const nq = {
method: 'POST',
headers: {
Host: 'www.google-analytics.com',
Origin: headers.get('origin'),
'Cache-Control': 'max-age=0',
'User-Agent': headers.get('user-agent'),
Accept: headers.get('accept'),
'Accept-Language': headers.get('accept-language'),
'Content-Type': headers.get('content-type') || 'text/plain',
Referer: headers.get('referer'),
},
body: body,
};
return new Request(ga_url, nq);
}
然后按照 Google Analytics 里的说明安装统计代码,只是把 JS 地址改成上面配置的地址。安装后查看下页面,在 F12 Devtools 里看下有没有 POST 到 COLLECT_PATH 的成功 204 响应。成功的话,就可以在 Google Analytics 的实时页面看到统计了。
页面插入对应 js 示例
<!-- Google tag (gtag.js) -->
<script async src="https://xxx.workers.dev/自定义ga.js"></script> // 对应worker里的DOMAIN 和 JS_FILE,需要保持一致
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXX'); // 将G-XXX改为你的id
</script>
接下来的可选操作是将自己的域名解析到 Worker.
如果域名是 NS 接入 Cloudflare 那就很简单了,假设域名是 example.com 想用 subdomain.example.com,若这个二级域名已经使用且经过 Cloudflare(开启了橙色云朵)即可下一步,没有的话需要设置这个二级域名为任意记录,并开启橙色云朵. 然后在 Workers 里「触发器-添加路由」添加 subdomain.example.com/* 即可.
如果域名是之前用 Partners 方法用 CNAME 接入的(现在 Partners 已经不能新增接入了),可以在 Partners 那边给二级域名开启橙色云朵,然后同样进行第二步操作. 其实还有一个方法,Cloudflare Pages 支持 CNAME 接入,可以创建一个空白 Pages,CNAME 到 yourpages.pages.dev,成功之后这个二级域名即接入了 Cloudflare 网络,那么还如前文所述一样操作即可.
当然,改完后不要忘记把 Workers 代码里的 DOMAIN 变量修改过来,还要更新页面安装的 JS 地址.
相比仅代理上报接口(Measurement Protocol),代理 gtag.js 的好处是不用自己维护这个文件的更新,且不用修改其他的代码(如使用 Google Analytics 做了自定义事件上报等等),甚至(或许可以,但未测试)支持 Tag Manager,功能更丰富. 当然缺点也显而易见,gtag.js 也会消耗一次请求,访问量较大时免费的请求数可能会吃紧。
通过性能分析,发现 gtag.js 文件过大,影响页面加载速度。
虽然使用了 Cloudfare 代理,但是 Google Analytics 原始的 js 文件为 80KB 左右。
搜索一番,找到一个瘦身版 Google Analytics:https://minimalanalytics.com/
Minimal Google Analytics Snippet A simple snippet for tracking page views on your website without having to add external libraries. Also works for single page applications made with the likes of react and vue.js.
Before Google Tag Manager + Analytics = 73kB
After Snippet = 1.5kB
使用瘦身版插入的 js 片断,只有 1.5kb 大小。
唯一的缺点就是只有基本统计功能,这对于我来说足够了。
将页面插入的 js 代码,更换为下述代码,需要将其中的 xxx.com/collect_path 改为第一步定义的 DOMAIN 和 COLLECT_PATH 变量值,并且将 G-XXXXXXXX 替换为你自己的 ID。
<script>
enScroll=!1,enFdl=!1,extCurrent=void 0,filename=void 0,targetText=void 0,splitOrigin=void 0;const lStor=localStorage,sStor=sessionStorage,doc=document,docEl=document.documentElement,docBody=document.body,docLoc=document.location,w=window,s=screen,nav=navigator||{},extensions=["pdf","xls","xlsx","doc","docx","txt","rtf","csv","exe","key","pps","ppt","pptx","7z","pkg","rar","gz","zip","avi","mov","mp4","mpe","mpeg","wmv","mid","midi","mp3","wav","wma"];function a(e,t,n,o){const j="G-XXXXXXXX",r=()=>Math.floor(Math.random()*1e9)+1,c=()=>Math.floor(Date.now()/1e3),F=()=>(sStor._p||(sStor._p=r()),sStor._p),E=()=>r()+"."+c(),_=()=>(lStor.cid_v4||(lStor.cid_v4=E()),lStor.cid_v4),m=lStor.getItem("cid_v4"),v=()=>m?void 0:enScroll==!0?void 0:"1",p=()=>(sStor.sid||(sStor.sid=c()),sStor.sid),O=()=>{if(!sStor._ss)return sStor._ss="1",sStor._ss;if(sStor.getItem("_ss")=="1")return void 0},a="1",g=()=>{if(sStor.sct)if(enScroll==!0)return sStor.sct;else x=+sStor.getItem("sct")+ +a,sStor.sct=x;else sStor.sct=a;return sStor.sct},i=docLoc.search,b=new URLSearchParams(i),h=["q","s","search","query","keyword"],y=h.some(e=>i.includes("&"+e+"=")||i.includes("?"+e+"=")),u=()=>y==!0?"view_search_results":enScroll==!0?"scroll":enFdl==!0?"file_download":"page_view",f=()=>enScroll==!0?"90":void 0,C=()=>{if(u()=="view_search_results"){for(let e of b)if(h.includes(e[0]))return e[1]}else return void 0},d=encodeURIComponent,k=e=>{let t=[];for(let n in e)e.hasOwnProperty(n)&&e[n]!==void 0&&t.push(d(n)+"="+d(e[n]));return t.join("&")},A=!1,S="https://xxx.com/collect_path",M=k({v:"2",tid:j,_p:F(),sr:(s.width*w.devicePixelRatio+"x"+s.height*w.devicePixelRatio).toString(),ul:(nav.language||void 0).toLowerCase(),cid:_(),_fv:v(),_s:"1",dl:docLoc.origin+docLoc.pathname+i,dt:doc.title||void 0,dr:doc.referrer||void 0,sid:p(),sct:g(),seg:"1",en:u(),"epn.percent_scrolled":f(),"ep.search_term":C(),"ep.file_extension":e||void 0,"ep.file_name":t||void 0,"ep.link_text":n||void 0,"ep.link_url":o||void 0,_ss:O(),_dbg:A?1:void 0}),l=S+"?"+M;if(nav.sendBeacon)nav.sendBeacon(l);else{let e=new XMLHttpRequest;e.open("POST",l,!0)}}a();function sPr(){return(docEl.scrollTop||docBody.scrollTop)/((docEl.scrollHeight||docBody.scrollHeight)-docEl.clientHeight)*100}doc.addEventListener("scroll",sEv,{passive:!0});function sEv(){const e=sPr();if(e<90)return;enScroll=!0,a(),doc.removeEventListener("scroll",sEv,{passive:!0}),enScroll=!1}document.addEventListener("DOMContentLoaded",function(){let e=document.getElementsByTagName("a");for(let t=0;t<e.length;t++)if(e[t].getAttribute("href")!=null){const n=e[t].getAttribute("href"),s=n.substring(n.lastIndexOf("/")+1),o=s.split(".").pop();(e[t].hasAttribute("download")||extensions.includes(o))&&e[t].addEventListener("click",fDl,{passive:!0})}});function fDl(e){enFdl=!0;const t=e.currentTarget.getAttribute("href"),n=t.substring(t.lastIndexOf("/")+1),s=n.split(".").pop(),o=n.replace("."+s,""),i=e.currentTarget.text,r=t.replace(docLoc.origin,"");a(s,o,i,r),enFdl=!1}
</script>
加速Google Analytics | 流动 https://liudon.com/posts/optimize-google-analytics/
Cloudflare Workers 反代 Google Analytics · xiaopc https://xiaopc.org/2022/04/04/%E6%97%A7%E6%96%87%E6%9B%B4%E6%96%B0-cloudflare-workers-%E5%8F%8D%E4%BB%A3-google-analytics/
Minimal Google Analytics Snippet | Minimal Analytics https://minimalanalytics.com/