feat: 来源归因规则引擎(渠道/AI/搜索词/诚实状态/历史重算)

- constants 规则库 v1:AI/引擎/社交域名表、搜索词参数白名单、付费点击参数、
  应用内 UA 标记、CHANNELS 枚举;page_view 白名单开 nav_type/prerendering
- services/attribution.classify 纯函数:付费>AI>引擎>社交>referral 分层,
  UTM 证据叠加(combined/conflict/label_only),应用内 UA 叠加/兜底,
  引擎 TLD 国家 variant,搜索词白名单提取(Google 恒空不虚构)
- SourceClassification 派生表(迁移 0004):first_touch/session_entry 双 scope,
  rule_version 可追溯
- ingest 增量 best-effort:首访客/新会话时分类落库(savepoint+静默,
  失败不影响采集);validation 对 page_view referrer 完整 URL 参数清洗+截500
- recompute_attribution 命令:全量/按站幂等重算
- API:访客列表归因五字段+channel 筛选、详情 attribution 块(含最近5会话入口)、
  dashboard channels 聚合(sources 原口径保留);TS VisitorListParams 加 channel
- 测试 97 → 124(引擎20 + 集成/接口7)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-16 17:03:15 +08:00
parent 8bde9b5b4b
commit 59de51c062
15 changed files with 816 additions and 15 deletions
+33 -1
View File
@@ -11,9 +11,41 @@ URL_PARAM_DROP_PATTERNS = ("email", "phone", "mobile", "token", "key", "password
SESSION_IDLE_MS = 30 * 60 * 1000
BOT_UA_KEYWORDS = ("bot", "spider", "crawl", "slurp", "bingpreview", "headless", "lighthouse")
EVENT_PROP_WHITELIST = { # spec §9.1:未知字段拒绝。免费文本/任意属性不得入库
"page_view": {"referrer", "referrer_domain", "utm"},
"page_view": {"referrer", "referrer_domain", "utm", "nav_type", "prerendering"},
"page_stay": {"active_ms", "path"},
"sdk_loaded": {"sdk_version"},
}
MAX_PROP_DEPTH = 2
MAX_PROP_JSON_BYTES = 4096
# ---------------------------------------------------------------------------
# 来源归因规则库 v1(tracker/services/attribution.py 消费)。改规则必须 bump 版本号
# 并跑 recompute_attribution 重算历史——已落库分类带 rule_version 可追溯。
# ---------------------------------------------------------------------------
ATTRIBUTION_RULE_VERSION = "v1"
AI_REFERRAL_DOMAINS = { # AI 平台 → 显示名
"chatgpt.com": "ChatGPT", "perplexity.ai": "Perplexity",
"copilot.microsoft.com": "Copilot", "gemini.google.com": "Gemini",
"deepseek.com": "DeepSeek", "claude.ai": "Claude", "poe.com": "Poe", "you.com": "You",
}
SEARCH_ENGINE_DOMAINS = { # 引擎后缀/域名 → 显示名("google."/"yandex." 前缀匹配任意国家站 TLD)
"google.": "Google", "bing.com": "Bing", "baidu.com": "百度", "yandex.": "Yandex",
"duckduckgo.com": "DuckDuckGo", "yahoo.com": "Yahoo", "ecosia.org": "Ecosia",
"search.naver.com": "Naver", "search.brave.com": "Brave",
}
SEARCH_TERM_PARAMS = { # 引擎 → referrer URL 里的搜索词参数名(白名单提取)
"Bing": ["q"], "百度": ["wd", "word"], "DuckDuckGo": ["q"], "Yandex": ["text"],
"Ecosia": ["q"], "Yahoo": ["p"], "Brave": ["q"], "Naver": ["query"],
} # 注:Google 不提供,绝不虚构
PAID_CLICK_PARAMS = {"gclid": "Google Ads", "fbclid": "Meta Ads",
"msclkid": "Bing Ads", "yclid": "Yandex Ads"}
SOCIAL_DOMAINS = {
"t.co": "Twitter/X", "twitter.com": "Twitter/X", "x.com": "Twitter/X",
"facebook.com": "Facebook", "linkedin.com": "LinkedIn", "instagram.com": "Instagram",
"vk.com": "VK", "reddit.com": "Reddit", "pinterest.com": "Pinterest",
"telegram.me": "Telegram", "t.me": "Telegram",
}
INAPP_UA_MARKERS = {"MicroMessenger": "微信", "Line/": "Line", "FBAN": "Facebook",
"FBAV": "Facebook", "Instagram": "Instagram", "WhatsApp": "WhatsApp"}
CHANNELS = ("natural_search", "paid_search", "ai_referral", "social",
"in_app", "referral", "direct", "unknown")