docs: M1 验收记录(本地全栈 A 部分 8/8,用户项清单)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# M1 验收记录(本地全栈端到端)
|
||||
|
||||
- 日期:2026-09-15
|
||||
- 分支:m1(HEAD `7ed55c8`)
|
||||
- 环境:Windows / Django runserver 127.0.0.1:8000(sqlite 开发模式)+ `python -m http.server 18080`(根目录 sdk/)+ admin Vite dev 127.0.0.1:3006(代理 /api → 8000)
|
||||
- 方法:Playwright 当"真实访客"走真实 SDK 产物(sdk/dist/v1.js,4648B);curl 验证信封/幂等/限流;`manage.py shell` 断言 DB;admin UI 与 API 对拍
|
||||
- 端口说明:计划端口 8080 被本机 Docker Desktop 后端占用(Task 16 已知),按预案改用 18080;demo 页 endpoint 为绝对地址 `http://127.0.0.1:8000/api/collect/batch`,不受影响
|
||||
- 测试数据基线:验收前库内已有 Task 16 残留访客 `b507cb61-…`、`curl_v1`(访客 2 / 事件 11 / 会话 2);本次新增访客 `e0456de2-…`、`curl_v2`
|
||||
|
||||
## ⚠️ 治理时间线(用户须先读)
|
||||
|
||||
**M1 采集开始日期 = 真实网站接入日(B-1 执行当日,待用户回填:____年__月__日)。**
|
||||
IP 截断(存储 IP 截短为前缀)任务**必须在该日期起 180 天内上线**,到期前请跟踪。本地全栈验收日为 2026-09-15(不含真实用户数据,不启动该时钟)。
|
||||
|
||||
## A. 本地全栈自动化验收(8/8 PASS)
|
||||
|
||||
### A1 新访客首次访问 — PASS
|
||||
- 操作:清空 localStorage 后打开 `http://127.0.0.1:18080/demo/index.html?utm_source=google&utm_medium=test`,点击页面(鼠标活动)停留约 20s(>15s 触发首批 flush)
|
||||
- 网络:`POST http://127.0.0.1:8000/api/collect/batch => 200`(Network 面板实录)
|
||||
- 后台访客列表新增 `e0456de2-ce7e-4580-be64-a6b29bda1f73`(与浏览器 `localStorage.ctk_vid` 一致),first_seen 21:44:25 == 页面打开时刻:
|
||||
- 环境:PC / Windows / Chrome / zh-CN / 1920x1080(详情页显示一致)
|
||||
- 来源:first_source=`google`,first_utm={utm_source:google, utm_medium:test},落地页含 utm 参数
|
||||
- 地理:country/province/city 全空——本地 IP 127.0.0.1 无地理归属,属预期
|
||||
- visit_count=1、active_days=1、session_count=1、online=true
|
||||
- 说明:首次打开复用了 Task 16 的 Playwright 浏览器 profile(`ctk_vid=b507cb61…` 仍在),该访客被识别为回访(visit_count 1→2)——恰证明跨会话身份保持是产品能力;随后清空存储重开才得到"新访客"。副作用:`b507cb61…` 的 visit_count 被 +1(历史测试数据,不影响本次断言)
|
||||
|
||||
### A2 浏览 3 页各停留 — PASS
|
||||
- 操作:index(21:44:25 起 ~64s)→ 点链接去 page2(21:45:29 起 ~31s)→ 返回 index(21:46:00 起 ~98s);每页期间点击标题保持鼠标活动;每页 >15s 触发定时 flush,导航时 pagehide→sendBeacon 发 page_stay
|
||||
- 时间线(API `visitors/{id}/timeline`,服务端已把 page_stay 合并进同 path 的 page_view):
|
||||
- page_view ×3:index?utm…(active_ms 45000)/ page2(30000)/ index(34000),均 >10s ✓
|
||||
- 会话 stay_ms=160000(=45+30+34+51s,含一条独立成条的 page_stay,见"发现 2")
|
||||
- page_stay 两种触发均验证:真实导航(pagehide)与合成 visibilitychange(hidden)
|
||||
|
||||
### A3 再次访问同访客(清 ctk_sid/ctk_act,保留 ctk_vid)— PASS
|
||||
- 操作:evaluate 删除 `ctk_sid`、`ctk_act`(`ctk_vid` 保留)→ reload
|
||||
- 结果:visit_count 1→2、session_count 1→2;时间线同一天(2026-09-15)出现**第 2 个会话组**:21:44(来源 google)与 21:47(来源 直接访问);admin 详情页显示 访问次数 2 / 会话数 2 / 总停留 3分14秒
|
||||
|
||||
### A4 幂等(curl 重放同批次)— PASS
|
||||
```
|
||||
首发:{"accepted":1,"duplicated":0} 重放:{"accepted":0,"duplicated":1}
|
||||
DB:event_id aaaaaaaa-bbbb-…-0001 恒为 1 行
|
||||
```
|
||||
- 加压复验:同一批次连发 121 次(幂等批次不产生新行)→ 118×200(均 duplicated=1) + 3×429(限流),DB 该 event_id 仍 1 行、事件总数无增长——重放风暴既不重复入库也不放大写放大
|
||||
|
||||
### A5 伪造 Origin → 403;超频 → 429 — PASS
|
||||
- `Origin: http://evil.example.com` + 合法批次体 → `403 {"code":403,"message":"not_allowed"}`(ACAO 回显错误体,跨域 SDK 可读)
|
||||
- `Origin: http://127.0.0.1:18080`(白名单)→ `200` + `Access-Control-Allow-Origin: http://127.0.0.1:18080`
|
||||
- 限流(collect 120/min/匿名 IP):
|
||||
- 空体突发 122 发 → 117×403(无 site_id)+ **5×429**
|
||||
- 幂等批次突发 121 发 → 118×200 + **3×429**
|
||||
- 捕获 429 响应全貌:`HTTP/1.1 429` + `Retry-After: 23` + 信封体 `{"code":429,"data":null,"message":"请求已被限流。 预计 23 秒后可用。"}`
|
||||
- 与 `tracker/tests/test_collect.py` 限流用例互证(阈值语义已由测试固化)
|
||||
- 口径备注:无 Origin 时能否上报由站点 `allow_no_origin` 决定(site_x=True);空 JSON 体(`{}`)因解析不出 site_id 也走 403;非法 JSON 报 400 invalid_json——均为 collect.py 既定检查顺序,非缺陷
|
||||
|
||||
### A6 Dashboard 对拍 — PASS
|
||||
- 同分钟快照:admin「数据总览」UI 今日 PV 27 / UV 4 / 会话 6 / 新访客 4 / 在线 1 == curl `dashboard/overview` today `{pv:27,uv:4,sessions:6,new_visitors:4,active_now:1}`;热门页面两张完全一致(3/3/2/1,路径与排序相同)
|
||||
- 访客"总停留"== 各会话 stay_ms 之和(shell 聚合 vs API 列表 total_stay_ms,4/4 访客全等):
|
||||
```
|
||||
e0456de2… API 194000ms == Σ(160000+34000) b507cb61… API 58000ms == Σ(58000+0)
|
||||
curl_v1 / curl_v2 API 0 == Σ 0
|
||||
```
|
||||
- 口径备注:overview 的 pv 统计**全事件**(stats.py 明示"pv/uv 均从事件表统计",test_dashboard 固化),top_pages 的 pv 只计 page_view——两者非同一口径,属设计非 bug。截图:`.playwright-mcp/验收-A6-数据总览.png`
|
||||
|
||||
### A7 SDK 性能 — PASS
|
||||
- 异步非阻塞:demo 页 `<script async src="../dist/v1.js">`(DOM 实测 async=true);Performance API:DOMContentLoaded 7ms / load 8ms,SDK 加载不阻塞渲染;`ctk.q` 命令队列在 async 脚本就绪后被正确消费(批次 site_id=site_x 到达服务端可证)
|
||||
- 体积:dist/v1.js 原始 4648B,gzip(9) 2167B ≤ 15KB 红线(本地 http.server 无压缩,gzip 为离线实测;生产 nginx gzip 后同量级)
|
||||
|
||||
### A8 admin 全链路 smoke — PASS
|
||||
- 路径:清 token → 登录页(admin / 滑块验证拖到底)→ 登录成功 → 数据总览 → 访客管理(列表)→ 访客详情(轨迹 tab:跨天时间线、日期/会话双层分组;会话 tab:会话列表 + 会话详情弹窗)→ 站点管理(site_x「demo」启用中,域名 127.0.0.1)
|
||||
- Console:admin 全程 **0 error / 2 warning**(全程唯一 error 为 demo 静态站 favicon.ico 404,系 `python -m http.server` 无 favicon 文件,与 SDK/admin 无关)
|
||||
- 截图:`.playwright-mcp/验收-A8-访客详情时间线.png`(含 21:44/21:47 两会话组与停留秒数)
|
||||
|
||||
## B. 用户手动项清单(待用户执行)
|
||||
|
||||
| # | 事项 | 步骤与依据 | 完成打勾 |
|
||||
|---|---|---|---|
|
||||
| B-1 | **真实网站接入(WordPress 或任意站)** | ① 按 `deploy/README-deploy.md` **§6** 后台建站点拿 site_id;② 按 **§7** 目标站 `<head>` 贴两行接入代码(endpoint/SDK 地址换正式域名);③ **隐私告知前置**:先在目标站挂隐私/Cookie 告知并获得访客同意,再启用采集——治理底线"先上告知再采集",顺序不可颠倒;④ 回填上方"M1 采集开始日期" | ☐ |
|
||||
| B-2 | **次日回访复验** | 次日用同一浏览器访问目标站(或本地 demo)→ 后台该访客:时间线出现**新日期组**、visit_count+1、active_days+1。当天无法模拟:服务端对 occurred_at 偏差 >120s 标 late、**>24h 直接拒绝**(`occurred_at_out_of_window`,constants.py `CLOCK_SKEW_REJECT_SEC=86400`)——防篡改设计,伪造时间戳走不通 | ☐ |
|
||||
| B-3 | **1Panel 生产部署 + HTTPS + 定时任务** | 按 `deploy/README-deploy.md`:§1 前置构建(admin/sdk dist + ip2region xdb)→ §2 MySQL 准备 → §3 配 .env(fail-fast 护栏)→ §4 compose 部署/建管理员 → §5 HTTPS(推荐 1Panel 反代+Let's Encrypt);**§8 定时任务**:1Panel 计划任务每日 00:10 `docker exec tracker-web python manage.py refresh_stats --days 2`(不做则趋势图历史全零);上线后浏览 §9 JWT 应急、§10 CDN/WAF 的 NUM_PROXIES 调整 | ☐ |
|
||||
| B-4 | **记录 M1 采集开始日期并跟踪 IP 截断 180 天期限** | B-1 完成当日回填本文档顶部日期;IP 截断功能须不晚于该日 +180 天上线(建议即刻立日历提醒) | ☐ |
|
||||
|
||||
## 发现与说明(均非缺陷,留档备查)
|
||||
|
||||
1. **Playwright profile 跨任务持久**:`ctk_vid` 存 localStorage,浏览器复用会以"回访访客"身份进门——测试新访客须先清存储;真实用户场景这正是跨会话身份保持的正确表现。
|
||||
2. **清存储时机与临终 page_stay 归属**:先 `localStorage.clear()` 再导航,旧页面 pagehide 发出的 page_stay 会按"flush 时读取的身份"落到新 visitor_id/新 session 下(本例 51s 独立成条)。真实用户无此操作序列(不会中途清存储),SDK 行为符合设计(身份在发送时解析);测试 A3 因此改为"先合成 visibilitychange 发完 page_stay、再清 sid/act reload"。
|
||||
3. **GBK 管道乱码**:Git Bash 下 `curl | python` 直读中文 JSON 会按 cp936 误解 UTF-8(如"直接访问"显示为乱码);加 `PYTHONIOENCODING=utf-8` 后核对无误,**服务端返回的始终是正确 UTF-8**(已抓包对字节核实)。工具链问题,非产品问题。
|
||||
4. **登录滑块验证码对自动化不友好**:Playwright `dragTo` 默认距离差几像素不触发成功(组件要求拖满 `宽度-滑块高`),需分步 mouse 移动拖到最右端。模板组件行为,不影响人工使用;如需常做 UI 自动化可考虑测试模式下禁用验证码。
|
||||
5. **8080 端口**:本机被 Docker Desktop 后端占用(WinError 10013 / PID 38864),非本任务进程,未处置;demo 静态站按预案用 18080。
|
||||
6. **favicon.ico 404**:`python -m http.server` 无该文件导致 demo 站 console 一条 404;生产 nginx 配置已含 favicon(deploy/nginx.conf),无需处理。
|
||||
|
||||
## 结论与遗留
|
||||
|
||||
- **A 部分 8/8 PASS**,无需修复项;未发现产品代码缺陷,未改产品代码(server 89 / sdk 46 测试基线不受影响)。
|
||||
- 遗留问题:
|
||||
- B 部分四项待用户执行(真实接入 / 次日复验 / 生产部署 / 采集开始日期登记)。
|
||||
- Task 16 发现 5(timezone_name 恒空)已在后续代码修复并经本次验收实证:ingest.py `_touch_visitor` 显式映射 `ctx.timezone → timezone_name`,新访客 e0456de2 落库 `Asia/Shanghai`(curl 访客无该字段为空,符合预期)。旧访客 b507cb61 创建于修复前,环境字段只在首建时定格,保持空属既定行为。
|
||||
- 收尾:打 tag `m1-beta`;验收用 runserver / http.server 进程已终止;admin Vite dev(3006,验收前已在本机运行,非本次启动)保留原状。
|
||||
Reference in New Issue
Block a user