fix(analytics): id-tiebreak snapshot ordering + rates/cn_day tests + breakdown ValueError

This commit is contained in:
guke
2026-07-08 17:52:40 +08:00
parent afb5f478c9
commit 2289b2720c
2 changed files with 44 additions and 13 deletions
+9 -12
View File
@@ -9,7 +9,7 @@ from __future__ import annotations
from collections import defaultdict
from datetime import UTC, datetime
from sqlalchemy import and_, func, select
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.core import rewards
@@ -31,7 +31,7 @@ def diff_snapshots(rows: list[dict]) -> list[dict]:
out: list[dict] = []
for group in parts.values():
group.sort(key=lambda r: r["created_at"])
group.sort(key=lambda r: (r["created_at"], r.get("id", 0)))
prev = {k: 0 for k in _COUNTS}
for r in group:
deltas = {f"d_{k}": max(0, int(r[k]) - prev[k]) for k in _COUNTS}
@@ -74,7 +74,7 @@ def _sum_deltas(deltas: list[dict]) -> dict:
def _fetch_rows(db: Session, date_from: datetime, date_to: datetime) -> list[dict]:
"""取 [from, to) 区间行 + 每分区在 from 左侧的最后一条基线行(供第一条区间增量做差)。"""
cols = (
H.device_id, H.epoch_id, E.event, H.created_at,
H.id, H.device_id, H.epoch_id, E.event, H.created_at,
H.app_ver, H.oem, H.os,
E.attempted, E.drop_capture, E.delivered, E.drop_undelivered,
)
@@ -83,8 +83,10 @@ def _fetch_rows(db: Session, date_from: datetime, date_to: datetime) -> list[dic
.where(H.created_at >= date_from, H.created_at < date_to)
).mappings().all()
# 注:基线子查询无下界扫 from 左侧全量(spec §7 已接受的取舍;量级变大再上物化 rollup)。
# 用 max(id) 而非 max(created_at) 选"最新一条":id 严格单调,避免 SQLite 秒级时间戳撞车时选歧义。
sub = (
select(H.device_id, H.epoch_id, E.event, func.max(H.created_at).label("mx"))
select(H.device_id, H.epoch_id, E.event, func.max(H.id).label("max_id"))
.join(E, E.snapshot_id == H.id)
.where(H.created_at < date_from)
.group_by(H.device_id, H.epoch_id, E.event)
@@ -92,13 +94,7 @@ def _fetch_rows(db: Session, date_from: datetime, date_to: datetime) -> list[dic
)
baseline = db.execute(
select(*cols).join(E, E.snapshot_id == H.id).join(
sub,
and_(
sub.c.device_id == H.device_id,
sub.c.epoch_id == H.epoch_id,
sub.c.event == E.event,
sub.c.mx == H.created_at,
),
sub, sub.c.max_id == H.id
)
).mappings().all()
@@ -128,7 +124,8 @@ def trend(db: Session, date_from: datetime, date_to: datetime) -> list[dict]:
def breakdown(db: Session, date_from: datetime, date_to: datetime, dim: str) -> list[dict]:
assert dim in ("event", "app_ver", "oem")
if dim not in ("event", "app_ver", "oem"):
raise ValueError(f"invalid dim: {dim!r}")
deltas = _in_range_deltas(db, date_from, date_to)
by_key: dict[str, list[dict]] = defaultdict(list)
for d in deltas:
+35 -1
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import datetime, timezone
from app.admin.repositories.analytics_health import diff_snapshots
from app.admin.repositories.analytics_health import _cn_day, _rates, diff_snapshots
def _row(device, epoch, event, ts, **cum) -> dict:
@@ -52,3 +52,37 @@ def test_diff_clamps_negative_on_reorder() -> None:
]
out = sorted(diff_snapshots(rows), key=lambda r: r["created_at"])
assert out[1]["d_attempted"] == 0
def test_rates_computes_both_formulas() -> None:
out = _rates({"attempted": 150, "drop_capture": 0, "delivered": 140, "drop_undelivered": 10})
assert out["track_success_rate"] == 1.0
assert out["report_success_rate"] == 140 / 150
def test_rates_zero_denominator_yields_none() -> None:
out = _rates({"attempted": 0, "drop_capture": 0, "delivered": 0, "drop_undelivered": 0})
assert out["track_success_rate"] is None
assert out["report_success_rate"] is None
def test_cn_day_beijing_boundary() -> None:
# UTC 15:59 → 北京 23:59 同日;UTC 16:00 → 北京 次日 00:00
assert _cn_day(datetime(2026, 7, 1, 15, 59, tzinfo=timezone.utc)) == "2026-07-01"
assert _cn_day(datetime(2026, 7, 1, 16, 0, tzinfo=timezone.utc)) == "2026-07-02"
def test_diff_same_timestamp_ordered_by_id() -> None:
ts = datetime(2026, 7, 1, 1, 0, tzinfo=timezone.utc)
# 相同 created_at,乱序传入(id=2 在前);应按 id 排序 → id=1(cum100) 在前
rows = [
{"id": 2, "device_id": "d", "epoch_id": "e", "event": "vp", "created_at": ts,
"app_ver": "v", "oem": "o", "os": "s",
"attempted": 150, "drop_capture": 0, "delivered": 0, "drop_undelivered": 0},
{"id": 1, "device_id": "d", "epoch_id": "e", "event": "vp", "created_at": ts,
"app_ver": "v", "oem": "o", "os": "s",
"attempted": 100, "drop_capture": 0, "delivered": 0, "drop_undelivered": 0},
]
out = diff_snapshots(rows)
assert out[0]["d_attempted"] == 100 # id=1 sorts first → its cumulative
assert out[1]["d_attempted"] == 50 # id=2 second → 150-100