ChrAlpha's Blog

Thumbnail-%E8%87%AA%E5%BB%BA%20Umami%20%E7%BB%9F%E8%AE%A1%E7%9A%84%E6%8E%92%E9%9B%B7%E8%AE%B0%E5%BD%95%E2%80%94%E2%80%94%E6%9C%AC%E5%9C%B0%E4%BE%9D%E8%B5%96%E5%BA%93%E4%B8%8E%20Cloudflare%20Workers%20%E5%8F%8D%E4%BB%A3

自建 Umami 统计的排雷记录——本地依赖库与 Cloudflare Workers 反代

2022-06-26·笔记本

Google 已经 宣布 于 2023 年 7 月 1 日起,Google Analytics 中的标准 Universal Analytics 媒体资源将停止命中数据,并敦促用户尽快切换至 Google Analytics 4 媒体资源。本来我对 Google Analytics 已经颇有不满,操控台臃肿笨拙不说,其收集方式甚至可能违背某些比较严格的条例,况且相当部分数据我并不需要。终于借着这次迁移的机会,不如把整个 Google Analytics 给换成 Umami 算了。

移除 Google Analytics 不影响 SEO

Google 又单独推通知,告知 Google Analytics 4 媒体资源可以绑定 Google Search Console 并建议绑定。我想会有读者和我冒出过同样的疑问——移除 Google Analytics 会影响网站在 Google 的权重、排名吗?

有这个想法还真不奇怪。好几年前,我刚开始接触建站时,当时顺手引入 Google Analytics 而没有管 Google Search Console,后来无聊敲关键词搜索便惊奇地发现 Google 已经收录网站。一直以来,Google Analytics 助力统计我的大部分网站。从一开始每天几个 PV,到后来几十、几百 PV,甚至还有一段时间上千(不过现在掉下来,写文章频率下降以及不推广)。中途不乏为能够更好地使用 Google Analytics 而 尝试优化

不过,天下没有不散的宴席。随着认识加深,我对 Google Analytics 的态度也发生了微妙的变化,终于借此契机彻底翻转。最初是 Google Analytics 最先将我的网站引入收录的,自然也会担心移除 Google Analytics 会不会影响 SEO。

这种担心是多余的,正如这节的子标题——「移除 Google Analytics 不影响 SEO」。本来 Google 收录、排名网站就属于黑盒操作,倘若公开很可能遭到有心人刻意刷排名。因此这里我们只能参考 Google 官方人员的说辞。

“Google Analytics is not used in search quality in any way for our rankings”.

Google Search Central - Is Google Analytics data a factor in a page’s ranking?

Just to be clear: there’s no penalty for using or not using GA with regards to search.

JohnMu (Search Advocate at Google)

尽管我们仍然可以保留怀疑,毕竟跳出率、平均访问时间确实是评判页面质量很值得参考的指标。但既然 Google 已经在多处明确表态,还是不要以最大的恶意揣摩它。

选择新网站统计工具

虽说本文只是排雷记录,但还是多废话几句选择 Umami 的缘由。

我的网站大多是搭建在 Cloudflare 上的,而 Cloudflare 在托管 DNS 时就已经会自己统计域名的访问量。只是这个统计不会排除各类非人类访问,导致其数据往往是 Google Analytics 十倍以上。

cloudflare dns analytics

而 Cloudflare 也有专门的 Cloudflare Web Analytics,这个倒是更像那么回事。然而,它少了两个我认为非常重要的指标——访问之间与跳出率,前者体现我的内容是否值得用户耐心下来慢慢揣摩,后者体现用户是否愿意在看完一篇内容后继续浏览更多。

cloudflare web analytics

又鉴于 Cloudflare 的影响力其 Web Analytics 被各大防跟踪屏蔽器拦截也是时常有之。如果你能接受上面两个指标的缺失、与屏蔽器导致的数据偏离,那么 Cloudflare Web Analytics 不失为一种优雅的选择,还是没必要去额外折腾。

既然我在意这两个数据,自然会去关注其它网站数据统计工具。其实也没费什么功夫,因为关注的博主也有几个正是使用 Umami。Umami 官网自己介绍的 free/fast/open-source 确实击中我的需求,可怜的访问量让我不甘额外为统计掏腰包,稳定迅速的加载也避免影响用户浏览体验,开源更是让这个工具共建共享。当然,还有其宣称的 beautiful,不可谓不是决定性的()。

依赖库在本地

首先声明,我尚且没有接触过 PaaS 服务,最多也就是租个 VPS 搓搓开发这样子。以下内容可能对于你来说过于常识,但是我相信相当一部分用户应该和我一样并不十分了解这类服务,所以记录下这个或许可以被称之为的「坑」。

Umami 文档 - Hosting 部分有提到,”All you need is a database (either MySQL or Postgresql) and server that can run Node.js (10.13 or newer).”。显然我没理解好这个 “need”,为后面出问题埋下了伏笔。

而被放在 GitHub Readme 页面位置的 Railway 应该是最简单的部署方式,只需要点击一个按钮基本就能完成数据库的新建与网站的部署。

但是到数据库初始化的时候,问题便暴露了。根据我朴素的理解,毕竟服务都是跑在 Railway 上的,那么这些依赖库也应安在 Railway 上了。然后就……

railway run psql -h hostname -U username -d databasename -f sql/schema.postgresql.sql

bash: psql: command not found

一开始我以为这是 Railway 没有配置好依赖。直到我看到 另一篇介绍 Umami 安装的文章 中提到「将 libpg 的 bin 路径添加到 PATH」,我才惊觉原来依赖需要安装到本地的?!

所以老老实实上 PostgreSQL 官网 下载安装并 添加 bin 路径到 PATH 中,Windows 默认大概率在 c/Program Files/PostgreSQL 位置。Linux/macOS 用户直接借助 homebrew 就能很好的管理 PostgreSQL。

安装 PostgreSQL 并添加 bin 路径完成后,可以在终端中键入 psql --help 测试。若正常,剩下的按照 Umami - Running on Railway 的步骤来基本都能很顺利。倘若在连接数据库的时候出现断联的,注意检查下广告屏蔽器是否误杀以及代理是否正常。

反向代理避开广告屏蔽器

Umami - FeaturesUmami - FAQ 中或都提到「专注隐私」「不收集 Cookie」,不为用户画像单纯只是收集页面情况的 Umami,私以为并不需要担心隐私方面的问题——我只关注我的页面访问情况,不关心用户究竟是谁也无意窥探他们隐私。但是许多广告屏蔽器不管,诸如 uBlock Origin 就阻拦了 Umami 脚本。

受到 Sukka 的 cloudflare-workers-async-google-analytics 项目启发,我就想着能否借助 Cloudflare Workers 实现一层反向代理避开某些过分的屏蔽规则。显然是成立的,而且在 Umami 项目 GitHub Discussions 里已经有人给出 代码样例,我就不再造轮子了。

const scriptName = '/whatever.js';
const endpoint = '/foo/bar';
const umamiUrl = 'https://your.umami.url';

const corsHeaders = {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
    'Access-Control-Max-Age': '86400',
};

const scriptWithoutExtension = scriptName.replace('.js', '');

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

async function handler(event) {
    const pathname = new URL(event.request.url).pathname;
    const [baseUri, ...extensions] = pathname.split('.');

    if (baseUri.endsWith(scriptWithoutExtension)) {
        return getScript(event, extensions);
    } else if (pathname.endsWith(endpoint)) {
        return postData(event);
    }
    return new Response(null, { status: 404 });
}

async function getScript(event, extensions) {
    let response = await caches.default.match(event.request);
    if (!response) {
        response = await fetch(umamiUrl + '/umami.js');
        var js = await response.text();

        js = js.replace('/api/collect', endpoint);
        response = new Response(js, {
            headers: {
                ...response.headers,
                ...corsHeaders,
                'Access-Control-Allow-Headers': response.headers.get('Access-Control-Request-Headers'),
            },
        });

        event.waitUntil(caches.default.put(event.request, response.clone()));
    }

    return response;
}

async function postData(event) {
    const request = new Request(event.request);
    request.headers.delete('cookie');
    response = await fetch(umamiUrl + '/api/collect', request);
    var js = await response.text();
    response = new Response(js, {
        headers: {
            ...response.headers,
            ...corsHeaders,
            'Access-Control-Allow-Headers': request.headers.get('Access-Control-Request-Headers'),
        },
    });
    return response;
}

代码有删改。

假如只是使用,只需要修改前三行的参数即可。然后将网页插入的 Script 链接进行如下修改:

- <script async defer data-website-id="xxx-Xxxx" src="https://your.umami.url/umami.js"></script>
+ <script async defer data-website-id="xxx-Xxxx" src="https://your.cf-workers.url/whatever.js"></script>

对于域名托管于 Cloudflare DNS 且开启了 Cloudflare CDN 的用户,我更推荐直接设置一个路由直接将该网站下的一个路径绑定到 Worker 上,少一次 DNS 解析,且 localhost 测试将不会发送数据。举例如下(假设该网站网址为 my.website.url)。

<script async defer data-website-id="xxx-Xxxx" src="/whatever.js"></script>
cloudflare workers route

更进一步,你可以将 /whatever.js/foo/bar,写成 vue.jsjquery.js,看那些屏蔽器还敢不敢屏蔽!不过记得上面的 Route、插入 Script 的链接、Cloudflare Workers 代码都要相应更改。

当然,我相信还是有读者希望了解避开屏蔽的具体实现。我们不妨先换位思一下,屏蔽器该如何判断 Umami 并实施拦截?

  • 通过请求链接名 umami.js 判断
  • 通过向 /app/collect 发送数据的行为判断

关于第一点,利用 Cloudflare Workers 监测 /whatever.js 的请求并返回原本 umami.js 的内容(getScript 函数),这样屏蔽器只以为你请求 /whatever.js 不知道你实际上请求 umami.js。至于第二点,既然已经是手动获取 umami.js 那不妨再在其中动点手脚,将原本要传给 /app/collect 的内容先传给另一个 end point 再由 Cloudflare Workers 处理转交给 /app/collectpostData 函数),这样只运行在本地的屏蔽器便难以判断 Umami 了。

此外,Umami 也有可以手动提交数据的 API,不过不再举例。

本文并没有详细讨论 Umami 的部署、使用方法,这部分不论中文还是英文都有丰富文章,便不再赘述。这次主要着眼于两个比较影响 Umami 部署、使用体验的细节,而仅此已经用去三、四千字,还望能够使读者踏上 Umami 之路更为顺心、不枉浪费在阅读的时间。

自建 Umami 统计的排雷记录——本地依赖库与 Cloudflare Workers 反代
本文作者
ChrAlpha
发布日期
2022-06-26
更新日期
2022-06-26
转载或引用本文时请遵守 CC BY-NC-SA 4.0 许可协议,注明出处、不得用于商业用途!
CC BY-NC-SA 4.0