8e0e1c5c42
第一迭代已完成(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 通过
180 lines
9.7 KiB
Markdown
180 lines
9.7 KiB
Markdown
# 埋点系统实施报告(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`: 事件字典 v1(11 个 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 字典外剥离(accepted,stripped 字段不入库)
|
||
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` 分段留 TODO(M0 时间不够)
|
||
|
||
**挂接点完成度** (报告 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 字典外字段剥离(accepted,stripped 字段不入库)
|
||
- [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 留 TODO(M0 允许)
|
||
|
||
---
|
||
|
||
## 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 项(健康档案/动态设备信息/查询端点)。
|