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」的集成行为。正则本体与文档一致。
44 lines
1.8 KiB
Python
44 lines
1.8 KiB
Python
"""埋点上报的请求/响应模型(架构文档 §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,不会返回部分值")
|