Files
patbond-doc/docs/development/iterations/iteration-1/19-analytics-implementation-report.md
lixi 8e0e1c5c42 docs: 第一迭代收官——进展看板/功能清单更新 + 迭代总结
第一迭代已完成(2026-09-03 → 2026-09-04):
- 后端:82 测试(JWT 会话、compose 编排、埋点系统)
- 前端:34 测试(登录纵切、埋点模块)
- 真机联调 E2E 7/7 通过,契约偏差 0 个
- OpenAPI 契约正式化,ADR-001~008 落地
- 报告 18(E2E)、19(埋点)、20(迭代总结)入档
- 进展看板标注「第一迭代已完成」+ 交付总结
- 功能清单更新:compose 、联调 、测试数 82/34

验收状态:PASSED(对照审计 M1 要求)
下一步:M2 宠物健康档案;M1 完善项(sessionId 生命周期、page_viewed、CI 启用)

门禁:mkdocs build --strict 通过
2026-09-04 17:31:15 +08:00

180 lines
9.7 KiB
Markdown
Raw Permalink 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.
# 埋点系统实施报告(M0 简化版)
> 角色: Senior Backend Developer + Senior Flutter Developer
> 日期: 2026-09-04
> 工单: 埋点系统落地(后端 + Flutter,第一迭代最后一块功能)
> 规范依据: `13-tracking-implementation-spec.md`(事件定义、OpenAPI、DDL、隐私红线)
## 1. 交付成果
### 1.1 后端(patbond-api
**提交**: `6d47c5a` — feat: 埋点接收端落地——V2 迁移 + POST /api/v1/events 批量上报(报告 13
**核心组件**:
- `V2__create_platform_product_events.sql`: Flyway 迁移,`platform.product_events` 表(客户端 UUIDv7 主键即幂等键,`user_id` 不设外键,`client_ts` 合理性约束 ±30d/+1d,三索引按报告 13 §2.2
- `POST /api/v1/events`: 批量上报端点(1-50 条、202 逐条结果 `accepted/duplicate/rejected`
- `EventDictionary`: 事件字典 v111 个 auth_* 事件 + 工单增补 `page_viewed`/`health_record_action`),props 白名单,隐私红线模式(`password|token|secret|phone|email|...`
- `AnalyticsService`/`AnalyticsRepository`/`AnalyticsController`: 事件处理管线(未知事件拒绝、字典外 props 剥离计数、红线字段整条拒绝、认证请求 userId 与 token subject 不一致拒绝)
- `BearerAuthFilter` 可选鉴权: `/api/v1/events` 允许匿名(规范:唯一匿名写端点;带 token 仍严格验签 401/40101
**测试数**: **82 测试**75 → 82),`./mvnw clean test` BUILD SUCCESS
新增测试(`AnalyticsIntegrationTest` 7 例):
1. V2 迁移生效验证(`product_events` 表存在)
2. 匿名事件批次落库(202 accepted
3. 未知事件名拒绝(202 rejected `unknown_event_name`
4. eventId 幂等去重(第二次上传 202 duplicate
5. props 字典外剥离(acceptedstripped 字段不入库)
6. 隐私红线字段拒绝(202 rejected `forbidden_field`
7. 空批次参数校验(400 40000)
**日志红线遵守**: props 内容不落日志(仅计数与字段名告警)。
---
### 1.2 前端(patbond-flutter
**提交**: `60d67a3` — feat: 埋点采集模块落地——AnalyticsService + 登录/注册/退出三事件(M0 简化版,报告 13)
**核心组件**:
- `lib/analytics/analytics_service.dart`: `AnalyticsService``trackEvent(name, props?)`/`identify(userId)`/`reset()`),隐私红线本地校验(props key 匹配 `password|token|secret|phone|...` 本地拒绝),匿名 ID 复用 `session.deviceId`,sessionId 简化为每事件生成(M0,完整实现需 `session_tracker`),网络失败静默丢弃(无重试,按规范)
- props 白名单校验: 客户端不做(后端剥离,减少客户端与字典耦合)
- 队列: 内存队列(max 500),满 20 触发上传;持久化到 `shared_preferences` 分段留 TODOM0 时间不够)
**挂接点完成度** (报告 13 表 2 前端五事件,工单允许部分挂接):
- ✅ 登录成功/失败: `auth_login_succeeded`identifierType/durationMs)、`auth_login_failed`failureReason
- ✅ 注册成功/失败: `auth_register_succeeded`durationMs)、`auth_register_failed`failureReason
- ✅ 退出: `auth_logout`serverRevoked
- ⬜ 页面浏览: `page_viewed`(M0 无路由埋点基础,留 TODO 注释)
- ⬜ 会话恢复: `auth_session_restore_*`(Splash 恢复流程待完善,留 TODO)
- ⬜ 健康档案: `health_record_action`(M2 实现档案功能后挂接,留 TODO 注释)
**测试数**: **34 测试**30 → 34),`flutter test` 全绿
新增测试(`test/analytics/analytics_service_test.dart` 4 例):
1. trackEvent 带必需字段(不抛异常)
2. 隐私红线字段本地拒绝(silent drop
3. identify 设置 userId
4. reset 清除 userId 但保留 anonymousId
**隐私红线遵守**: props 携带 `password|token|secret|phone|email|...` key 模式本地拒绝,整条事件不发送。
**Dart 格式化**: 1 changed`analytics_service.dart`),`dart format` 无错误
**分析问题**: `flutter analyze` 86 issues(与上一波同源,非本次引入)
---
## 2. 与规范的偏差(M0 简化策略)
| 规范要求 | M0 实施 | 理由 |
| --- | --- | --- |
| sessionId 生命周期管理(冷启动/后台 30 分钟后重新生成) | 每事件独立生成 UUID | M0 无 WidgetsBindingObserver 集成,完整实现需 `session_tracker.dart`(留 TODO |
| 队列持久化到 shared_preferences 分段 | 内存队列(max 500) | M0 时间不够,`sqflite` 未引入、追加文件需 `path_provider`;内存队列足够冷启动前积压 |
| page_viewed 四次挂接(登录/注册/首页/个人中心) | 未实现 | M0 无路由埋点基础(留 TODO 注释,M1 集成路由观察者后补齐) |
| auth_session_restore_* 三事件 | 未实现 | Splash 恢复流程待完善(M0 仅占位,M1 实现后补齐) |
| health_record_action | 未实现 | M2 档案功能才有载体(留 TODO 注释) |
| appVersion / osVersion 动态读取 | 硬编码 `1.0.0+1` / `android-14` | 需 `package_info_plus` / `device_info_plus`M0 未引入(留 TODO |
所有简化均为工单「时间不够可留 TODO」明确允许;核心管线(事件上报、字典校验、去重、隐私防护)完整交付。
---
## 3. 遗留项(按优先级)
1. **Flutter sessionId 生命周期**M1: 引入 `session_tracker.dart`WidgetsBindingObserver 监听前后台切换),冷启动或后台超 30 分钟重新生成,复用 `session.deviceId` 持久化逻辑。
2. **page_viewed 路由埋点**M1: 集成 Flutter `RouteObserver`,自动在登录/注册/首页/个人中心页 `didPush` 时触发 `page_viewed`pageName/referrer)。
3. **队列持久化**M1 或 M2: 改用 `shared_preferences` 分段写入(按规范 §3.3),或评估引入 `sqflite`(报告 13 原建议)。当前内存队列 max 500 足够冷启动前积压,但进程杀死会丢失。
4. **auth_session_restore_* 事件**(M1): Splash 恢复流程完善后,在 `restoreSession()` 开始/成功/失败三处挂接。
5. **health_record_action**M2: 档案增删改查实现后挂接。
6. **动态设备信息**M1: 引入 `package_info_plus` / `device_info_plus` 读取真实 appVersion / osVersion。
7. **后端 GET /internal/events 查询端点**(M2 或审计需要时): 规范 §1 可选项,当前未实现(已有表和索引,补端点 1 小时)。
---
## 4. 验收要点
### 4.1 后端
- [x] Flyway V2 迁移生效(`platform.product_events` 表与三索引存在)
- [x] `POST /api/v1/events` 匿名请求落库(202 accepted,无 token 不拒绝)
- [x] 带 token 请求正常校验(无效 token 401/40101
- [x] eventId 去重(同 eventId 第二次上传 202 duplicate
- [x] 未知事件名拒绝(202 rejected `unknown_event_name`
- [x] props 字典外字段剥离(acceptedstripped 字段不入库)
- [x] 隐私红线字段拒绝(202 rejected `forbidden_field`
- [x] 空批次 400 40000(参数校验)
- [x] 门禁 82 测试全绿
### 4.2 前端
- [x] 登录成功/失败挂接 `auth_login_succeeded` / `_failed`
- [x] 注册成功/失败挂接 `auth_register_succeeded` / `_failed`
- [x] 退出挂接 `auth_logout`
- [x] 隐私红线本地校验(props key 命中模式不发送)
- [x] identify / reset 生命周期正确
- [x] 门禁 34 测试全绿(4 个 analytics 新增测试)
- ⚠️ page_viewed / auth_session_restore_* / health_record_action 留 TODOM0 允许)
---
## 5. 后续接入指南
### 5.1 新增事件类型
1. 后端 `EventDictionary` 加事件名与 props 白名单
2. 前端 `AnalyticsService.trackEvent()` 在业务点调用
3. 更新事件字典文档(报告 13 §4)
4. 两侧集成测试各补一例
### 5.2 完整 sessionId 实现(M1
```dart
// lib/analytics/session_tracker.dart
class SessionTracker with WidgetsBindingObserver {
String _sessionId = const Uuid().v4();
DateTime? _backgroundAt;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
_backgroundAt = DateTime.now();
} else if (state == AppLifecycleState.resumed) {
if (_backgroundAt != null &&
DateTime.now().difference(_backgroundAt!) > Duration(minutes: 30)) {
_sessionId = const Uuid().v4();
}
}
}
String get sessionId => _sessionId;
}
```
`AnalyticsService` 构造时注入,替换当前的 `Uuid().v4()` 临时方案。
---
## 6. 数据质量验收清单(报告 13 §5)
| 指标 | M0 状态 | 验收方式 |
| --- | --- | --- |
| 去重命中率(重复 eventId 占比) | ✅ 服务端 ON CONFLICT 生效 | 集成测试验证 duplicate 状态 |
| 隐私泄露零容忍(props 携带手机号/密码等) | ✅ 前后端双重防护 | 测试覆盖 `forbidden_field` 拒绝路径 |
| client_ts 合理性(±30d/+1d | ✅ DDL 约束生效 | 数据库约束阻止异常插入 |
| 事件完整率(成功上报比例) | ⚠️ M0 无持久化,进程杀死会丢 | M1 队列持久化后达 95%+ |
| sessionId 稳定性(同会话内不变) | ⚠️ M0 每事件独立 UUID | M1 SessionTracker 后达标 |
---
## 7. 总结
**M0 交付状态**: 埋点采集管线完整交付(事件上报、字典校验、去重、隐私防护),登录/注册/退出三核心事件挂接完成,82 后端测试 + 34 前端测试全绿。简化项(sessionId 生命周期、队列持久化、page_viewed 路由埋点)均为工单明确允许的 TODO,不影响核心功能验收。
**测试数增量**: 后端 75 → 82+7),前端 30 → 34+4
**挂接点完成度**: 3/5(登录/注册/退出完成,page_viewed / health_record_action 留 M1/M2
**遗留项**: 7 项,优先级明确,预计 M1 补齐前 4 项(sessionId/page_viewed/队列持久化/Splash 恢复事件),M2 补齐后 3 项(健康档案/动态设备信息/查询端点)。