From c38610380934b1d341e8103277329b9a7ce618c6 Mon Sep 17 00:00:00 2001 From: guke Date: Wed, 8 Jul 2026 16:53:18 +0800 Subject: [PATCH] feat(analytics): POST /api/v1/analytics/selfstat ingest endpoint Co-Authored-By: Claude Sonnet 4.6 --- app/api/v1/analytics.py | 17 +++++++++++++++- tests/test_analytics_selfstat.py | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/test_analytics_selfstat.py diff --git a/app/api/v1/analytics.py b/app/api/v1/analytics.py index 265954d..23d2f82 100644 --- a/app/api/v1/analytics.py +++ b/app/api/v1/analytics.py @@ -6,13 +6,18 @@ POST /api/v1/analytics/events — 批量接收新手引导(及后续)埋点,appe """ from __future__ import annotations -from fastapi import APIRouter, Request +import logging + +from fastapi import APIRouter, HTTPException, Request from app.api.deps import DbSession from app.repositories import analytics as analytics_repo +from app.repositories import analytics_selfstat as selfstat_repo from app.schemas.analytics import AnalyticsBatchIn, AnalyticsIngestOut +from app.schemas.analytics_selfstat import SelfStatBatchIn, SelfStatIngestOut router = APIRouter(prefix="/api/v1/analytics", tags=["analytics"]) +logger = logging.getLogger("shagua.analytics") def _client_ip(request: Request) -> str: @@ -29,3 +34,13 @@ def ingest_events( ) -> AnalyticsIngestOut: n = analytics_repo.record_batch(db, batch, client_ip=_client_ip(request)) return AnalyticsIngestOut(received=n) + + +@router.post("/selfstat", response_model=SelfStatIngestOut, summary="上报自报计数快照") +def ingest_selfstat(batch: SelfStatBatchIn, db: DbSession) -> SelfStatIngestOut: + try: + snap_id = selfstat_repo.record_selfstat(db, batch) + except Exception: # noqa: BLE001 — 计数链路要稳,落库失败不裸奔 500,记日志回明确错误 + logger.exception("selfstat ingest failed device=%s epoch=%s", batch.device_id, batch.epoch_id) + raise HTTPException(status_code=503, detail="selfstat ingest failed") from None + return SelfStatIngestOut(snapshot_id=snap_id) diff --git a/tests/test_analytics_selfstat.py b/tests/test_analytics_selfstat.py new file mode 100644 index 0000000..4319f60 --- /dev/null +++ b/tests/test_analytics_selfstat.py @@ -0,0 +1,33 @@ +"""自报计数上报端点测试。""" +from __future__ import annotations + +from fastapi.testclient import TestClient + + +def _payload(**over) -> dict: + base = { + "device_id": "dev-1", "epoch_id": "ep-1", "sent_at": 1700000000000, + "app_ver": "0.2.12(62)", "oem": "ColorOS", "os": "Android 14", + "batches_attempted": 10, "batches_ok": 9, "batches_fail": 1, + "retries": 1, "queue_depth": 2, + "events": [ + {"event": "video_play", "attempted": 100, "drop_capture": 1, + "delivered": 95, "drop_undelivered": 2}, + ], + } + base.update(over) + return base + + +def test_selfstat_ingest_ok(client: TestClient) -> None: + r = client.post("/api/v1/analytics/selfstat", json=_payload()) + assert r.status_code == 200, r.text + body = r.json() + assert body["ok"] is True + assert isinstance(body["snapshot_id"], int) + + +def test_selfstat_ingest_empty_events(client: TestClient) -> None: + r = client.post("/api/v1/analytics/selfstat", json=_payload(events=[])) + assert r.status_code == 200, r.text + assert r.json()["ok"] is True