test(server): v1/v2混合停留聚合回归+聚合点核查适配

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-18 12:12:03 +08:00
parent 5e024b814b
commit 0cb8c6fb0c
3 changed files with 70 additions and 1 deletions
+1 -1
View File
@@ -68,7 +68,7 @@ class VisitSession(models.Model):
landing_path = models.CharField(max_length=500, blank=True, default="")
event_count = models.PositiveIntegerField(default=0)
duration_ms = models.PositiveBigIntegerField(default=0) # 会话内事件时间跨度
stay_ms = models.PositiveBigIntegerField(default=0) # 有效停留累计(page_stay active_ms 之和)
stay_ms = models.PositiveBigIntegerField(default=0) # 有效停留(v1 active_ms 加法 + v2 page_id 区间并集,落库后重算回写)
resume_of = models.ForeignKey("self", null=True, blank=True, on_delete=models.SET_NULL,
related_name="resumes") # schema v2:session_resume 续接的前一会话(被指向会话删除后置 NULL 保留链)
+27
View File
@@ -4,6 +4,8 @@ from django.contrib.auth import get_user_model
from django.utils import timezone
from rest_framework.test import APIClient
from tracker.models import DailyStat, Event, Site, SourceClassification, VisitSession, Visitor
from tracker.services.ingest import ingest_batch
from tracker.tests.test_ingest import SITE_ID, _batch, _ev, _v2_batch, _v2_event
pytestmark = pytest.mark.django_db
User = get_user_model()
@@ -263,3 +265,28 @@ def test_overview_site_filter_and_404(client, member_user):
assert r.status_code == 404
body = r.json()
assert body["code"] == 404 and body["data"] is None and body["message"] == "site_not_found"
def test_overview_avg_stay_mixed_v1_v2_session(client, member_user):
"""v1+v2 混合会话回归(阶段A T4):avg_stay_ms 读 VisitSession.stay_ms 重算值。
经真实 ingest 落库——v1 page_stay active_ms=8000(加法)+ v2 同 page_id 相邻区间
[0,15000][15000,30000](并集=30000,非加法 60000)→ 会话 stay_ms=38000;
单会话 avg=38000/1。聚合点不直接 SUM(Event.properties.active_ms),
T3 重算式回写后读数侧自动正确——本用例钉死该语义不被回归。"""
_auth(client, member_user)
site = Site.objects.create(name="s", domains=["a.com"], site_id=SITE_ID)
ingest_batch(site, _batch( # v1 批:建会话 sm + v1 停留增量
[_ev(1), _ev(2, "page_stay", properties={"active_ms": 8000, "path": "/p"})],
sid="sm"))
r = ingest_batch(site, _v2_batch([ # v2 批:同会话同 page_id 两段相邻区间
_v2_event("page_stay", "p1", "sm", seq=1,
active_from=0, active_to=15000, flush_reason="periodic"),
_v2_event("page_stay", "p1", "sm", seq=2,
active_from=15000, active_to=30000, flush_reason="periodic")],
sid="sm"))
assert (r["accepted"], r["rejected"]) == (2, [])
assert VisitSession.objects.get(session_id="sm").stay_ms == 38000 # 写侧:v1 SUM + v2 并集
d = client.get("/api/admin/dashboard/overview", {"range": "today"}).json()["data"]
assert d["cards"]["visits"]["value"] == 1
assert d["cards"]["avg_stay_ms"] == {"value": 38000, "delta_pct": None} # 38000/1;上期无会话 → None
+42
View File
@@ -4,6 +4,8 @@ from django.contrib.auth import get_user_model
from django.utils import timezone
from rest_framework.test import APIClient
from tracker.models import Event, Site, SourceClassification, VisitSession, Visitor
from tracker.services.ingest import ingest_batch
from tracker.tests.test_ingest import SITE_ID, _batch, _ev, _v2_batch, _v2_event
pytestmark = pytest.mark.django_db
User = get_user_model()
@@ -474,3 +476,43 @@ def test_device_model_and_env_extra_fields(client, member_user):
assert d["env_extra"]["network_rtt"] == "50"
d = client.get("/api/admin/visitors/v-old").json()["data"]
assert d["device_model"] == "" and d["env_extra"] == {}
def test_mixed_v1_v2_stay_totals_and_detail(client, member_user):
"""v1+v2 混合会话回归(阶段A T4):total_stay_ms/会话 stay_ms 均读 VisitSession.stay_ms
重算值(v1 active_ms=8000 加法 + v2 同 page_id 区间 [0,15000][15000,30000] 并集 30000
= 38000);详情/单会话/时间线对多条 page_stay 事件正常返回不崩——v2 区间事件
properties 无 active_ms,时间线合并展示为 0 累加(展示层不炸、不虚增)。"""
_auth(client, member_user)
site = Site.objects.create(name="s", domains=["a.com"], site_id=SITE_ID)
ingest_batch(site, _batch(
[_ev(1), _ev(2, "page_stay", properties={"active_ms": 8000, "path": "/p"})], sid="sm"))
ingest_batch(site, _v2_batch([
_v2_event("page_stay", "p1", "sm", seq=1,
active_from=0, active_to=15000, flush_reason="periodic"),
_v2_event("page_stay", "p1", "sm", seq=2,
active_from=15000, active_to=30000, flush_reason="periodic")],
sid="sm"))
sess = VisitSession.objects.get(session_id="sm")
assert sess.stay_ms == 38000 and sess.event_count == 4 # 写侧:v1 SUM + v2 并集
d = client.get("/api/admin/visitors").json()["data"] # 列表:Sum(sessions__stay_ms)
assert d["total"] == 1
assert d["list"][0]["total_stay_ms"] == 38000 and d["list"][0]["session_count"] == 1
d = client.get("/api/admin/visitors/v1").json()["data"] # 详情:行字段 + 会话摘要
assert d["total_stay_ms"] == 38000
assert [s["stay_ms"] for s in d["sessions"]] == [38000]
d = client.get("/api/admin/visitors/v1/sessions/sm").json()["data"] # 单会话:多条 page_stay 正常
assert d["stay_ms"] == 38000 and d["event_count"] == 4
# 4 条事件(1 page_view + 3 page_stay)合并成一条:v1 的 8000 并入 page_view,
# v2 两段区间事件无 active_ms → 展示层累加 0(真实停留看 stay_ms=38000)
assert [(e["event_name"], e["path"], e["active_ms"]) for e in d["events"]] == \
[("page_view", "/p", 8000)]
r = client.get("/api/admin/visitors/v1/timeline") # 时间线:同款合并不崩
assert r.status_code == 200
evs = r.json()["data"]["days"][0]["sessions"][0]["events"]
assert [(e["event_name"], e["path"], e["active_ms"]) for e in evs] == \
[("page_view", "/p", 8000)]