无监控的网站分析
Google Analytics 实际上让您付出了什么
بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ
字号
行距
字体
您有一个网站。您知道上面发生了什么吗?
大多数人并不知道。这个网站之前也不知道。站点已经上线,页面在添加,文章在写,内容被翻译成五种语言。这一切都没有产生任何信号来表明是否有人在读。没有页面浏览数,也没有引荐数据,也没有办法判断首页是否在做它该做的事,或者访客是不是马上就跳出了。
显而易见的答案是 Google Analytics。加一个脚本标签,得到一个仪表盘。大多数网站正是这么做的。但 Google Analytics 并不是免费的。您用访客的数据来支付。Google 把每一次页面访问都和它已经知道的关于这个人的所有信息结合起来:搜索历史、YouTube 习惯、位置数据。您的网站成了一份从未经过访客同意而构建起来的画像中的又一个数据点。
还有 GDPR 的问题。如果您的业务面向欧盟的访客,Google Analytics 要求一个法律上合规的同意横幅。大多数实现都做错了。法律风险是真实存在的。
这个网站需要分析。但不是来自 Google 的。
术语说明
什么叫自托管?
运行在您所控制的服务器上的软件,而不是运行在第三方平台上。数据、配置和访问权都留在您这里。
什么是尊重隐私的网站分析?
一种统计和理解网站访客的方式,但不把他们识别为具体个人。没有跨站追踪,没有他们其他浏览行为的画像,没有任何第三方接触这些数据。您能看到自己网站上发生了什么;其他人看不到。
什么是私有网络?
一个只有成员才能看到的网络。网络外的设备根本不知道里面运行的服务存在,所以管理工具(比如分析仪表盘)可以在那里运行,完全不会出现在公共互联网上。
替代方案是什么样的
这个工具叫 Umami。
开源,采用 MIT 许可,注重隐私,无 cookies。它运行在您自己的服务器上。因沙安拉数据永远不会离开您的基础设施。不需要同意横幅,因为没有任何东西需要同意。没有 cookies,没有个人数据,没有跨站追踪。
它给您的是:页面浏览数、引荐来源、按浏览器和设备的分类、来源国家以及自定义事件。干净的仪表盘,实时的数据。因沙安拉一位企业主可以打开它,不需要培训就能理解发生了什么。
它不给您的是:个人访客画像、会话录制或热力图。它不识别具体的访客。它告诉您在您的网站上发生了什么。
Umami 已经在服务器上运行,处于一个私有 VPN 后面。有意思的决定是从那里开始的。
谁能看到这个仪表盘?
大多数自托管分析的指南都以「访问 your-domain.com 然后登录」结束。您的分析仪表盘就这样出现在了公共互联网上。任何人都能找到它。自动化扫描器会发现它。机器人会尝试默认凭据。
仪表盘是一个管理工具。它有登录页面、Web 应用和后端的数据库。每一项都是攻击面。如果您错过一个安全更新,仪表盘以及可能整个服务器都会暴露。
这个网站上的 Umami 实例并不存在于公共互联网。它运行在一个私有网络(NetBird)后面。
仪表盘只能从属于这个网络的设备访问。对其他所有人来说,没有登录页面,没有错误信息,没有任何回应。这个域名解析到一个只在 VPN 内部存在的 IP;从公共互联网根本没有东西可以连接。没什么可攻击的,因为没什么可找的。
这是一个远不止于网站分析的原则:最高形式的安全是一开始就不可见。
底层运行的是什么
分析服务以一个 Podman pod 中两个容器的方式运行:PostgreSQL 和 Next.js 应用,共享一个网络命名空间,使应用能在不暴露数据库的情况下访问它。所有进程都以一个专用的服务账号用户运行,没有 shell,与服务器其他服务一起被监控。Beszel 跟踪资源健康(CPU、RAM、磁盘、网络);Dozzle 处理日志(服务正在做什么、遇到了什么错误)。资源和日志回答的是不同的问题,而一个您在生产中信任的服务两者都需要。
但访客需要能访问它
如果分析服务对公共互联网是隐藏的,访客的浏览器又是怎样向它发送数据的?
它们并不直接和分析服务对话。它们和网站本身对话。Web 服务器(Caddy)充当代理,把两个特定路径在内部转发到分析服务。使用 Umami 文档中的默认值,大致是这样:
(umami_proxy) { handle /umami.js { reverse_proxy 127.0.0.1:8089 }
handle /api/send { reverse_proxy 127.0.0.1:8089 }}一个路径提供追踪脚本。另一个接收分析数据。两者都被引入到网站的 Caddy 块中:
example.com { import tls_config root * /var/www/example.com import umami_proxy import static_files}从访客的角度看,两次请求都发往与网站相同的域名,没有外部服务,也没有跨域请求。浏览器从网站加载一个脚本,然后把数据发回给网站。
分析服务在幕后处理一切,对访客完全不可见,也从外部完全无法到达。
对一家企业来说,在您和您的分析数据之间没有任何供应商。因沙安拉价格不会一夜之间改变,功能不会被废弃,产品也不会被收购后关闭。历史数据保留在您所控制的服务器上,可按您的条件查询和导出。
看不见的问题:广告拦截器
广告拦截器也会拦截尊重隐私的分析。
Umami 不使用 cookies。它不追踪个人。它不和任何人共享数据。但主流的拦截列表(EasyPrivacy、uBlock Origin、AdGuard)都包含专门针对 Umami 的规则。拦截器不评估伦理。它们匹配模式:
- 域名模式:子域名中包含
analytics、stats或tracking的任何东西 - 脚本名:由已知分析工具提供的
script.js - 端点模式:
/api/send在 Umami 的拦截列表条目中是被特别命中的
广告拦截器使用率的估计通常落在 10% 到 40% 的区间,在年轻和懂技术的受众中更高。这些访客永远不会出现在您的分析中。您将基于不完整的数据做决定,而且您并不知道数据是不完整的。
自托管解决了数据主权的问题。它没有解决广告拦截器的问题。解决办法必须再走一步。
让分析消失
广告拦截器问题的解决办法是概念性的,而不是技术性的。让分析请求看起来像普通的站点资源。脚本换一个名字,端点换一个路径,两者都和站点已经在加载的其他东西融为一体。拦截列表的模式就匹配不到了。
反向代理把一个公共路径映射到应用内部所期望的内容。浏览器看到的是一个普通的同源请求;分析服务在背后接收数据。
当配置在欺骗您
这一步花的时间最长。这里是「我跟着教程做了」和「我理解这个系统」之间区别显现的地方。
Umami 公开了两个用于重命名的环境变量。TRACKER_SCRIPT_NAME 控制服务器提供追踪脚本时使用的文件名。COLLECT_API_ENDPOINT 控制追踪器把数据 POST 到的路径。两者看起来都像普通的运行时配置。
TRACKER_SCRIPT_NAME 确实如此。脚本的文件名在服务器响应请求时决定。设置环境变量,重启,服务器就以新名字提供脚本。完成。
COLLECT_API_ENDPOINT 是陷阱。设置之后重启,服务端的路由是听话的。它在新路径接受请求。一切看起来都正确。但在浏览器里,追踪器仍然向 /api/send 发送数据。代理不知道这个路径,所以数据无处可去。仪表盘显示零访客。任何地方都没有错误。一切看起来都好。什么都不工作。
源代码解释了原因。追踪脚本是在构建 Docker 镜像时由 Rollup 拼起来的:
plugins: [ replace({ __COLLECT_API_ENDPOINT__: process.env.COLLECT_API_ENDPOINT || '/api/send', }),]端点作为字符串字面量被编译进 JavaScript 输出。在运行时,环境变量改变了服务器监听的位置,但浏览器执行的是把旧路径硬编码进去的预编译代码。
来自 ghcr.io/umami-software/umami 的官方 Docker 镜像在追踪脚本中硬编码了 /api/send。把 .env 文件中的 COLLECT_API_ENDPOINT 设为新值,会让服务器在新路径接受请求,但追踪器仍然告诉浏览器要发到 /api/send。两边在静默地相互矛盾。
解决办法是从源码构建镜像,把新值在编译时硬编码进去。Umami 的 Dockerfile 默认没有把这个变量暴露成 build 参数,但在 builder 阶段加两行就把这个缺口补上了:
ARG COLLECT_API_ENDPOINT=/api/sendENV COLLECT_API_ENDPOINT=$COLLECT_API_ENDPOINTARG 声明一个构建期变量;ENV 把它暴露给 Rollup,让这个值能被编译进追踪脚本。有了这两行,用自定义值进行构建只需要一个 flag:
podman build --build-arg COLLECT_API_ENDPOINT=<your-endpoint> -t umami-custom:latest .结果就是和官方镜像同样的镜像,只是某个 JavaScript 文件里的某个字符串不同。
如果主机以 rootless 模式运行 Podman,且使用一个独立的服务账号用户,那么在您的开发用户下构建的镜像对那个服务用户是不可见的。每个 rootless 用户都有一个隔离的镜像存储。传输只需要两条命令:
podman save localhost/umami-custom:latest -o /tmp/umami-custom.tarcd /tmp && sudo -u umami podman load -i /tmp/umami-custom.tarcd /tmp 是有意义的。sudo -u umami 默认继承调用者当前的工作目录。如果您的 shell 处在您开发用户的 home 里,加载就会失败,报 cannot chdir: Permission denied,因为服务用户没有权限读您的 home。错误指向 chdir,而不是 sudo 或 podman,所以第一次踩到时原因并不明显。
以上内容覆盖了一个自定义 Umami 构建大致是什么样的。这个网站把 <your-endpoint> 设成了什么、与之匹配的、由公共代理改写的脚本路径,这些不在本文里。
Astro 这一侧
追踪脚本需要在每一页都加载。在 Astro 中,这意味着把它加到共享的 layout 里:
<script is:inline defer src="/umami.js" data-website-id="..." data-host-url="/"></script>几点要注意。
is:inline 告诉 Astro 让这个 script 标签保持原样,不要处理或打包。追踪器是一个 fire-and-forget 的第三方脚本;它不需要 Astro 的打包流水线。
defer 在不阻塞页面渲染的情况下加载脚本。分析永远不应该让页面变慢。
Astro 的参考文档加了一段说明,读起来比它的实际范围更宽:
Will not be bundled into an external file. This means that attributes like
deferwhich control the loading of an external file will have no effect.
这段说的是 inline 内容的情况,那种情况下没有外部文件可以延迟加载。对于一个带 src 的 script,is:inline 只是告诉 Astro 不要处理这个标签。dist 输出会原样发出它,defer 完整保留,浏览器会照常应用这个属性。
data-host-url="/" 覆盖了追踪器在构建端点时使用的基地址。没有它,追踪器会从脚本自身的位置推断基地址。一个位于子目录路径(比如 /path/to/umami.js)的脚本会向 /path/to/api/... 发送数据,而不是 /api/...。把这个值设为 /(或站点根,无论是什么)就能保证它正确。
这个网站还有一个独立的 404.html,它在 Astro 流水线之外(为了绕开框架的一个路由 bug,在另一篇文章中有记录)。
由于它不走共享 layout,所以同样的 script 标签是手动加进去的。
为什么具体方案保持私密
概念上的解决办法已经命名了:让脚本和端点看起来像普通的站点资源。这个网站就是这么做的。具体的名字,以及把名字选择和一个网站的内容画像绑在一起的推理,不在本文里。
运行这一切的成本是真实的。上游镜像可以用 podman pull 更新。一个自定义构建意味着每次上游发布都要从源码重新构建。
广告拦截器存在,是因为追踪器招来了不信任。公共互联网上的大多数分析其实就是监控:第三方脚本、指纹识别、向下游传递数据。拦截器无法分辨哪些网站是诚实的。它们拦截的是模式。
一种基于模式匹配的反制手段会同等程度地帮助诚实和不诚实的网站。一篇带可用配方的公开文章,等于把同一把钥匙递给一位数自己预约的牙医,也递给一家想把它的像素塞回某人屏幕的追踪公司。第一种用法没问题。第二种用法正是让拦截器变得必要的原因。
所以这篇文章止步于原则。
完整的设置、具体的名字以及决定它们的推理,出于上述原因不会进入公开文档。如果这与您的情况相关,关于 页面是开始的地方。
页面速度怎么办?
一个快速的网站能留住访客。慢的页面会让人跳出、在搜索排名中下降、显得不专业。分析必须和这个成本相权衡。
对于沉重的分析脚本,有一个已知的解决办法:Partytown。它把脚本移到一个后台线程,让主页面在追踪器干活的时候保持响应。对于加载像 Google Tag Manager 或 Facebook Pixel 这样的脚本(50 到 200 KB 的 JavaScript 在持续轮询和注入)的网站,Partytown 是真正的速度收益。
这个方案不需要它。
Umami 追踪器未压缩 2.63 KB,gzip 后 1.43 KB。它以 defer 加载,在页面被访问时触发一次 POST 请求,然后退出。
加上 Partytown 实际上会让事情变慢。它的运行时大约 15 KB(gzip 后),全部都加载进访客的浏览器:1.2 KB 的 loader 加上 13.6 KB 的 worker,后者在后台拦截请求。这些都不在服务器上运行。把一个 1.43 KB 的脚本包在 15 KB 的额外开销里,相当于脚本本身体积的十倍。
诚实的答案是更简单的那个。一个足够小的追踪器,小到没有任何东西在拖慢页面,所以页面就快。
归根结底
这个方案的每一块都解决一个具体的问题。Umami 在不带监控的前提下替代 Google Analytics。私有网络让仪表盘待在公共互联网之外。Caddy 代理让访客通过网站本身访问到分析。广告拦截器问题有一个解决办法,本文指向了它而没有铺开。每一个决定单独看都很小。
放在一起,它们决定两件事:您是否知道您的网站上发生了什么,以及您和访客之间的关系是否仍然属于您。
因沙安拉这值得做对。
我需要广告拦截器的绕开方案,还是只要自托管就够了?
仅自托管就能解决数据主权的问题。广告拦截器问题与之独立:使用默认端点时,仍有一定比例的访客不会出现在您的数字中。
如果您的唯一目标是停止向 Google 提交数据,那么默认安装的 Umami 就足够了。如果您希望访客数能够反映现实,那么通过代理重命名的方法才能弥合这个差距。
和 Google Analytics 相比,这个方案的成本如何?
服务器成本,不是订阅费用。占用的资源是一个数据库和一个小型的 Node.js 应用,与您已经在托管的其他服务一起运行。没有按访问、按事件或按功能的定价。模式是:您为本来就需要的服务器付费。
Umami 开箱即用是否符合 GDPR?
比 Google Analytics 更接近,但不是自动的。Umami 不使用 cookies,也不与第三方共享数据,这消除了最常见的合规失败。
有些部分仍然需要您关注:您的隐私政策、您的服务器所在的国家,以及您是否存储 IP 地址。
我可以迁移我的 Google Analytics 历史数据吗?
通常不行。Umami 从安装的那一刻开始收集数据;GA 的历史数据停留在 Google 的产品内部。有些人会导出 GA 报告以供归档参考,但这些指标无法干净地与 Umami 后续的数据合并。
设置 1 / 1
返回 设置航空航天工程师
公开透明的道德创业者
你专注你的事业
我负责数字化的部分