fix: 访客API 微收口(duration_ms 移除/truncated 标记/锁定测试/source 收敛)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user