86 lines
4.1 KiB
Python
86 lines
4.1 KiB
Python
|
|
from datetime import timedelta
|
|||
|
|
from django.utils import timezone
|
|||
|
|
from tracker.models import Site, Visitor, VisitSession, Event
|
|||
|
|
from tracker.services.ingest import ingest_batch
|
|||
|
|
|
|||
|
|
NOW = timezone.now()
|
|||
|
|
|
|||
|
|
def _batch(events, vid="v1", sid="s1"):
|
|||
|
|
return {"visitor_id": vid, "session_id": sid, "context": {
|
|||
|
|
"referrer_domain": "google.com", "utm": {"utm_source": "google"},
|
|||
|
|
"path": "/landing", "device_type": "PC", "os": "Windows",
|
|||
|
|
"browser": "Chrome", "language": "en-US", "screen": "1920x1080"},
|
|||
|
|
"events": events}
|
|||
|
|
|
|||
|
|
def _ev(i, name="page_view", **kw):
|
|||
|
|
d = dict(event_id=f"e{i}", event_name=name, occurred_at=NOW,
|
|||
|
|
path="/p", title="T", properties={})
|
|||
|
|
d.update(kw)
|
|||
|
|
return d
|
|||
|
|
|
|||
|
|
# 1. 首批建档
|
|||
|
|
def test_first_batch_creates_visitor_session_events(db):
|
|||
|
|
site = Site.objects.create(name="s", domains=["a.com"])
|
|||
|
|
r = ingest_batch(site, _batch([_ev(1), _ev(2)]), ip="114.114.114.114")
|
|||
|
|
assert (r["accepted"], r["duplicated"], r["rejected"]) == (2, 0, [])
|
|||
|
|
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 环境改为断言不抛异常即可
|
|||
|
|
assert Event.objects.filter(site=site).count() == 2
|
|||
|
|
|
|||
|
|
# 2. 重放幂等 + 不抬高会话聚合
|
|||
|
|
def test_duplicate_events_never_inflate_aggregates(db):
|
|||
|
|
site = Site.objects.create(name="s", domains=["a.com"])
|
|||
|
|
ingest_batch(site, _batch([_ev(1), _ev(2, "page_stay", properties={"active_ms": 3000, "path": "/p"})]))
|
|||
|
|
r = ingest_batch(site, _batch([_ev(1)])) # 重放
|
|||
|
|
assert r["duplicated"] == 1 and Event.objects.count() == 2
|
|||
|
|
sess = VisitSession.objects.get()
|
|||
|
|
assert sess.event_count == 2 and sess.stay_ms == 3000 # 重放没有重复加分
|
|||
|
|
|
|||
|
|
# 3. 新会话递增 visit_count(F 表达式)
|
|||
|
|
def test_new_session_increments_visit_count(db):
|
|||
|
|
site = Site.objects.create(name="s", domains=["a.com"])
|
|||
|
|
ingest_batch(site, _batch([_ev(1)], sid="s1"))
|
|||
|
|
ingest_batch(site, _batch([_ev(2)], sid="s2"))
|
|||
|
|
v = Visitor.objects.get(visitor_id="v1")
|
|||
|
|
assert v.visit_count == 2 and VisitSession.objects.count() == 2
|
|||
|
|
|
|||
|
|
# 4. 服务端 30 分钟兜底切分(同 sid 间隔>30min 切出 s1#2,且同批后续事件复用同段)
|
|||
|
|
def test_server_backstop_splits_idle_gap(db):
|
|||
|
|
site = Site.objects.create(name="s", domains=["a.com"])
|
|||
|
|
ingest_batch(site, _batch([_ev(1, occurred_at=NOW - timedelta(minutes=90))]))
|
|||
|
|
r = ingest_batch(site, _batch([_ev(2), _ev(3)])) # 两条同批
|
|||
|
|
assert VisitSession.objects.count() == 2 # s1 与 s1#2,不会切成 3 段
|
|||
|
|
assert r["accepted"] == 2
|
|||
|
|
|
|||
|
|
# 5. 拒绝事件带原因上报
|
|||
|
|
def test_rejected_events_reported_with_reason(db):
|
|||
|
|
site = Site.objects.create(name="s", domains=["a.com"])
|
|||
|
|
r = ingest_batch(site, _batch([_ev(1, event_name="bad"), _ev(2)]))
|
|||
|
|
assert r["accepted"] == 1 and r["rejected"][0]["reason"] == "event_name_not_allowed"
|
|||
|
|
|
|||
|
|
# 6. page_stay 的 active_ms 累计进 stay_ms
|
|||
|
|
def test_stay_ms_accumulates(db):
|
|||
|
|
site = Site.objects.create(name="s", domains=["a.com"])
|
|||
|
|
ingest_batch(site, _batch([
|
|||
|
|
_ev(1), _ev(2, "page_stay", properties={"active_ms": 5000, "path": "/p"})]))
|
|||
|
|
ingest_batch(site, _batch([
|
|||
|
|
_ev(3, occurred_at=NOW + timedelta(minutes=1)),
|
|||
|
|
_ev(4, "page_stay", properties={"active_ms": 7000, "path": "/p"},
|
|||
|
|
occurred_at=NOW + timedelta(minutes=1))]))
|
|||
|
|
assert VisitSession.objects.get().stay_ms == 12000
|
|||
|
|
|
|||
|
|
# 7. 爬虫 UA 标记(访客+事件)
|
|||
|
|
def test_bot_ua_marked(db):
|
|||
|
|
site = Site.objects.create(name="s", domains=["a.com"])
|
|||
|
|
ingest_batch(site, _batch([_ev(1)]), ip="66.249.66.1", ua="Mozilla/5.0 (compatible; Googlebot/2.1)")
|
|||
|
|
assert Visitor.objects.get().is_bot is True
|
|||
|
|
assert Event.objects.get().is_bot is True
|
|||
|
|
|
|||
|
|
# 8. 超批拒绝
|
|||
|
|
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"}]
|