Files
fangke/docs/superpowers/specs/2026-09-18-collection-phase-b-design.md
T

120 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 采集阶段B:业务事件体系(零网站改动)— 设计文档
日期:2026-09-18
分支:m1
状态:设计已获用户批准(四轮自审+独立复审,共 28 项修正)
依据:gpt/14 §5/§6.2;前置阶段A 已上线(2026-09-18,归属冻结/区间停留/清洗三轨制/契约 v2)
实测基础:hesucar.com 载体盘点(2026-09-18 Playwright)——tel/mailto/wa.me 链接、Contact Form 7 v6.1.6 留言表单(Name/Tel/Message→Inquiry Now)、悬浮聊天组件
## 核心约束与决策
1. **零网站改动**:hesucar WordPress 不加任何代码。表单服务端确认通道**只做契约预留**(阶段C 接入)。
2. SDK 自动监听(内置埋点),不用 track 手动调用承载业务事件。
3. 事件全部携带阶段A 归属四元组(PageSession.attribution())。
4. 范围:contact_click / form_*(CF7 全过程)/ product_view(URL 推导)/ track 通道修复 / D06 归因扩展(paid_social + utm_medium)/ admin 时间线新事件 / 询盘漏斗最小视图。
5. 排除:服务端确认实现(阶段C)、次批事件(scroll_depth/section_view/media_progress 等)、sdk_diag、capabilities、完整漏斗报表、导出对账。
## 1. SDK(0.2.0 → 0.2.1):新模块 `sdk/src/business.ts`
与 page.ts/stay.ts 同模式,init 时安装;所有回调 try/catch 静默(红线)。
### 1.1 contact_click
- document 级点击委托(capture):命中链接 `wa.me / api.whatsapp.com / whatsapp://` → `channel:"whatsapp"`;`tel:` → `"phone"`;`mailto:` → `"email"`。属性仅 `{channel}`(枚举;placement 已砍——YAGNI)。
- 点击后立即 `transport.flush(true)`(beacon)——跳转窗口内保证发出。
- **known-limitation**:非链接按钮(如悬浮组件 Get Quotation)无标准特征,不识别不硬造。
- 语义:只证明点击意图,不宣称联系成功(OT09)。
### 1.2 form_*(Contact Form 7 全过程)
监听 CF7 DOM 事件(**已按线上 CF7 6.1.6 源码实测核验**,2026-09-18):CF7 6.x 无 wpcf7invalid/wpcf7mailsent 等旧事件——全过程状态走 **`wpcf7statuschanged`**(`detail.status` ∈ init/validating/invalid/unaccepted/spam/mail_sent/mail_failed/resetting);`wpcf7submit` 在 AJAX 响应后才触发(带 apiResponse,弃用);CF7 拦截原生 submit 于 form 元素冒泡阶段(`preventDefault` + `wpcf7.submit` 内构造 FormData),所有自定义事件 `bubbles:true`。FormData 构造含 hidden 字段(`:where(input,textarea,select):enabled` + default append)。
| 事件 | 触发 | 属性 |
|---|---|---|
| form_start | .wpcf7-form 内首次 isTrusted focusin/input(每表单实例一次) | form_id |
| form_submit | **表单原生 submit(document capture,与 _ctk_sid 写入同点——先于 CF7 拦截)**,每次尝试 | form_id, submission_id |
| form_error | wpcf7statuschanged(status=invalid/unaccepted → error_code "invalid";spam → "spam") | form_id, submission_id, error_code, field_count |
| form_submit_result | wpcf7statuschanged(status=mail_sent → "mail_sent";mail_failed → "mail_failed") | form_id, submission_id, result_code |
- **submission_id 语义**:一次提交尝试 = 一个 ID——**原生 submit 捕获时生成并写入隐藏字段**(§1.5 时序设计),form_submit / result 事件引用该 ID。form_start 不生成。
- **form_id 规整**:从 CF7 unit_tag/action 解析表单编号 → `cf7-{ID}`(如 cf7-156),不暴露完整内部串。
- **不采任何输入值**(OT10);error_code 只取状态码 + 字段数,不采 invalid_fields 明细。
- form_submit_result 是**客户端声明**(事件名本身表达,不升级为可信确认)。
### 1.3 product_view
- 规则:path **段匹配** `product/{slug}`(**非锚定**——覆盖 `/{lang}/product/{slug}/` 多语言前缀;实测样本 9 种语言前缀 ar/de/es/fr/it/pt/ro/ru/th,锚定匹配会静默丢失约半数产品页);pattern 内置默认,init 可覆盖——为未来站点预留,hesucar 用默认。
- 属性:`{product_id: slug, inferred: true, rule_version: "v1"}`。多语言同产品无法关联——诚实标注(inferred 降级路径,gpt/14 §5.1)。
- **触发时机**:init 时同步判定 + SPA 切段回调。PageSession 新增 `onSegmentChange(fn)` 钩子——**触发点钉死在 `onRouteChange` 内新段创建之后**(模块级注册表,不在 `close()` 内触发——close 还覆盖 pagehide/重复 init,放在那里会在卸载时重复发 product_view);单点 hook history,business.ts 注册回调判定新段 path,不重复 wrap。
- PV 不双计:服务端 PV 统计已有 event_name 过滤(87261ab)。
### 1.4 track 通道修复(阶段A 终审 I-1)
- `ctk('track', name, props)`:transport.push 前用 `getPageSession()?.attribution()` 补归属顶层字段;事件名必须在服务端白名单内(白名单扩后合法名可通过)。
- **约定**:自动监听的事件(contact_click/form_*/product_view)勿手动 track,否则双发。
### 1.5 隐藏字段 `_ctk_sid`(服务端确认契约预留的浏览器侧半边)
- **submission_id 写入时序(结构性修正)**:`wpcf7submit` 在 AJAX 响应到达后才触发——POST 已发出,届时更新字段必然晚。因此改为:**capture 阶段监听表单原生 `submit` 事件**(document 级 capture 委托,先于 CF7 的 handler 构造 FormData 执行)——原生 submit 时生成当次 submission_id 并写入隐藏字段;wpcf7submit 触发的 form_submit 事件引用同一 ID。
- form_start 时注入 `<input type="hidden" name="_ctk_sid" value="">` 空占位(提交前字段恒存在;值由上述 submit 捕获填充)。
- 作用:CF7 提交 POST 天然携带,阶段C 的 WP mu-plugin 从 `$_POST['_ctk_sid']` 读取对齐。CF7 忽略未注册字段、正常传输——对现有表单行为无害。
- known-limitation:`_ctk_sid` 空值仍可能出现(capture submit 未及触发的极端时序),阶段C 按 §5.4 兜底(浏览器侧缺失不影响服务器事实)。
## 2. Server:契约扩展
- `EVENT_NAMES` +6:contact_click / form_start / form_error / form_submit / form_submit_result / product_view。
- `EVENT_PROP_WHITELIST` 严格枚举(§1 表格所列键;无自由文本字段)。
- validate_event 新键校验:`channel` ∈ {whatsapp, phone, email};`result_code` ∈ {mail_sent, mail_failed};`error_code` 短枚举(invalid/spam/other);`submission_id` uuid 或空串;`form_id` 格式 `cf7-\d+`;`product_id` slug 格式(**≤100 字符**——独立于清洗 _SAFE_VALUE 的 31 上限,产品 slug 可较长);`field_count` 非负整数(≤99);`inferred` 布尔;`rule_version` 短 slug。
- 幂等沿 uq_site_event;归属走既有 v2 路径,零新机制。
## 3. D06 归因扩展(attribution.py + constants.py)
- **优先级重排**:点击参数决定平台性质——fbclid → **paid_social**(Meta 平台本质),gclid/msclkid/yclid → paid_search;utm_medium 仅在无点击参数时参与判定(label_only 证据)。
- `CHANNELS` +`paid_social`;utm_medium 规范化映射(写死):**fb 家族 utm_source + 任意付费类 medium(cpc/ppc/paidsearch/paid/social/paid_social/social_ads/paidsocial)→ paid_social,此规则优先于强化集**(实测样本主导模式即 `utm_medium=paid&utm_source=fb`,gpt/14 §6.1 的 28 条 fb/paid——不放本规则则它们仍全落 paid_search,违背本节目的);其余付费 medium(无 fb source)→ paid_search 强化(label_only 证据)。
- **`ATTRIBUTION_RULE_VERSION` bump v1→v2** + 历史重算:`recompute_attribution` 管理命令**已存在**(management/commands/recompute_attribution.py,含 --site 参数与既有幂等测试 test_attribution.py:226 可参照),本任务只需 bump 版本 + 新渠道分支测试 + 线上重算执行。
- fb 家族 utm_source 判定复用既有广告家族归一表(132e2fe 引入的家族映射)。
- admin 中文标签同步:付费社交(渠道徽章/来源分布,grep 全部渠道显示点统一改)。
## 4. Admin 前端
### 4.1 时间线新事件展示
- EventTimeline/访客详情的事件类型映射扩展:contact_click(咨询点击 💬+channel)、form_start/submit/error/result(表单各阶段 📋+关键属性)、product_view(产品查看 🚗+product_id)——具体图标按现有组件风格。
- 新事件自动进时间线(timeline API 原样返回),仅前端徽章/文案工作。
### 4.2 询盘漏斗卡(数据总览)
- 五级:浏览(page_view) → 咨询点击 → 表单开始 → 提交 → 邮件已发(**显示措辞"邮件已发(未验证)"**——客户端声明的诚实边界)。首级用 page_view 而非上游文档的 product_view:product_view 为 inferred 推导会低漏计,page_view 是全量基线(偏离理由留档)。
- **口径:逐级独立 distinct session 计数**(各级 Count("session", distinct=True)),**非严格嵌套漏斗**——允许跳级真实路径(gpt/14 §5.4"允许直接进入表单");各级数字不嵌套,**UI 不显示级间百分比转化率**(避免嵌套误读),只显示各级计数。
- **全级过滤 `is_bot=False`**(Event.is_bot 既有标记——爬虫浏览/垃圾提交不灌漏斗)。
- 当日/近 7 天切换;新 API `GET /api/admin/funnel-summary?days=N`(site 权限沿既有)。
- 卡片位置随 dashboard 现有排版(趋势/渠道卡附近),实现时看布局。
## 5. 服务端确认——契约预留(不实现)
spec 附录写死(阶段C 直接照做,客迹侧零改动):
1. submission_id:uuid v4,**原生 submit 捕获时生成、form_submit 事件引用**(§1.5 时序),`_ctk_sid` 隐藏字段随 POST 传输。
2. 认证端点:`POST /api/integrations/form-confirm`,站点级密钥(HMAC 签名 + 时间窗 ±5min + nonce 防重放 + submission_id 幂等);事件 form_submission_confirmed(白名单届时加入)。
3. WordPress mu-plugin 示例(约 20 行 PHP):hook `wpcf7_mail_sent` → 读 `$_POST['_ctk_sid']` + 表单摘要 → 签名转发。
4. 浏览器事件缺失时服务器确认仍成立(业务事实保留,浏览轨迹标缺);同 ID 重试不重复计数,内容变化报冲突。
## 6. 验证与部署
- **CF7 事实核验(已完成,2026-09-18)**:线上 CF7 6.1.6 源码实测——事件集/时序/FormData 行为已写入 §1.2(wpcf7statuschanged 状态机 + 原生 submit capture 确定性时序 + hidden 字段采集确认)。开发期不真提交验证 mailsent——按源码实现,**部署后以本节末条的真实测试询盘闭环验证**。
- SDK 单测:点击委托命中/漏判、CF7 事件模拟、submission_id 每次尝试独立、form_start 去重、pattern 匹配、onSegmentChange、track 补归属。
- server 测试:6 新事件校验(枚举/格式)、漏斗 API distinct session 口径、归因 paid_social/utm_medium 分支、规则 v2 重算幂等。
- admin:时间线渲染新事件、漏斗卡数据正确。
- **部署顺序**:server → admin → SDK 0.2.1(SDK 先上会被白名单拒);**整体排在 9/20 观察窗 D02 确认之后**(开发期自然覆盖,非阻塞开发)。
- **归因重算的预期管理**:D06 重算在部署时一次执行——渠道分布/来源证据中部分"付费搜索"数字移至"付费社交"(Meta 付费流量类目变化),属预期口径调整非 bug,部署汇报中说明。
- 线上全链路:Playwright 点 hesucar WhatsApp 链接/tel/mailto → 事件落库;空表单提交 → form_start+submit+error 落库。
- **mailsent 真实闭环**(用户已接受):部署后在 hesucar 表单真实提交一次测试询盘(Name 填 "TEST-KEJI" 标记,商家邮箱收一封测试邮件)——一次性验证 form_submit_result 全链路(事件→白名单→落库→漏斗末级计数)。
## 7. 风险与边界
- CF7 事件形态按公开源码+invalid 实测锚定,mailsent 分支上线后以真询盘验证(诚实标注)。
- `_ctk_sid` 注入对表单无行为影响(CF7 忽略未注册字段);capture 原生 submit 先于 CF7 handler 构造 FormData,写入顺序确定性成立;残余风险仅为 capture 未及触发的极端时序空值(§5.4 兜底)。
- product_view 为 inferred URL 推导:跨语言同产品不关联、URL 结构变化需 bump rule_version。
- track 与自动监听双发:靠文档约定(不强制拦截),RawBatch 可诊断。
- 询盘漏斗末级为客户端声明(未验证措辞),阶段C 接确认通道后升级为"已验证询盘"。