增长子系统把 Funnel、Experiment 和 ShortLink 建模为持久聚合,并用 AnalyticsEvent 作为曝光、转化和点击事实。管理路由(漏斗、实验、短链、仪表盘、web-vitals、二维码)现在用 [GenerateEndpoint] 加声明式 IAuthorizedRequest 授权——授权决策随消息经 Mediator 管线传递(治理工作 G-10),而非 Host 端点调 RequirePermission。匿名遥测、公开实验分配、短链跳转、隐私自助与分析死信运维端点仍用 Host 端 RequirePermission。短链是全局资源,实验/漏斗按租户隔离。
1. 管理表面
创建 Funnel/Experiment/ShortLink 与二维码要求 website.growth.manage;结果、Dashboard、Web Vitals、Funnel projection 要求 website.analytics.read。
2. Funnel 定义
FunnelKey 规范化且在租户内唯一;Stage 是名称 + EventName 有序列表。Store 先查重复再 Add,但没有展示数据库唯一索引作为并发最终防线的证据。
投影按 Visitor 事件时间有序计算阶段到达,并可按受支持维度 segment。必须用明确时间窗口和半开区间 [from,to) 解读结果。
3. Experiment 定义
ExperimentKey 在租户内唯一,GoalMetric 指向 conversion event;Variants 有 Key 与 Weight。创建从 ICurrentTenant 取租户,不接受客户端 TenantId。
当前没有开始/停止时间、Draft/Running/Completed 状态、流量比例、互斥层、样本量计划或 guardrail metric。创建后定义即用于分配,不能宣称完整实验平台。
4. 稳定分配
公开 Endpoint 用服务端 PublicTenantId 和 VisitorId 调 AssignAsync。服务返回 Variant 与受 Data Protection 保护的 AssignmentToken,并由 Endpoint 记录 experiment-assigned 曝光。
如果分析队列已满,分配本身已计算,但 Endpoint 返回采集失败,客户端拿不到结果;重试可能再次产生曝光。分配与曝光不是同一持久事务。
// ① 租户来自站点配置,VisitorId 来自中间件。Result<ExperimentAssignment> assigned = await mediator.Send( new AssignExperimentQuery(publicTenantId, experimentKey, visitor.VisitorId), ct);if (assigned.IsFailure) return Problem(assigned.Error);
// ② 服务端而非浏览器记录带保护 token 的曝光。Result captured = collector.Capture(context, [BuildAssignmentExposure(assigned.Value!, clock.UtcNow)]);
// ③ 当前采集失败会阻断分配响应;GA 需明确降级/持久 inbox 语义。return captured.IsFailure ? Problem(captured.Error) : Ok(assigned.Value);5. 转化验证
客户端不能提交 experiment-assigned。事件带 experimentKey/variant/assignmentToken 任一维度时,Handler 调 ValidateConversionAsync,验证 token 与目标实验/Visitor/goal 匹配。
维度必须作为一组出现;测试应覆盖缺字段、篡改、跨实验、跨 Visitor、错误 Goal、过期/密钥轮换和保护器不可用。
6. 结果与显著性
Experiment Result 统计 exposures、conversions、conversion rate,并计算卡方显著性。统计显著不等于业务显著,也不替代预注册假设、样本量、停止规则、多重检验和数据质量。
文档应给出解释限制,不能把某个 p 值自动写成胜者。低样本、零曝光和 SRM 都需要显式状态。
7. ShortLink 创建
短码 6..8 位 ASCII 字母数字,统一小写。随机码使用密码学随机数,最多碰撞 8 次;自定义码只试一次。分布式锁降低并发,数据库唯一索引是最终防线。
TargetUrl 只允许绝对 HTTP(S),长度 ≤2048;ExpiresAt 必须未来。当前没有域名 allowlist/denylist、恶意 URL 扫描、租户 owner、创建审计、更新或撤销 API。
8. 缓存与解析
解析先读全局 Redis Key;无过期短链缓存 12 小时,有过期链接缓存不超过 30 分钟或剩余寿命。数据库只返回未删除且未过期记录。
当前没有管理删除/修改,所以缓存失效路径不完整也尚未暴露。未来添加撤销时必须先定义跨实例立即失效 SLA。
9. 重定向与点击
/r/{code} 解析后先把 short-link-click 写入进程队列;采集失败则不返回 301/302。分析依赖因此阻断导航,这是重要产品选择而非天然正确。
PermanentRedirect 返回 301,会被浏览器/CDN长期缓存;错误目标难以召回。默认应使用临时重定向,只有不可变、审核过的目标才允许永久。
开放重定向可能被用于钓鱼,即使 scheme 合法。需要品牌域策略、安全浏览检查、运营审计、举报/紧急下架和响应头策略。
10. QR SVG
管理请求按 Website:PublicBaseUrl 生成 {base}/r/{escaped-code},要求 HTTP(S),QRCoder 返回 SVG。Endpoint 返回字符串时必须确认 Content-Type 与 CSP,避免把 SVG 当任意可执行 HTML 嵌入。
PublicBaseUrl 只验证绝对 URL;生产应限制为已配置站点 authority,防止配置错误生成外域品牌二维码。
11. Feature 与租户
Experiments 和 ShortLinks Feature 默认关闭但未执行。Experiment/Funnel 按租户;ShortLink 表、锁与缓存 Key 都是全局,CustomCode 全局唯一。
若产品需要租户自定义短码,必须决定 URL 空间是全局 /r/code 还是含 tenant/host,并把 owner 写入聚合、权限谓词和缓存键。
12. 测试与核查
覆盖定义并发唯一、可信租户、稳定分桶、权重边界、token 篡改/轮换、曝光队满、转化重复、显著性边界、Funnel 顺序/segment、短码碰撞/锁、URL scheme/恶意域、过期缓存、301、点击队满、QR BaseUrl 和双 ORM。
# ① 管理请求的授权动作与精确权限。rg -n "GenerateEndpoint|ResourceDescriptor|AuthorizationAction" \ src/Platform/Website/BitzOrcas.Platform.Website.Application/Commands/GrowthCommands.cs# ② 全局短链与租户实验/漏斗存储谓词。rg -n "website:global:v1:short-link|TenantId.*ExperimentKey|TenantId.*FunnelKey" \ src/Platform/Website -g '*.cs'13. 实验治理清单
- 实验何时开始、暂停、停止和归档?
- 目标指标与 guardrail 是否在运行前冻结?
- Visitor 是否可同时进入互斥实验?
- 权重修改后 sticky assignment 如何解释?
- Assignment token 的用途、版本和有效期是什么?
- 曝光失败时返回分配还是失败关闭?
- 重复曝光和转化如何去重?
- Bot、内部员工和测试流量如何排除?
- SRM、低样本和多重检验如何提示?
- 结果导出是否包含算法/窗口版本?
14. 短链治理清单
- 允许哪些目标域、端口和路径?
- 是否检查 IDN 同形字、恶意域和重定向链?
- 谁拥有短码,谁能紧急撤销?
- 301 的审批与不可逆缓存风险如何控制?
- 点击采集失败是否应该阻断导航?
- 缓存失效 SLA 和 tombstone 如何设计?
- 自定义码是全局、租户还是域名空间?
- 目标变更、过期与删除是否保留审计历史?
- QR SVG 如何安全返回和嵌入?
- 举报、封禁和误封恢复 Runbook 在哪里?