fix: 访客API 微收口(duration_ms 移除/truncated 标记/锁定测试/source 收敛)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-15 15:38:58 +08:00
parent 9a2cd76e99
commit fa90835ffa
2 changed files with 63 additions and 24 deletions
+44 -7
View File
@@ -162,9 +162,9 @@ def test_detail_shape(client, member_user):
assert [s["session_id"] for s in d["sessions"]] == ["s-new", "s-old"] # 最近在前
srow = d["sessions"][0]
assert set(srow) == {"session_id", "started_at", "ended_at", "referrer_domain", "utm",
"landing_path", "event_count", "duration_ms", "stay_ms"}
"landing_path", "event_count", "stay_ms"} # duration_ms 内部字段不展示
assert srow["utm"] == {"utm_source": "wechat"}
assert srow["stay_ms"] == 30000 and srow["duration_ms"] == 120000 and srow["event_count"] == 2
assert srow["stay_ms"] == 30000 and srow["event_count"] == 2
def test_timeline_day_grouped_and_stay_merge(client, member_user):
@@ -184,7 +184,10 @@ def test_timeline_day_grouped_and_stay_merge(client, member_user):
r = client.get("/api/admin/visitors/v1/timeline")
assert r.status_code == 200
groups = r.json()["data"]
data = r.json()["data"]
assert set(data) == {"days", "truncated"}
assert data["truncated"] is False # 未触 1000 上限
groups = data["days"]
assert [g["date"] for g in groups] == [TODAY.isoformat(), YESTERDAY.isoformat()] # 日期组倒序
today_g, yest_g = groups
assert set(today_g) == {"date", "sessions"}
@@ -214,9 +217,9 @@ def test_timeline_days_param_and_404(client, member_user):
r = client.get("/api/admin/visitors/v1/timeline", {"days": "200"})
assert r.status_code == 200 # 超界夹紧到 90,不报错
assert [g["date"] for g in r.json()["data"]] == [TODAY.isoformat(), YESTERDAY.isoformat()]
assert [g["date"] for g in r.json()["data"]["days"]] == [TODAY.isoformat(), YESTERDAY.isoformat()]
r = client.get("/api/admin/visitors/v1/timeline", {"days": "1"})
assert [g["date"] for g in r.json()["data"]] == [TODAY.isoformat()] # 只含今天窗口
assert [g["date"] for g in r.json()["data"]["days"]] == [TODAY.isoformat()] # 只含今天窗口
r = client.get("/api/admin/visitors/v1/timeline", {"days": "abc"})
assert r.status_code == 200 # 非法 days → 默认 30
r = client.get("/api/admin/visitors/nope/timeline")
@@ -242,10 +245,10 @@ def test_session_detail_ordered(client, member_user):
assert r.status_code == 200
d = r.json()["data"]
assert set(d) == {"session_id", "started_at", "ended_at", "referrer_domain", "utm",
"landing_path", "event_count", "duration_ms", "stay_ms", "events"}
"landing_path", "event_count", "stay_ms", "events"} # duration_ms 不展示
assert d["session_id"] == "s1" and d["referrer_domain"] == "d.com"
assert d["landing_path"] == "/start" and d["event_count"] == 5
assert d["duration_ms"] == 90000 and d["stay_ms"] == 45000
assert d["stay_ms"] == 45000
evs = d["events"]
assert [e["event_name"] for e in evs] == ["page_stay", "sdk_loaded", "page_view", "page_stay"] # occurred_at 正序
assert [e["path"] for e in evs] == ["/x", "/", "/a", "/b"]
@@ -253,3 +256,37 @@ def test_session_detail_ordered(client, member_user):
r = client.get("/api/admin/visitors/v1/sessions/nope")
assert r.status_code == 404 and r.json()["message"] == "session_not_found"
def test_timeline_truncates_at_1000(client, member_user):
_auth(client, member_user)
site = Site.objects.create(name="s", domains=["a.com"])
v = _visitor(site, "v1")
s = _session(site, v, "s1", at=NOW - timedelta(hours=3))
base = NOW - timedelta(hours=3) # 跨不跨天皆可,按 path 集合断言不依赖分组形态
Event.objects.bulk_create([Event(site=site, visitor=v, session=s,
event_id=f"e{i}", event_name="page_view",
occurred_at=base + timedelta(seconds=i), path=f"/p{i}") for i in range(1001)])
r = client.get("/api/admin/visitors/v1/timeline")
assert r.status_code == 200
d = r.json()["data"]
assert d["truncated"] is True # 触上限(前端可区分"没动静"和"被截断")
paths = [e["path"] for g in d["days"] for sess in g["sessions"] for e in sess["events"]]
assert len(paths) == 1000 # 条数封顶
assert "/p0" not in paths # 最旧一条被截掉
assert "/p1000" in paths # 最新保留
def test_timeline_orphan_events_without_session(client, member_user):
_auth(client, member_user)
site = Site.objects.create(name="s", domains=["a.com"])
v = _visitor(site, "v1")
_event(site, v, None, "o1", "sdk_loaded", NOW, path="/") # session=NULL 孤立事件
_event(site, v, None, "o2", "page_stay", NOW + timedelta(seconds=1), path="/x", active_ms=100)
r = client.get("/api/admin/visitors/v1/timeline")
assert r.status_code == 200 # 不崩溃,归入 session_id=null 桶
sess = r.json()["data"]["days"][0]["sessions"][0]
assert sess["session_id"] is None and sess["started_at"] is None
assert [e["event_name"] for e in sess["events"]] == ["sdk_loaded", "page_stay"]