无追踪流量统计实战:Cloudflare Web Analytics 与自建 Umami
不用 Google Analytics,用 Cloudflare Web Analytics 和自建 Umami 获得不收集隐私、不触发 Cookie 弹窗的访问统计。

为什么换掉 Google Analytics
个人博客想看访问量,默认答案往往是 Google Analytics(GA)。但用了一段时间后,我决定把它撤掉,原因有三个:
- 隐私合规压力:GA 默认收集 IP、设备指纹、跨站行为,并向客户端写入 Cookie。在 GDPR、个保法等框架下,这意味着你 legally 需要向访客展示「本网站使用 Cookie」的同意弹窗,还要维护一份隐私政策。对一个静态博客来说,这完全是自找麻烦。
- Cookie 弹窗伤害体验:读者打开一篇技术文章,先被一个占屏三分之一的横幅拦住——这和博客「内容优先」的气质背道而驰。
- 加载开销不小:GA 的
gtag.js加上后续的数据上报,会让 Lighthouse 的 Performance 分数掉好几分。而 Astro 静态站本来引以为傲的就是「零 JS 运行时」,为了一个统计脚本破功很不值。
替代方案的思路是:只收集聚合的匿名数据,不碰 Cookie,不做跨站追踪。这样既不触发 Cookie 同意要求,也对读者更诚实。本文介绍两个我实际用过的方案:零维护的 Cloudflare Web Analytics,和数据完全自有的自建 Umami。
两个方案可以同时启用,也可以只用一个。本博客(Astro)已经内置了接入代码,只需要填入配置即可,下文会讲到。
方案一:Cloudflare Web Analytics
如果你的站点已经托管在 Cloudflare(或者域名 DNS 在 Cloudflare),这是成本最低的方案:免费、无 Cookie、服务端在边缘节点统计,几乎零维护。
开启步骤
- 登录 Cloudflare Dashboard,在左侧导航找到 Analytics & Logs → Web Analytics(旧版界面直接叫 Web Analytics)。
- 点击 Add a site,输入你的站点主机名(如
blog.example.com)。 - 如果域名没有接入 Cloudflare 代理,页面会提示你复制一段 JS beacon 代码——关键是里面的 token(一串 32 位十六进制字符串)。
- 如果域名已在 Cloudflare 托管,也可以直接在站点下开启自动注入,但手动拿 token 的方式对任何托管平台都通用。
本博客的接入方式
本博客已经在 src/consts.ts 里预留了配置项:
// 无追踪流量统计(留空则不注入任何脚本,二者可独立启用)
export const ANALYTICS = {
cloudflareToken: '',
umamiScriptUrl: '',
umamiWebsiteId: '',
};
把拿到的 token 填进 cloudflareToken 即可。注入逻辑在 src/components/BaseHead.astro 中,是条件渲染——留空就不会输出任何脚本:
{
ANALYTICS.cloudflareToken && (
<script
defer
src="https://static.cloudflareinsights.com/beacon.min.js"
data-cf-beacon={JSON.stringify({ token: ANALYTICS.cloudflareToken })}
/>
)
}
defer 保证脚本不阻塞首屏渲染;BaseHead 被所有页面共用,所以填一次全站生效,不需要逐页修改。
方案二:自建 Umami
如果你想要更完整的功能(实时访客、事件追踪、UTM 分析、自定义报表),或者不想把数据交给任何第三方,可以自建 Umami——一个开源(MIT)、专注隐私的统计工具。它不写 Cookie、匿名化 IP,同样不需要弹窗。
docker-compose 部署
Umami 官方提供了镜像,配合 PostgreSQL 一个 compose 文件就能跑起来:
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
ports:
- '3000:3000'
environment:
DATABASE_URL: postgresql://umami:umami@db:5432/umami
DATABASE_TYPE: postgresql
APP_SECRET: replace-me-with-a-random-string
depends_on:
db:
condition: service_healthy
restart: always
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: umami
volumes:
- umami-db-data:/var/lib/postgresql/data
restart: always
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']
interval: 5s
timeout: 5s
retries: 5
volumes:
umami-db-data:
启动:
docker compose up -d
docker compose logs -f umami # 确认数据库迁移完成、服务监听 3000
注意:
APP_SECRET一定要换成随机长字符串(openssl rand -hex 32),生产环境的POSTGRES_PASSWORD也不要用示例里的弱口令。
Nginx 反代 + TLS
不建议把 3000 端口直接暴露到公网,用 Nginx 反代并挂上 Let’s Encrypt 证书:
server {
listen 443 ssl;
server_name stats.example.com;
ssl_certificate /etc/letsencrypt/live/stats.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/stats.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
证书用 certbot --nginx -d stats.example.com 一条命令签发即可。若服务器在 Cloudflare 后面,也可以用 Origin Certificate。
创建网站,拿 website-id
- 浏览器打开
https://stats.example.com,用默认账号admin/umami登录,第一时间改密码。 - 进入 设置 → 网站 → 添加网站,填名称和域名(域名只是标识,不影响统计范围)。
- 在网站的 Tracking code 标签页,能看到自动生成的脚本,形如:
<script defer src="https://stats.example.com/script.js" data-website-id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"></script>
把 src 和 data-website-id 分别填入 src/consts.ts:
export const ANALYTICS = {
cloudflareToken: '',
umamiScriptUrl: 'https://stats.example.com/script.js',
umamiWebsiteId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
};
BaseHead.astro 里对应的条件注入如下,两个字段都非空才输出脚本:
{
ANALYTICS.umamiScriptUrl && ANALYTICS.umamiWebsiteId && (
<script defer src={ANALYTICS.umamiScriptUrl} data-website-id={ANALYTICS.umamiWebsiteId} />
)
}
两个方案对比
| 维度 | Cloudflare Web Analytics | 自建 Umami |
|---|---|---|
| 部署成本 | 零,dashboard 点几下 | 一台 VPS + Docker + 证书维护 |
| 数据归属 | Cloudflare(仅聚合数据) | 完全自有,存在自己的 Postgres |
| 功能 | 访问量、来源、页面、设备等基础指标 | 实时访客、事件追踪、UTM、漏斗、多站点 |
| 脚本体积 | beacon.min.js,约几 KB | script.js,约 2 KB |
| 隐私合规 | 无 Cookie,不收集个人数据 | 无 Cookie,IP 匿名化,可完全自控 |
| 适用人群 | 只想看个大概流量、不想运维 | 想要完整功能、愿意自己运维一台小机器 |
我的建议是:纯展示型博客用 Cloudflare 就够了;如果你还运营多个站点、想看事件级数据,再上 Umami。两者字段独立,同时填也不冲突。
验证
配置生效与否,不要靠猜,部署后按下面几步确认。
确认脚本已注入
重新构建部署后,打开博客任意页面,DevTools → Elements 面板搜索 beacon 或 script.js,应该能在 <head> 里看到对应的 <script defer ...> 标签。也可以直接 curl:
curl -s https://blog.example.com/ | grep -E 'beacon|website-id'
如果什么都没匹配到,先检查 src/consts.ts 里的字段是否真的非空——条件注入意味着空字符串会被静默跳过,这是最常见的坑。
确认数据上报
- Cloudflare:DevTools → Network 面板刷新页面,过滤
cloudflareinsights.com,应看到对beacon.min.js的请求和后续上报。Dashboard 的 Web Analytics 面板通常几分钟内出数据,最长可能有小时级延迟,别急着判定失败。 - Umami:Network 面板过滤你的统计域名,应看到对
/api/send的 POST 请求返回 200。然后登录 Umami 面板,切到 实时(Realtime) 视图,自己访问一下博客,几秒内就能看到这次访问。
常见坑
- AdBlock 拦截:uBlock Origin 等插件默认拦截 Umami 和 Cloudflare beacon。自己测试时记得先关掉拦截器,否则永远看不到请求——这也意味着你的真实统计会系统性偏低 10%–30%,要有心理预期。
- CSP 拦脚本:如果站点配置了 Content-Security-Policy,记得把
static.cloudflareinsights.com和你的 Umami 域名加进script-src,上报接口加进connect-src。 - Umami 里看不到自己:Umami 面板登录后默认不统计管理员自己的访问行为之外,早期版本还依赖
data-domains等属性;如果脚本注入了但没数据,优先看/api/send的响应内容,而不是面板。 - docker-compose 版本:
depends_on的condition: service_healthy需要较新的 Compose 插件,老版本docker-compose1.x 会报错,升级或用docker compose(V2)命令。
至此,你的博客就有了一套不写 Cookie、不弹窗、读者友好的访问统计。数据够了就看,不看也不欠谁——这才是个人网站该有的样子。
$ giscus --load# 使用 GitHub 账号登录即可发表评论