"""客户端埋点上报接口。 POST /api/v1/analytics/events — 批量接收新手引导(及后续)埋点,append 落 analytics_event 表。 **不强制登录**(未登录态也要采集行为):user_id 由客户端在 body 里可选带上,不靠 Bearer。 服务端补 client_ip(X-Forwarded-For)与 created_at(接收时间 = server_at)。 """ from __future__ import annotations 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: """取客户端 IP:生产经 nginx 反代优先 X-Forwarded-For 第一段,否则直连 IP(同 admin get_client_ip)。""" xff = request.headers.get("x-forwarded-for") if xff: return xff.split(",")[0].strip() return request.client.host if request.client else "" @router.post("/events", response_model=AnalyticsIngestOut, summary="批量上报埋点事件") def ingest_events( batch: AnalyticsBatchIn, request: Request, db: DbSession ) -> 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)