docs: Task16 SDK↔后端联调记录
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# Task 16 联调记录:SDK ↔ 后端本地集成验证
|
||||
|
||||
- 日期:2026-09-15
|
||||
- 分支:m1
|
||||
- 验证路径:**Playwright 真实浏览器**(主链路)+ **curl**(信封/Origin/幂等)+ **manage.py shell**(DB 断言)
|
||||
- 环境:Windows Git Bash;server 为 Django runserver(sqlite 开发模式),demo 静态站为 `python -m http.server`
|
||||
|
||||
## 0. 环境与命令实录
|
||||
|
||||
### 后端(server/)
|
||||
```bash
|
||||
DB_ENGINE=sqlite .venv/Scripts/python manage.py migrate # 含 tracker.0002 迁移,OK
|
||||
DJANGO_SUPERUSER_PASSWORD='Abcd1234!' DB_ENGINE=sqlite .venv/Scripts/python manage.py createsuperuser --noinput --username admin --email admin@example.com
|
||||
# site_x 在本机 dev 库已存在(早期 'probe' 站占用 site_id),任务原文的 get_or_create(name='demo', defaults=dict(site_id='site_x',...))
|
||||
# 会撞 UNIQUE(site.site_id)——改为原地更新既有行(等效最终状态):
|
||||
DB_ENGINE=sqlite .venv/Scripts/python manage.py shell -c "..."
|
||||
# Site.objects.get(site_id='site_x') -> name='demo', domains=['127.0.0.1'], allow_no_origin=True
|
||||
DJANGO_DEBUG=1 DB_ENGINE=sqlite .venv/Scripts/python manage.py runserver 127.0.0.1:8000 --noreload # 后台
|
||||
```
|
||||
|
||||
### SDK 与 demo 静态站(sdk/)
|
||||
```bash
|
||||
pnpm build # dist/v1.js 4648 字节(≤15KB 红线 ✓)
|
||||
python -m http.server 18080 --bind 127.0.0.1 # 注意:根目录必须是 sdk/(见发现 3)
|
||||
```
|
||||
|
||||
**访问 URL**:`http://127.0.0.1:18080/demo/index.html?utm_source=google&utm_medium=test`
|
||||
(计划原文写 `--directory demo` + `/index.html`,但 demo 页引用 `../dist/v1.js`,`--directory demo` 时该路径逃逸出 web 根 → 404,SDK 根本加载不到。须以 sdk 为根、走 `/demo/index.html`。)
|
||||
|
||||
### 浏览器操作序列(Playwright MCP)
|
||||
1. 打开 index.html(带 utm_source=google&utm_medium=test),停留约 20s
|
||||
2. 点击「去第 2 页」链接(同站内导航 → page2.html),停留约 8s
|
||||
3. evaluate 合成 `visibilitychange`(hidden→visible 各一次)触发 page_stay + flush(true)
|
||||
4. 再停留数秒,reload page2(同 visitor 再访问)
|
||||
|
||||
## 1. 断言清单结果(全部通过)
|
||||
|
||||
### ① Event 表三类事件、is_bot=False —— PASS
|
||||
```
|
||||
sdk_loaded | /demo/index.html?utm_source=google&utm_medium=test | {sdk_version: 0.1.0}
|
||||
page_view | /demo/index.html?utm_source=google&utm_medium=test | {utm:{utm_source:google,utm_medium:test}, referrer_domain:""}
|
||||
page_stay | /demo/index.html?utm_source=google&utm_medium=test | {active_ms: 29000}
|
||||
sdk_loaded | /demo/page2.html
|
||||
page_view | /demo/page2.html | {referrer_domain:"127.0.0.1", referrer:"http://127.0.0.1:18080/demo/index.html?..."}
|
||||
page_stay | /demo/page2.html | {active_ms: 18000}
|
||||
page_stay | /demo/page2.html | {active_ms: 11000} ← reload 时 pagehide 触发
|
||||
sdk_loaded/page_view ×2(reload 后)
|
||||
```
|
||||
所有事件 `is_bot=False`、`late=False`。
|
||||
|
||||
### ② Visitor 归因 —— PASS
|
||||
```
|
||||
visit_count=1(≥1) active_days=1(==1) is_bot=False
|
||||
first_utm={utm_source: google, utm_medium: test}
|
||||
first_landing_path=/demo/index.html?utm_source=google&utm_medium=test # 含 utm 参数 ✓
|
||||
device=PC / os=Windows / browser=Chrome / language=zh-CN / screen=1920x1080
|
||||
last_ip=127.0.0.1
|
||||
```
|
||||
|
||||
### ③ VisitSession —— PASS
|
||||
```
|
||||
event_count=9 stay_ms=58000(=29000+18000+11000,>0 ✓)
|
||||
utm={utm_source: google, utm_medium: test} ✓
|
||||
referrer_domain="" —— 首个落地页为地址栏直开(无 referrer),按设计首触留空;
|
||||
page2 的页级来源 referrer_domain=127.0.0.1 已记在事件 properties 中 ✓
|
||||
duration_ms=1 —— 批内跨度累加(计划 §Task18 明示"批内跨度累加,仅内部字段不展示"),符合设计,非 bug
|
||||
```
|
||||
|
||||
### ④ 刷新后幂等 —— PASS
|
||||
- 同一 visitor_id `b507cb61-…`(get_or_create 幂等,Visitor 全程仅 1 行)
|
||||
- reload 复用 sessionStorage 的 session_id(30 分钟会话窗口内,属预期),未虚增 visit_count
|
||||
- `Event 总数 == 去重 event_id 数`(9 == 9;含 curl 批后 11 == 11)
|
||||
- curl 重放同批次:`{"accepted":0,"duplicated":1}` —— 幂等去重生效
|
||||
|
||||
### ⑤ 响应信封(curl)—— PASS
|
||||
| 请求 | 结果 |
|
||||
|---|---|
|
||||
| 无 Origin(allow_no_origin=True) | `200 {"code":200,"data":{"accepted":1,"duplicated":0},"message":"ok"}` |
|
||||
| `Origin: http://evil.example.com` | `403 {"code":403,"data":null,"message":"not_allowed"}`(且带 ACAO 回显,跨域 SDK 可读错误体) |
|
||||
| `Origin: http://127.0.0.1:18080`(白名单) | `200` + `Access-Control-Allow-Origin: http://127.0.0.1:18080` 回显 |
|
||||
| 同批重放 | `200 {"accepted":0,"duplicated":1}` |
|
||||
|
||||
### ⑥ SDK 异步加载 —— PASS
|
||||
- demo 页 `<script async src="../dist/v1.js">`,Network 面板可见 `GET /dist/v1.js => 304`
|
||||
- 内联队列 `ctk.q` 模式生效:async 脚本加载完成前排队的 `init` 被正确消费(批次 site_id=site_x、endpoint 指向 8000 可证)
|
||||
- 浏览器实际发出 2 次 fetch POST(各 200),sendBeacon 3 次(见发现 4)
|
||||
|
||||
### 附加:管理 API 对拍(真实数据,为 Task 17/18 铺路)
|
||||
- `POST /api/admin/auth/token`(admin)→ JWT
|
||||
- `GET /api/admin/dashboard/overview` → `today{pv:11,uv:2,sessions:2,new_visitors:2,active_now:2}`,top_pages/sources 正确
|
||||
- `GET /api/admin/visitors/{id}` → `total_stay_ms=58000, first_source=google, online=true`
|
||||
|
||||
## 2. 发现的问题与处置
|
||||
|
||||
| # | 发现 | 定性 | 处置 |
|
||||
|---|---|---|---|
|
||||
| 1 | dev 库已有 site_x('probe')时,任务原文 `get_or_create(name='demo', defaults=dict(site_id='site_x',…))` 撞 UNIQUE(site.site_id) 抛 IntegrityError | 环境态问题(命令对全新库才成立) | 原地更新既有行了事;后续复用 dev 库时同样处理 |
|
||||
| 2 | 本机 8080 端口被占(WinError 10013,PID 38864 非本任务进程) | 环境问题 | 改用 18080;demo 页 endpoint 为绝对地址,端口无关 |
|
||||
| 3 | `python -m http.server --directory demo` 时 demo 页的 `../dist/v1.js` 逃逸 web 根 → 404,SDK 不加载 | **使用方式陷阱**(demo 页相对路径要求以 sdk/ 为服务根) | 以 sdk/ 为根、URL 用 `/demo/index.html`;计划文档该步的 `--directory demo` 写法不可用,已在本记录纠正 |
|
||||
| 4 | sendBeacon 发出的 page_stay 批次不出现在 Playwright network 列表 | 工具观测局限 | 以服务端 DB 断言为准(events/stay_ms 均完整入库) |
|
||||
| 5 | SDK context 携带 `timezone:"Asia/Shanghai"`,但 Visitor.timezone_name 落库为空 | 计划内缺口(计划 `_FIELD_LEN` 即未含 timezone_name,实现与计划一致) | 非 M1 bug;建议 M2 在 `_FIELD_LEN` 增补 `timezone_name: 64` |
|
||||
| 6 | duration_ms=1 而会话真实跨度约 178s | **非 bug**:计划明示"批内跨度累加,仅内部字段不展示",展示口径用 stay_ms | 无需修复(联调中曾疑为 bug,核对计划与 test_visitors 注释后排除) |
|
||||
|
||||
**结论:未发现产品代码 bug,无需 fix 提交。** 88(server)/46(sdk)测试基线不受影响(本次未改产品代码)。
|
||||
|
||||
## 3. 验证后状态
|
||||
- db.sqlite3 已在 .gitignore,不入库(保留供 Task 17/18 复用真实数据)
|
||||
- runserver / http.server 后台进程已终止
|
||||
Reference in New Issue
Block a user