diff --git a/docs/superpowers/specs/2026-06-23-cloudflare-analytics-only-design.md b/docs/superpowers/specs/2026-06-23-cloudflare-analytics-only-design.md new file mode 100644 index 000000000..ba491eb15 --- /dev/null +++ b/docs/superpowers/specs/2026-06-23-cloudflare-analytics-only-design.md @@ -0,0 +1,219 @@ +# Cloudflare Analytics Only 设计 + +## 背景 + +当前 `server/src` 已经有 Cloudflare Analytics Engine 热查询和 Cloudflare Pipelines 归档写入的第一阶段代码,但分析链路仍保留 Tinybird 作为写入目标、读路径回退和部分审计存储。用户目标是移除 Tinybird 回退方案,只保留 Cloudflare analytics pipelines,并移除 `@tinybirdco/sdk` 依赖。 + +本设计覆盖: + +- `/Volumes/sker/autumn/server/src` +- `/Volumes/sker/autumn/vite/src` +- 与上述改动直接相关的 `server/package.json`、`bun.lock`、`server/wrangler.jsonc`、单元测试和文档 + +本设计不把整个项目一次性改成完全无 Tinybird 代码。重点是移除 analytics 的 Tinybird fallback/dual-write,以及删除 `@tinybirdco/sdk` 的运行时依赖来源。 + +## 目标 + +- 事件分析写入只走 Cloudflare Analytics Engine 和 Cloudflare Pipelines。 +- analytics 读路径不再静默回退 Tinybird。 +- 移除 `@tinybirdco/sdk` 依赖和 `server/src` 中对它的 import。 +- 前端不再显示 Tinybird 专有错误文案。 +- migration item audit 不再依赖 Tinybird SDK,改为 Cloudflare-friendly 的稳定存储边界。 +- 保留主业务可靠性:分析写入失败不影响 `track/check`、余额、扣减、订阅或幂等状态。 + +## 非目标 + +- 不在本次彻底删除 `@chronark/zod-bird` 和所有 Tinybird pipes。 +- 不重写所有 revenue analytics、复杂 aggregate/groupable 查询和 ClickHouse SQL 查询。 +- 不把 R2/R2 SQL 冷归档同步接入实时 dashboard。 +- 不把 Cloudflare analytics 写入失败升级为业务请求失败。 +- 不在本次解决所有 Cloudflare-only runtime 遗留项,例如 `nodejs_compat` 或全部 Node/Bun SDK 残留;这些已有独立设计线。 + +## 推荐方案 + +采用聚焦方案: + +1. 移除 analytics 的 Tinybird 写入和读回退。 +2. 保留 Cloudflare hot store 作为唯一实时 analytics store。 +3. 对 Cloudflare hot store 暂不支持的查询返回明确错误,而不是回退 Tinybird。 +4. 将 migration item audit 从 Tinybird SDK 改到 Postgres-backed 存储。 +5. 删除 `@tinybirdco/sdk` 依赖。 + +不采用“彻底移除所有 Tinybird 代码”的原因是范围会扩大到 revenue analytics、复杂 pipes 和历史 SQL 语义,风险明显高于当前目标。不采用“只删 `@tinybirdco/sdk`”的原因是它保留 analytics fallback,不满足用户目标。 + +## 架构边界 + +`server/src/internal/analytics` 继续拥有业务 analytics 接口,但生产运行路径收敛为 Cloudflare: + +- `EventBatchingManager` 继续负责批处理和 Postgres 插入队列。 +- 事件分析写入改为语义明确的 `writeEventsToCloudflareAnalytics(...)`。 +- `Analytics Engine` 承载近实时热查询。 +- `EVENTS_ARCHIVE_PIPELINE` 承载归档写入。 +- `AnalyticsStore` 接口可以保留,但 selector 不再返回 Tinybird store 作为 fallback。 +- `tinybirdEventStore` 可以在后续彻底迁移时删除;本次若仍被未迁移路径引用,必须确保它不是 Cloudflare analytics 读路径的 fallback。 + +`vite/src` 只做用户可见状态调整:移除 `Tinybird is disabled` 文案和对 `ErrCode.TinybirdDisabled` 的 analytics 页面专属判断,改为 Cloudflare analytics unavailable/unsupported 的通用状态。 + +## 写入数据流 + +写入流: + +1. 业务产生 `EventInsert[]`。 +2. `EventBatchingManager` 将事件批量入队到 Postgres 插入 job。 +3. 同一批事件发送到 `writeEventsToCloudflareAnalytics({ env, events, logger })`。 +4. `writeEventsToAnalyticsEngine(...)` 调用 `env.EVENTS_ANALYTICS.writeDataPoint(...)`。 +5. `writeEventsToArchivePipeline(...)` 调用 `env.EVENTS_ARCHIVE_PIPELINE.send(...)`。 +6. 任一 Cloudflare analytics 写入失败只记录日志和 Sentry,不抛出到主业务流。 + +Cloudflare Analytics Engine 的 datapoint 字段顺序是稳定契约。现有 `CLOUDFLARE_ANALYTICS_INDEX_FIELDS`、`CLOUDFLARE_ANALYTICS_BLOB_FIELDS`、`CLOUDFLARE_ANALYTICS_DOUBLE_FIELDS` 必须继续由测试锁定,避免查询层的 `blobN`、`doubleN` 映射漂移。 + +Pipelines binding 继续使用 `wrangler.jsonc` 的 `pipelines` 配置,并使用 `stream` 字段。不要回退到旧字段名,也不要在业务代码里硬编码 Cloudflare 资源名。 + +## 读取数据流 + +实时 analytics 读路径: + +1. `listEventNames`、`listRawEvents`、`listEvents`、`listByCursor` 调用 store selector 或直接调用 `cloudflareHotEventStore`。 +2. Cloudflare hot read 开启且查询在热窗口内时执行 Analytics Engine SQL API 查询。 +3. 查询使用当前 `eventSchema` 字段顺序构建 SQL。 +4. 查询结果映射回现有 API response shape。 + +不再支持的自动回退: + +- Cloudflare hot read disabled -> 不回退 Tinybird。 +- 超出热窗口 -> 不回退 Tinybird。 +- `filter_by` 等 hot store 暂不支持能力 -> 不回退 Tinybird。 +- Analytics Engine SQL API 未配置 -> 不回退 Tinybird。 + +这些情况应返回明确错误。建议新增或复用语义清楚的错误码: + +- `CloudflareAnalyticsUnavailable` +- `CloudflareAnalyticsUnsupportedQuery` + +错误文案应描述当前 analytics 查询不可用或查询能力暂不支持,不提 Tinybird。 + +## Migration Item Audit 替代 + +`@tinybirdco/sdk` 当前还用于 migration item audit。为了删除该依赖,本次将该审计存储改为 Postgres-backed。 + +理由: + +- migration item audit 是低频业务审计数据,不是高吞吐指标。 +- 它需要按 `org_id`、`env`、`migration_internal_id`、`migration_run_id`、`item_kind`、`item_id` 做精确读取和 latest-by-item 聚合。 +- Postgres 已经是业务主存储,事务、索引和测试路径都更清楚。 +- 这避免为了低频审计路径引入 R2 SQL 或 Analytics Engine 查询语义。 + +设计: + +- 定义 Postgres 表或复用现有迁移审计表,字段对应当前 `TinybirdMigrationItemEvent`。 +- 保留当前 repo 函数名或导出 shape,降低调用方改动。 +- `insertMigrationItemEvents` 批量插入 Postgres,失败只记录日志,不阻塞 migration 主流程。 +- `listMigrationItemEvents` 和 `listLatestMigrationItemEvents` 从 Postgres 查询,并保持现有排序和空列表行为。 +- 删除 `initTinybirdV2.ts` 和 `migrationItemEventsDataSource.ts` 对 `@tinybirdco/sdk` 的依赖。 + +如果已有数据库迁移框架需要 schema 变更,本实现计划必须使用数据库迁移安全流程:先加表/索引,再切代码,避免读写代码和 schema 不匹配。 + +## 配置变化 + +移除或废弃: + +- `CLOUDFLARE_ANALYTICS_TINYBIRD_FALLBACK_ENABLED` +- Tinybird secondary dual-write 配置 +- `TINYBIRD_API_URL` / `TINYBIRD_TOKEN` 在 analytics 写入路径中的使用 +- `@tinybirdco/sdk` package dependency + +保留或确认: + +- `CLOUDFLARE_ANALYTICS_WRITE_ENABLED` +- `CLOUDFLARE_ANALYTICS_ARCHIVE_ENABLED` +- `CLOUDFLARE_ANALYTICS_HOT_READ_ENABLED` +- `CLOUDFLARE_ANALYTICS_HOT_RETENTION_DAYS` +- `CLOUDFLARE_ANALYTICS_DATASET` +- `EVENTS_ANALYTICS` +- `EVENTS_ARCHIVE_PIPELINE` +- `CLOUDFLARE_ACCOUNT_ID` 和 `CLOUDFLARE_API_TOKEN`,用于 Analytics Engine SQL API + +Cloudflare 写入和归档 flags 可以继续独立控制,但它们不表示 Tinybird fallback 是否存在。 + +## 错误处理 + +- 写入失败:Cloudflare analytics 写入失败不影响主业务。记录结构化日志和 Sentry,包含 service、eventCount 和错误信息。 +- 读取失败:不隐藏失败,不回退 Tinybird。返回 Cloudflare analytics unavailable/unsupported 类型错误。 +- 配置缺失:hot read 开启但缺少 SQL API token 或 account id 时返回 service unavailable 类错误。 +- 查询不支持:`filter_by` 或超出热窗口等情况返回 unsupported query 类错误。 +- migration audit 写入失败:记录日志,不阻塞 migration 主流程。 +- migration audit 读取无数据:保持当前保守行为,返回空列表。 + +## 前端行为 + +`vite/src/views/customers/customer/analytics/AnalyticsView.tsx` 不再使用 `ErrCode.TinybirdDisabled` 驱动 UI 状态。 + +新行为: + +- 对 Cloudflare analytics unavailable 显示中性状态。 +- 对 unsupported query 显示查询暂不支持状态。 +- 页面文案不出现 `Tinybird`。 +- 不改变 analytics 页面主体布局和交互,避免把后端迁移扩大成 UI 重构。 + +## 测试计划 + +更新现有测试: + +- `server/tests/unit/analytics/cloudflare-config.test.ts` + - 删除 `tinybirdFallbackEnabled` 断言。 + - 验证 Cloudflare write/read/archive flags 和 bindings。 +- `server/tests/unit/analytics/select-analytics-store.test.ts` + - 不再期待 Tinybird fallback。 + - 验证 disabled、outside range、unsupported feature 返回 Cloudflare analytics 错误或 unsupported decision。 +- `server/tests/unit/analytics/cloudflare-writers.test.ts` + - 验证 Cloudflare 写入失败不抛出到调用方。 + - 验证没有 Tinybird 调用。 +- `server/tests/unit/runtime-env-imports.test.ts` + - 增加 `server/src` 禁止 import `@tinybirdco/sdk` 的 guard。 +- `vite` 相关测试或静态断言 + - 验证 `vite/src` 不再出现 `Tinybird is disabled`。 + +新增或调整测试: + +- migration item audit Postgres repo 单元测试。 +- package dependency guard,验证 `server/package.json` 不声明 `@tinybirdco/sdk`。 +- 如果新增数据库表,增加迁移 shape 或 repo 查询测试。 + +推荐验证命令: + +```sh +cd server && bun test --isolate tests/unit/analytics +cd server && bun test --isolate tests/unit/runtime-env-imports.test.ts +bun -F @autumn/server ts +bun -F @autumn/vite ts +``` + +如果数据库迁移加入本次实现计划,还需要运行对应 migration/schema 验证命令。 + +## 验收标准 + +- `rg -n "@tinybirdco/sdk" server/src server/package.json` 无结果。 +- `server/package.json` 不再依赖 `@tinybirdco/sdk`。 +- analytics 写入路径不再调用 `sendEventsToTinybird`。 +- Cloudflare analytics 写入仍可同时覆盖 Analytics Engine 和 Pipelines。 +- analytics 读路径不再因为 disabled、outside range、unsupported feature 回退 Tinybird。 +- `vite/src` 不再显示 Tinybird 专有文案。 +- analytics 单元测试和 runtime import guard 通过。 +- TypeScript 检查通过,或明确记录与本次无关的既有失败。 + +## 实施顺序 + +1. 先更新测试和 guards,让 Tinybird fallback 与 `@tinybirdco/sdk` import 成为可重复验证的失败。 +2. 收敛 analytics 写入函数命名和调用方,删除 Tinybird primary/secondary dual-write。 +3. 收敛 analytics store selection,移除 Tinybird fallback。 +4. 新增 Cloudflare analytics unavailable/unsupported 错误语义,并更新前端文案。 +5. 将 migration item audit 改为 Postgres-backed。 +6. 删除 `@tinybirdco/sdk` 依赖并更新 lockfile。 +7. 跑验证命令,修复由本次变更引入的失败。 + +## 风险与后续 + +- 部分复杂 analytics 查询仍可能依赖 Tinybird pipes 或 ClickHouse SQL。若它们属于 dashboard 核心能力,应单独开第二份 spec 迁移到 Analytics Engine、R2 SQL 或 Postgres 聚合。 +- Revenue analytics 当前不纳入本次迁移,后续需要单独设计指标语义和数据源。 +- Analytics Engine 热窗口之外的历史查询当前不自动支持。后续应基于 Pipelines 归档和 R2 SQL 设计异步或低频历史查询路径。 +- Cloudflare-only runtime 仍有更大范围的 Node/Bun 兼容残留,本次只处理与 analytics/Tinybird SDK 直接相关的部分。