fix(server): PV统计加event_name过滤——增量停留事件不再灌倍PV(gzip 4817B记录:0.1.0为3838,增量989)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-18 15:16:05 +08:00
parent f24060de87
commit 87261abf16
4 changed files with 44 additions and 4 deletions
+4 -2
View File
@@ -12,7 +12,7 @@
""" """
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from django.db.models import Count, Sum from django.db.models import Count, Q, Sum
from django.db.models.functions import Trunc from django.db.models.functions import Trunc
from django.utils import timezone from django.utils import timezone
from rest_framework.response import Response from rest_framework.response import Response
@@ -128,9 +128,11 @@ class OverviewView(EnvelopeMixin, APIView):
return rows.setdefault( return rows.setdefault(
(sid, day), {"pv": 0, "uv": 0, "sessions": 0, "new_visitors": 0}) (sid, day), {"pv": 0, "uv": 0, "sessions": 0, "new_visitors": 0})
# pv 只计 page_view(与 daily_counts 同口径):page_stay 增量/sdk_loaded 不并计
for r in (ev.annotate(day=Trunc("occurred_at", "day", tzinfo=tz)) for r in (ev.annotate(day=Trunc("occurred_at", "day", tzinfo=tz))
.values("site", "day") .values("site", "day")
.annotate(pv=Count("id"), uv=Count("visitor", distinct=True))): .annotate(pv=Count("id", filter=Q(event_name="page_view")),
uv=Count("visitor", distinct=True))):
row = _row(r["site"], r["day"].date()) row = _row(r["site"], r["day"].date())
row["pv"], row["uv"] = r["pv"], r["uv"] row["pv"], row["uv"] = r["pv"], r["uv"]
for r in (vs.annotate(day=Trunc("started_at", "day", tzinfo=tz)) for r in (vs.annotate(day=Trunc("started_at", "day", tzinfo=tz))
+4 -2
View File
@@ -7,7 +7,9 @@ def daily_counts(site, date) -> dict:
"""四指标共享口径(site=None 聚合全部站点)。所有指标排除爬虫。 """四指标共享口径(site=None 聚合全部站点)。所有指标排除爬虫。
日界取 TIME_ZONE(Asia/Shanghai)自然日:make_aware(datetime.combine(date, time.min))。 日界取 TIME_ZONE(Asia/Shanghai)自然日:make_aware(datetime.combine(date, time.min))。
口径:pv/uv 均从事件表统计(排除 is_bot),uv=distinct visitor,与 pv 同源; 口径:pv/uv 均从事件表统计(排除 is_bot):pv 只计 event_name=page_view
(SDK 0.2.0 每 15s 一条的 page_stay 增量与 sdk_loaded 不并计,防 PV 随停留时长灌倍),
uv=distinct visitor(任意事件即计);
sessions 按会话 started_at 落入当日且访客非爬虫;new_visitors 按 Visitor.first_seen_at 落入当日。 sessions 按会话 started_at 落入当日且访客非爬虫;new_visitors 按 Visitor.first_seen_at 落入当日。
refresh_daily 落库与 dashboard 今日实时位共用此函数,防两处口径漂移(Task8 审查结论)。 refresh_daily 落库与 dashboard 今日实时位共用此函数,防两处口径漂移(Task8 审查结论)。
""" """
@@ -23,7 +25,7 @@ def daily_counts(site, date) -> dict:
ev = ev.filter(site=site) ev = ev.filter(site=site)
vs = vs.filter(site=site) vs = vs.filter(site=site)
vis = vis.filter(site=site) vis = vis.filter(site=site)
return {"pv": ev.count(), return {"pv": ev.filter(event_name="page_view").count(),
"uv": ev.values("visitor").distinct().count(), "uv": ev.values("visitor").distinct().count(),
"sessions": vs.count(), "sessions": vs.count(),
"new_visitors": vis.count()} "new_visitors": vis.count()}
+21
View File
@@ -290,3 +290,24 @@ def test_overview_avg_stay_mixed_v1_v2_session(client, member_user):
d = client.get("/api/admin/dashboard/overview", {"range": "today"}).json()["data"] d = client.get("/api/admin/dashboard/overview", {"range": "today"}).json()["data"]
assert d["cards"]["visits"]["value"] == 1 assert d["cards"]["visits"]["value"] == 1
assert d["cards"]["avg_stay_ms"] == {"value": 38000, "delta_pct": None} # 38000/1;上期无会话 → None assert d["cards"]["avg_stay_ms"] == {"value": 38000, "delta_pct": None} # 38000/1;上期无会话 → None
def test_overview_trend_pv_only_page_view(client, member_user):
"""PV 口径回归(阶段A T11):SDK 0.2.0 每 15s 一条 page_stay 增量 + sdk_loaded,
趋势自愈回写 DailyStat.pv 只计 page_view(与 daily_counts 同口径)——
增量停留事件不得灌倍 PV;uv 仍按任意事件 distinct visitor 计。"""
_auth(client, member_user)
site = Site.objects.create(name="s", domains=["a.com"])
v1 = _visitor(site, "v1", last_seen=timezone.now())
s1 = _session(site, v1, "s1")
# 模拟一次活跃访问的事件流:1 次浏览 + SDK 装载 + 3 条 15s 停留增量
names = ["page_view", "sdk_loaded", "page_stay", "page_stay", "page_stay"]
for i, name in enumerate(names):
Event.objects.create(site=site, visitor=v1, session=s1,
event_id=f"pvx-{i}", event_name=name,
occurred_at=NOW + timedelta(seconds=i), path="/")
d = client.get("/api/admin/dashboard/overview").json()["data"]
assert d["trend"][-1] == {"date": TODAY.isoformat(), "uv": 1, "sessions": 1}
row = DailyStat.objects.get(site=site, date=TODAY)
assert (row.pv, row.uv) == (1, 1) # 5 条事件仅 1 条 page_view 计 PV
+15
View File
@@ -58,3 +58,18 @@ def test_refresh_daily_excludes_bots(db):
assert (d.pv, d.uv, d.sessions) == (1, 1, 1) # 只计 human 的事件/会话 assert (d.pv, d.uv, d.sessions) == (1, 1, 1) # 只计 human 的事件/会话
t = refresh_daily(site, timezone.localdate()) # 两位访客 first_seen_at 均为运行时,刷今天窗口才能非空验证 new_visitors t = refresh_daily(site, timezone.localdate()) # 两位访客 first_seen_at 均为运行时,刷今天窗口才能非空验证 new_visitors
assert t.new_visitors == 1 # 爬虫访客不计入新增 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