访问统计

配置第一方访问统计、事件采集、转化漏斗、Web Vitals、数据保留和第三方统计服务。

Saavo 内置了一套面向单个站点的第一方访问统计功能。前台脚本负责采集页面浏览、自定义事件和 Web Vitals,数据经过 Cloudflare Queue 写入独立的 D1 数据库,管理员可在 Dashboard 中查看流量、事件、会话、转化漏斗和页面性能。

项目也支持 Google Analytics、Microsoft Clarity、Umami 等第三方统计服务。第一方统计与第三方脚本彼此独立:第一方统计由 config/deploy.ts 控制,第三方脚本在 config/analytics.ts 中配置。

这套功能只服务于当前部署的站点,不是多站点统计平台。这套访问统计定位是轻量级的 SaaS 网站前期使用(前 100 天),后期可以考虑迁移到第三方统计服务。

已有功能

功能入口或资源当前默认值
浏览器采集脚本/js/analytics.js自动记录页面浏览和 SPA 路由变化
第一方采集接口POST /api/analytics/collect已开启
异步写入队列analytics-events / ANALYTICS_QUEUE已配置生产者和消费者
统计数据库ANALYTICS_DB与业务数据库 DB 分开
统计后台/dashboard/analytics仅管理员可访问
数据清理Cron 30 */6 * * *每 6 小时执行一次
第三方统计脚本config/analytics.ts所需 ID 均为空,不会加载

统计后台包含以下页面:

  • 总览:页面浏览量、访客数、访问次数、跳出率、平均访问时长,以及页面、来源、设备和地区分布。
  • 事件:自定义事件趋势、事件属性和最近活动。
  • 会话:浏览器会话、访问记录、页面与事件时间线,以及 identify() 写入的属性。
  • 转化漏斗:按配置中的步骤和时间窗口计算进入人数、完成人数、转化率和流失情况。
  • 性能:真实用户的 LCP、INP、CLS、FCP、TTFB 和页面停留时长,并提供 P75、趋势和分组统计。

数据写入流程

一次正常的浏览器采集会经过以下流程:

  1. 页面渲染时检查统计总开关、站点状态、访问域名和忽略路径,符合条件才加载 /js/analytics.js。
  2. 浏览器脚本采集页面浏览、自定义事件或性能指标,并发送到 POST /api/analytics/collect。
  3. 采集接口校验站点 ID、访问划分策略、来源域名、忽略规则、机器人流量和请求频率。
  4. 校验通过后,事件先写入 analytics-events 队列,再由消费者写入 ANALYTICS_DB。
  5. Dashboard 直接查询保留期内的统计数据,因此队列尚未消费时,后台不会立即出现刚上报的事件。

采集接口成功接收消息后返回 202。队列写入数据库失败时最多尝试 3 次,最后一次仍然失败会记录错误并放弃该消息,当前项目没有为统计队列配置死信队列。

默认配置

第一方统计配置位于 config/deploy.ts 的 analytics 段。当前主要配置如下:

analytics: {
    enabled: true,

    retention: {
        enabled: true,
        rawDays: 100,
        eventBatchSize: 5_000,
        sessionBatchSize: 1_000,
        maxD1OperationsPerRun: 40,
        maxRuntime: '2m',
    },

    site: {
        id: '00000000-0000-4000-8000-000000000001',
        status: 'active',
        allowedDomains: [],
        visitStrategy: 'umami',

        tracker: {
            performance: true,
            respectDoNotTrack: true,
            excludeSearch: false,
            excludeHash: true,
        },

        collection: {
            ignoredPathPrefixes: ['/dashboard'],
            ignoredIps: [],
            ignoredUserAgentSubstrings: [],
            ignoredDistinctIds: [],
        },
    },

    rateLimit: {
        enabled: true,
        maxRequests: 120,
        windowDuration: '1m',
    },
}

这些默认值表示:

  • /dashboard 及其子路径不会加载第一方统计脚本,也不会被采集接口接受。
  • 保留 URL 查询参数,但移除 URL hash。
  • 采集 Web Vitals 和页面停留时长,并尊重浏览器的 Do Not Track 设置。
  • 每个站点、每个可信客户端 IP 在 1 分钟内最多提交 120 次采集请求。
  • 原始事件保留当前 UTC 日以及此前 100 个完整 UTC 日。rawDays 不会直接删除已识别会话及其 Trait(会话属性),清理任务只会额外回收已经没有事件引用的匿名会话。

必须修改的配置

上线前需要确认站点 ID、站点地址和 Cloudflare 资源都属于当前项目。

  1. 为 site.id 生成一个新的 UUID。
  2. 确认 VITE_SITE_URL 是当前环境的站点地址。统计服务会自动允许该地址的主机名,本地站点会自动允许 localhost 和 127.0.0.1。只有需要接受额外域名时,才在 allowedDomains 中补充,默认保持空数组即可。
  3. 本地环境由 saavo:init 初始化 ANALYTICS_DB,首次远程部署时,由 deploy:init 创建并绑定数据库和 analytics-events Queue。
  4. 后续数据库变更写入 schema/migrations,再通过 db:migrate:local 或部署脚本应用,不要把初始化文件当作增量迁移重复执行。具体流程见本地运行。
  5. 将 analytics.site.dashboard.funnels 中的示例漏斗改为产品真实的转化路径。

当前项目预置了四个漏斗:联盟推荐转化、注册转化、价格页到创建 Checkout、登录后继续 Checkout。这些漏斗只是样板,步骤、事件名和属性过滤都来自当前模板业务,不应原样用于其他产品。

如何划分访问次数

visitStrategy 决定一次访问如何划分。当前值为 umami,也支持 sliding。站点开始产生正式数据后,不要直接修改这个值,否则同一批数据会混用不同的统计口径。确需切换时,应使用新的站点 ID,或者先完成旧数据的清理或迁移。

数据保留期

原始事件清理由 30 */6 * * * 触发。任务按批次删除过期事件和无事件引用的匿名会话,并受单次 D1 操作数和运行时间限制。一次没有清完时,后续 Cron 会继续执行。如果清理速度长期低于新增数据速度,系统会写入警告和告警日志。

wrangler.jsonc 中的 Cron 字符串必须与 src/entry/index.ts 的 ANALYTICS_RETENTION_CRON 完全一致,否则定时入口不会执行对应任务。

记录业务事件

浏览器事件

浏览器端使用 trackBrowserEvent():

import { trackBrowserEvent } from '@/libs/analytics/client';

trackBrowserEvent('login_modal_opened', {
    reason: 'checkout',
});

浏览器事件名可以由业务自行定义。建议集中维护命名和属性规范,避免同一行为出现多套名称。统计脚本未加载、被用户禁用或请求失败时,trackBrowserEvent() 会直接跳过,不会影响原来的业务操作。

也可以通过 HTML 属性记录点击事件:

<button
  data-saavo-event="checkout_started"
  data-saavo-event-plan="pro"
>
  立即购买
</button>

服务端事件

需要由服务端确认的业务结果使用 trackRequestEvent():

import { trackRequestEvent } from '@/core/services/analytics/service';

trackRequestEvent(c, {
    name: 'checkout_session_created',
    data: {
        productId,
        planId,
        planType: plan.type,
        provider: 'stripe',
    },
});

当前服务端只接受以下事件:

  • signup_completed
  • login_completed
  • checkout_session_created
  • affiliate_referral_clicked

新增服务端事件时,必须扩展 AnalyticsRequestEvent 类型并明确事件属性,不能直接传入任意字符串。服务端事件通过 waitUntil() 异步提交,统计失败只记日志,不会把已经成功的注册、登录或 Checkout 请求改成失败。

项目中的 getService() 和 postService() 会自动附带当前统计会话的签名请求头,使后续服务端事件能够沿用浏览器侧的 Session(会话)和 Visit(访问)。业务代码不要自行生成或解析 x-saavo-* 请求头。

关联会话

需要把匿名会话与内部用户标识关联时,可以使用:

window.saavo.identify('internal-user-id', {
    plan: 'pro',
});

请使用内部 ID 或不可逆哈希,不要上报邮箱、手机号、姓名等非必要个人信息。identify() 保存的是会话属性的最新状态,不是属性变更历史。需要保留当时状态时,应把属性写入对应事件。

排除不需要统计的流量

当前项目提供四种配置级排除条件:

  • ignoredPathPrefixes:按路径前缀排除,默认包含 /dashboard。
  • ignoredIps:按可信客户端 IP 精确排除。
  • ignoredUserAgentSubstrings:按 User-Agent 子串排除,不区分大小写。
  • ignoredDistinctIds:按浏览器提供的 distinct ID(访客标识)精确排除。

这些规则在服务端执行,修改后需要重新部署。

管理员也可以在 Dashboard 的用户菜单中选择 Exclude this browser,把 saavo.disabled=1 写入当前浏览器的 localStorage。该设置只影响这个浏览器,不会按账号或 IP 排除其他设备。恢复时选择 Resume browser analytics,或者删除该存储项。

管理员排除浏览器统计

接入第三方统计服务

config/analytics.ts 当前支持:

  • Google Analytics
  • Microsoft Clarity
  • Cloudflare Web Analytics
  • Umami
  • Plausible
  • PostHog
  • Seline
  • Ahrefs Web Analytics
  • DataFast
  • OpenPanel

所有提供商的必填 ID、密钥或脚本地址默认都是空值,因此不会加载任何第三方统计脚本。使用某个提供商时,应填写它所需的全部字段,并保持 config/cookie-consent.ts 中对应项目的键名一致。

第三方脚本的加载方式取决于 Cookie 同意模式:

  • silent:直接加载已经配置好的第三方脚本。当前项目使用此模式。
  • prompt:只有用户同意对应统计项目后才加载。
  • disabled:不显示同意界面,也不注入第三方统计脚本。

第一方 /js/analytics.js 不受第三方统计分类控制。即使用户拒绝第三方统计脚本,只要第一方统计仍然开启且请求符合站点规则,浏览器仍会请求 /api/analytics/collect。上线前应根据目标市场和实际采集字段评估隐私告知与同意要求,详见 Cookie 同意。

关闭第一方统计

不需要第一方统计时,将总开关设为 false:

analytics: {
    enabled: false,
}

重新构建并部署后:

  • 页面不再加载 /js/analytics.js。
  • POST /api/analytics/collect 不再注册。
  • Dashboard 中的统计菜单和页面不再显示,对应后台接口也不再有效。
  • 已在 analytics-events 中等待处理的消息会被确认并丢弃。
  • 数据保留任务停止,不会继续删除历史统计数据。

关闭总开关不会自动删除 ANALYTICS_DB 中已有的数据。如需物理清除,必须先确认备份、保留要求和恢复方案,再对数据库执行单独的数据清理操作。

使用独立的采集域名

默认脚本向同源的 /api/analytics/collect 上报。需要使用独立采集域名时,在构建统计脚本前设置:

VITE_SAAVO_COLLECT_API_HOST=https://analytics.example.com
VITE_SAAVO_COLLECT_API_ENDPOINT=/api/analytics/collect

这两个值在构建 /js/analytics.js 时写入,不是运行时 Secret。独立采集域还需要正确处理来源校验、域名配置和浏览器跨域策略。同源部署不要设置它们。

上线检查

  • site.id 已换成新的 UUID。
  • VITE_SITE_URL 指向当前站点,allowedDomains 只包含实际需要的额外域名。
  • ANALYTICS_DB、ANALYTICS_QUEUE 和 analytics-events 已在自己的 Cloudflare 账户中创建并正确绑定。
  • 新数据库已初始化,且没有对现有数据库重复执行会删表的 schema/analytics.sql。
  • 前台页面能加载 /js/analytics.js,采集请求返回 202。
  • /dashboard 和其他排除路径不会产生采集请求。
  • 四个示例漏斗已经删除或改成产品真实路径。
  • Cron 配置与 ANALYTICS_RETENTION_CRON 一致,清理日志中没有持续出现积压告警。
  • 未使用的第三方配置保持为空,使用的提供商已与 Cookie 同意策略对应。
  • 隐私政策已经说明实际采集的数据、用途和保留时间。

建议使用未设置 saavo.disabled 的浏览器访问前台,再以管理员身份打开统计后台核对数据。不要用 Dashboard 自身的访问量验证采集,因为 /dashboard 默认被排除。

常见问题

接下来