Files
meida_backend/packages/analytics/schemas.py
T
xsl 64175a1ea1 feat(analytics): POST /events/batch with fail-close validation
ob_event 落库通道:形状校验 + props 字节上限 + client_ts 合理性 +
PII 键剔除,整批原子(FR-SYS-06 服务端部分)。

实现:packages/analytics/schemas.py(EventIn/EventBatchRequest/
EventBatchResponse + 事件名形状正则常量);service.py 拆出纯函数
validate_batch(批量/props 字节/事件名形状/client_ts 窗口/PII 剔除,
任一不过整批 40020),AnalyticsService.record 组装 ObEvent 逐条
add + 一次 flush;routers/events.py 提供 POST /events/batch
(/api/v1 与 /v1 别名,认证必需)。

测试:单测 tests/unit/test_events_validation.py 8 例(合法批、超批量、
props 超字节/不可序列化、事件名形状含尾随换行、client_ts 双边界、
naive 时区按 UTC、PII 键含大小写变体剔除);集成
tests/integration/test_events.py 3 例(两前缀可用+落库断言、
非法批 40020 且库无半条、无 token 40100)。门禁全绿:ruff
check/format、mypy(T2 列表 + domain strict)、lint-imports 4 契约、
单测 177 通过、集成 6 通过。

偏差说明:event_name 正则未写成 Field(pattern=...),而是作为
EVENT_NAME_PATTERN 常量放 schemas.py、由服务层 validate_batch 强制——
若挂在请求体校验上,非法事件名会先被 RequestValidationError 拦截、
返回通用 40001/422 信封,无法满足任务契约 §4「大写 event_name →
40020」的集成行为。正则本体与文档一致。
2026-08-14 16:16:26 +08:00

44 lines
1.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""埋点上报的请求/响应模型(架构文档 §9.3 events,FR-SYS-06 服务端部分)。
统一信封(§9.2)之外,这里只描述 ``data`` 载荷。事件字典(event dictionary)
V1 不做服务端枚举校验 —— 客户端事件名集合由 TD-23 埋点字典维护、尚未定稿,
先只用形状正则兜底,字典定稿后再收紧。
"""
from __future__ import annotations
import re
from datetime import datetime
from typing import Any
from pydantic import BaseModel, Field
#: 事件名形状:小写字母开头,仅 [a-z0-9_.],总长 2~64(架构 §6.2 event_name)。
#:
#: 注意它刻意不写成 ``Field(pattern=...)`` —— 请求体校验失败会落进通用的
#: 40001/422 信封,而埋点链路要求 fail-close 地返回域错误 40020(客户端据此
#: 丢弃本地缓存、静默降级)。因此正则在 service 层 ``validate_batch`` 里强制。
EVENT_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_.]{1,63}$")
class EventIn(BaseModel):
"""单条埋点事件(客户端时钟不可信,client_ts 仅作参考)。"""
event_name: str = Field(description="事件名;形状见 EVENT_NAME_PATTERN,服务层 fail-close 校验")
props: dict[str, Any] = Field(
default_factory=dict, description="事件属性;PII 键会被服务端剔除"
)
client_ts: datetime | None = Field(
default=None, description="客户端事件时间(ISO 8601);缺省时仅用服务端时间"
)
class EventBatchRequest(BaseModel):
"""整批上报:任一条非法则整批拒绝,不部分落库。"""
events: list[EventIn] = Field(min_length=1, description="本批事件,上限见 EVENTS_MAX_BATCH")
class EventBatchResponse(BaseModel):
accepted: int = Field(description="本批成功落库的事件条数;失败走 40020,不会返回部分值")