diff --git a/docs/联调记录-m1-task16.md b/docs/联调记录-m1-task16.md new file mode 100644 index 0000000..13f4118 --- /dev/null +++ b/docs/联调记录-m1-task16.md @@ -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 页 `