fix: ingest 同批去重/会话族切分修复/xdb 无关测试/边界加固
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
from tracker.models import Site, Visitor, VisitSession, Event
|
||||
from tracker.services.geo import resolver as _geo
|
||||
from tracker.services.ingest import ingest_batch
|
||||
|
||||
NOW = timezone.now()
|
||||
@@ -26,7 +27,8 @@ def test_first_batch_creates_visitor_session_events(db):
|
||||
v = Visitor.objects.get(site=site, visitor_id="v1")
|
||||
assert v.first_referrer_domain == "google.com" and v.visit_count == 1
|
||||
assert v.active_days == 1 and v.last_ip == "114.114.114.114"
|
||||
assert v.country == "中国" and v.city == "南京市" # 真实 xdb(114 DNS,江苏南京);无 xdb 环境改为断言不抛异常即可
|
||||
if _geo._searcher: # 地理断言仅在有真实 xdb 时执行,CI/fresh clone 不因缺库变红
|
||||
assert v.country == "中国" and v.city == "南京市" # 114 DNS(江苏南京)
|
||||
assert Event.objects.filter(site=site).count() == 2
|
||||
|
||||
# 2. 重放幂等 + 不抬高会话聚合
|
||||
@@ -83,3 +85,52 @@ def test_batch_size_invalid(db):
|
||||
site = Site.objects.create(name="s", domains=["a.com"])
|
||||
r = ingest_batch(site, _batch([_ev(i) for i in range(60)]))
|
||||
assert r["rejected"] == [{"reason": "batch_size_invalid"}]
|
||||
|
||||
# 9. 同批重复 event_id:去重后只入一份,聚合不灌倍
|
||||
def test_intra_batch_duplicate_event_id_deduped(db):
|
||||
site = Site.objects.create(name="s", domains=["a.com"])
|
||||
evs = [_ev(1, "page_stay", properties={"active_ms": 1000, "path": "/p"})
|
||||
for _ in range(3)] # 3 份同 id
|
||||
r = ingest_batch(site, _batch(evs))
|
||||
assert (r["accepted"], r["duplicated"]) == (1, 2)
|
||||
sess = VisitSession.objects.get()
|
||||
assert sess.event_count == 1 and sess.stay_ms == 1000
|
||||
|
||||
# 10. 切分后同 sid 小间隔连发:空闲比较取会话族最大 ended_at,不再每批新开段
|
||||
def test_split_then_small_gap_reuses_segment(db):
|
||||
site = Site.objects.create(name="s", domains=["a.com"])
|
||||
ingest_batch(site, _batch([_ev(1, occurred_at=NOW - timedelta(minutes=90))]))
|
||||
ingest_batch(site, _batch([_ev(2)])) # 90min 间隔 → 切出 s1#2
|
||||
ingest_batch(site, _batch([_ev(3, occurred_at=NOW + timedelta(minutes=1))]))
|
||||
ingest_batch(site, _batch([_ev(4, occurred_at=NOW + timedelta(minutes=2))]))
|
||||
assert VisitSession.objects.count() == 2 # 保持 s1 与 s1#2,不切 s1#3/s1#4
|
||||
assert Visitor.objects.get(visitor_id="v1").visit_count == 2
|
||||
|
||||
# 11. invariant 4:乱序批次(后入更早发生)ended_at 不回退
|
||||
def test_ended_at_never_regresses_on_out_of_order_batch(db):
|
||||
site = Site.objects.create(name="s", domains=["a.com"])
|
||||
ingest_batch(site, _batch([_ev(1, occurred_at=NOW + timedelta(minutes=1))]))
|
||||
ingest_batch(site, _batch([_ev(2, occurred_at=NOW)]))
|
||||
sess = VisitSession.objects.get()
|
||||
assert abs((sess.ended_at - (NOW + timedelta(minutes=1))).total_seconds()) < 1
|
||||
|
||||
# 12. 首触持久:第二批换 referrer 不改首触字段
|
||||
def test_first_touch_immutable_across_batches(db):
|
||||
site = Site.objects.create(name="s", domains=["a.com"])
|
||||
ingest_batch(site, _batch([_ev(1)]))
|
||||
b = _batch([_ev(2)])
|
||||
b["context"]["referrer_domain"] = "bing.com"
|
||||
ingest_batch(site, b)
|
||||
v = Visitor.objects.get(visitor_id="v1")
|
||||
assert v.first_referrer_domain == "google.com"
|
||||
assert v.first_utm == {"utm_source": "google"}
|
||||
|
||||
# 13. payload 缺 visitor_id/session_id 提前拒绝
|
||||
def test_missing_payload_fields_rejected(db):
|
||||
site = Site.objects.create(name="s", domains=["a.com"])
|
||||
p = _batch([_ev(1)])
|
||||
del p["visitor_id"]
|
||||
assert ingest_batch(site, p)["rejected"] == [{"reason": "invalid_payload"}]
|
||||
p = _batch([_ev(1)])
|
||||
del p["session_id"]
|
||||
assert ingest_batch(site, p)["rejected"] == [{"reason": "invalid_payload"}]
|
||||
|
||||
Reference in New Issue
Block a user