from datetime import date, timedelta from django.utils import timezone from tracker.models import Site, Visitor, VisitSession, Event from tracker.services.stats import refresh_daily # 用上海本地时刻做基准:refresh_daily 的日界是上海自然日(make_aware), # 若直接用 UTC 的 timezone.now(),在本地 00:00-08:00 之间跑测试时 NOW.date() 会错一天 NOW = timezone.localtime(timezone.now()) YESTERDAY = NOW - timedelta(days=1) def _mk(site, vid, sid, day, n_events, stay=0): v, _ = Visitor.objects.get_or_create(site=site, visitor_id=vid) sess = VisitSession.objects.create(site=site, visitor=v, session_id=sid, started_at=day, ended_at=day + timedelta(minutes=5), stay_ms=stay, event_count=n_events) for i in range(n_events): Event.objects.create(site=site, visitor=v, session=sess, event_id=f"{vid}-{sid}-{day:%Y%m%d}-{i}", event_name="page_view", occurred_at=day + timedelta(seconds=i), path="/") def test_refresh_daily_two_days(db): site = Site.objects.create(name="s", domains=["a.com"]) _mk(site, "v1", "s1", YESTERDAY, 3) # v1 昨天 3 事件 _mk(site, "v1", "s2", NOW, 2, stay=8000) # v1 今天 2 事件 _mk(site, "v2", "s3", NOW, 4) # v2 今天 4 事件 y = refresh_daily(site, YESTERDAY.date()) t = refresh_daily(site, NOW.date()) # new_visitors 口径 = first_seen_at 落入当日。测试里 v1 虽有昨日事件,但访客记录是测试运行时(今天)建档的 # (auto_now_add),故昨天 new=0、今天 new=2(v1+v2)——按 first_seen_at 真实语义断言 assert (y.pv, y.uv, y.sessions, y.new_visitors) == (3, 1, 1, 0) assert (t.pv, t.uv, t.sessions, t.new_visitors) == (6, 2, 2, 2) # 今天:pv=6 uv=2(v1+v2) sessions=2 new=2(v1+v2) def test_refresh_daily_shanghai_boundary(db): # 固定时间戳钉死日界(不依赖运行时刻):UTC 2026-09-15T16:30 = 上海 09-16 00:30 → 落 9-16 不落 9-15 site = Site.objects.create(name="s", domains=["a.com"]) v = Visitor.objects.create(site=site, visitor_id="v") s = VisitSession.objects.create(site=site, visitor=v, session_id="s", started_at="2026-09-15T16:30:00Z", ended_at="2026-09-15T16:30:00Z") Event.objects.create(site=site, visitor=v, session=s, event_id="e", event_name="page_view", occurred_at="2026-09-15T16:30:00Z", path="/") d15 = refresh_daily(site, date(2026, 9, 15)) d16 = refresh_daily(site, date(2026, 9, 16)) assert (d15.pv, d16.pv) == (0, 1) def test_refresh_daily_excludes_bots(db): # 爬虫口径:事件/会话/访客三处 is_bot 均不计入四指标(与 pv/uv/sessions 统一) site = Site.objects.create(name="s", domains=["a.com"]) bot = Visitor.objects.create(site=site, visitor_id="bot", is_bot=True) human = Visitor.objects.create(site=site, visitor_id="human") bs = VisitSession.objects.create(site=site, visitor=bot, session_id="bs", started_at="2026-09-15T10:00:00Z", ended_at="2026-09-15T10:01:00Z") hs = VisitSession.objects.create(site=site, visitor=human, session_id="hs", started_at="2026-09-15T10:00:00Z", ended_at="2026-09-15T10:01:00Z") Event.objects.create(site=site, visitor=bot, session=bs, event_id="b1", event_name="page_view", occurred_at="2026-09-15T10:00:00Z", path="/", is_bot=True) Event.objects.create(site=site, visitor=human, session=hs, event_id="h1", event_name="page_view", occurred_at="2026-09-15T10:00:30Z", path="/") d = refresh_daily(site, date(2026, 9, 15)) assert (d.pv, d.uv, d.sessions) == (1, 1, 1) # 只计 human 的事件/会话 t = refresh_daily(site, timezone.localdate()) # 两位访客 first_seen_at 均为运行时,刷今天窗口才能非空验证 new_visitors assert t.new_visitors == 1 # 爬虫访客不计入新增 def test_refresh_daily_pv_only_counts_page_view(db): # PV 口径回归(阶段A T11):SDK 0.2.0 每 15s 一条 page_stay 增量 + 启动 sdk_loaded, # pv 只计 page_view——增量停留事件不得灌倍 PV(5 分钟活跃访问 ≈ 22 条事件只有 1 条是浏览)。 # 固定时间戳钉死日界不依赖运行时刻(同 boundary 用例做法) site = Site.objects.create(name="s", domains=["a.com"]) v = Visitor.objects.create(site=site, visitor_id="v1") sess = VisitSession.objects.create(site=site, visitor=v, session_id="s1", started_at="2026-09-15T10:00:00Z", ended_at="2026-09-15T10:05:00Z") names = ["page_view", "sdk_loaded", "page_view", "page_stay", "page_stay", "page_stay"] for i, name in enumerate(names): Event.objects.create(site=site, visitor=v, session=sess, event_id=f"x{i}", event_name=name, occurred_at=f"2026-09-15T10:00:0{i}Z", path="/") d = refresh_daily(site, date(2026, 9, 15)) assert (d.pv, d.uv, d.sessions) == (2, 1, 1) # 6 条事件仅 2 条 page_view 计 PV;uv 仍按任意事件 distinct