feat(admin): 时间线业务事件徽章+询盘漏斗卡+付费社交渠道标签(含server时间线安全字段)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-19 00:12:13 +08:00
parent 8d7fc99ef6
commit c9967b6d70
7 changed files with 324 additions and 15 deletions
+33 -2
View File
@@ -23,6 +23,34 @@ TIMELINE_MAX_EVENTS = 1000
ONLINE_WINDOW = timedelta(minutes=5) # 与 dashboard active_now 同口径
ATTR_SESSION_ENTRIES = 5 # 详情页归因块带出的最近会话入口分类数
# 业务事件时间线安全字段白名单(阶段B SDK 0.2.1 事件):按事件名枚举可透出的非 PII 字段
# (OT10 白名单内),不在表内的事件/字段一律不透出 properties 原文——隐私边界与
# _merge_page_stay 的既有白名单口径一致。submission_id 只透出前 8 位(对账够用)。
TIMELINE_SAFE_FIELDS = {
"contact_click": ("channel",),
"form_start": ("form_id",),
"form_submit": ("form_id", "submission_id"),
"form_error": ("error_code", "field_count"),
"form_submit_result": ("result_code",),
"product_view": ("product_id", "inferred"),
}
def _safe_fields(e):
"""事件行安全字段提取:白名单命中才输出,且只出白名单键(缺失键不占位);
form_submit 的 submission_id 截前 8 位。"""
keys = TIMELINE_SAFE_FIELDS.get(e.event_name)
if not keys:
return {}
props = e.properties or {}
out = {}
for k in keys:
v = props.get(k)
if v is None:
continue
out[k] = str(v)[:8] if k == "submission_id" else v
return out
def _source(referrer_domain, utm):
"""来源三级兜底:referrer_domain → utm.utm_source → 直接访问(访客档案/会话共用)。"""
@@ -182,7 +210,8 @@ def _merge_page_stay(events):
最近一条同 path 的 page_view 条目,把 active_ms 累加进去(同页多段隐藏/恢复多次累加);
找不到匹配(含 page_stay 早于任何 page_view 出现)则独立成条目(event_name 保持 page_stay)。
作用域天然限定在本组事件序列(单会话/时间线单会话桶)——跨会话绝不合并。
只出 time/event_name/path/title/active_ms 白名单字段,不透出 properties 原文(隐私)。
只出 time/event_name/path/title/active_ms(+业务事件 _safe_fields 白名单字段),
不透出 properties 原文(隐私)。
"""
items = []
for e in events:
@@ -198,8 +227,10 @@ def _merge_page_stay(events):
else:
# page_view 初始 active_ms=0;其余(sdk_loaded/自定义事件)无停留概念 → null
active = 0 if e.event_name == "page_view" else None
# 业务事件附带 _safe_fields 白名单字段(其余 properties 键不透出)
items.append({"time": _iso(e.occurred_at), "event_name": e.event_name,
"path": e.path, "title": e.title, "active_ms": active})
"path": e.path, "title": e.title, "active_ms": active,
**_safe_fields(e)})
return items