fix: 来源冲突比较升级为广告家族归一(meta/google/bing/yandex 家族)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-17 11:23:02 +08:00
parent aba9e8eb26
commit 132e2feb75
2 changed files with 46 additions and 4 deletions
+19 -4
View File
@@ -19,8 +19,18 @@ from tracker import constants as C
_CTRL_CHARS = re.compile(r"[\x00-\x1f\x7f]") _CTRL_CHARS = re.compile(r"[\x00-\x1f\x7f]")
_GENERIC_TLDS = ("com",) # 引擎 host 落在这些 TLD 时不作为国家 variant 显示 _GENERIC_TLDS = ("com",) # 引擎 host 落在这些 TLD 时不作为国家 variant 显示
# UTM 标签 ↔ 平台名别名(冲突判定前归一):缩写/中文品牌名 → 标准名;baidu 恒等仅为可读 # 广告平台家族归一表(冲突判定前):同一家族的各种写法(缩写/全称/带 ads 后缀/
_UTM_ALIASES = {"fb": "facebook", "ig": "instagram", "tt": "tiktok", # 中文俄文别称)→ 家族名。fbclid 推出的 "Meta Ads" 与 utm_source=fb/ig 同属 meta
# 家族 → 印证而非矛盾。键为去空格后的紧凑形态("meta ads" → "metaads")。
_AD_FAMILIES = {
"fb": "meta", "facebook": "meta", "fbads": "meta",
"ig": "meta", "instagram": "meta", "meta": "meta", "metaads": "meta",
"google": "google", "googleads": "google", "adwords": "google",
"bing": "bing", "bingads": "bing", "microsoftads": "bing",
"yandex": "yandex", "yandexads": "yandex", "директ": "yandex",
}
# UTM 标签 ↔ 平台名别名(家族表之外的零散别名);baidu 恒等仅为可读
_UTM_ALIASES = {"tt": "tiktok",
"wa": "whatsapp", "g": "google", "baidu": "baidu", "百度": "baidu"} "wa": "whatsapp", "g": "google", "baidu": "baidu", "百度": "baidu"}
@@ -94,8 +104,10 @@ def _in_app_name(ua: str) -> str:
def _canon(name: str) -> str: def _canon(name: str) -> str:
"""平台名/UTM 标签宽松归一(仅用于冲突判定):小写 → 去协议与路径 → 去 www. 前缀 """平台名/UTM 标签宽松归一(仅用于冲突判定):小写 → 去协议与路径 → 去 www. 前缀
→ 去最后一段域名后缀(chatgpt.com→chatgpt、google.ru→google)→ 别名表 → 仅留 → 去最后一段域名后缀(chatgpt.com→chatgpt、google.ru→google)→ 去空格("meta
字母数字(含汉字)。fb↔Facebook、百度↔baidu 归一后同源,不再误报 conflict。""" ads"→"metaads")→ 家族表(fb/ig↔Meta Ads→meta;adwords→google;директ→yandex)
→ 别名表 → 仅留字母数字(含汉字)。同广告家族(meta/google/bing/yandex)归一后
同源,不再误报 conflict。"""
s = (name or "").lower().strip() s = (name or "").lower().strip()
if "://" in s: if "://" in s:
s = s.split("://", 1)[1] s = s.split("://", 1)[1]
@@ -104,6 +116,9 @@ def _canon(name: str) -> str:
s = s[4:] s = s[4:]
if "." in s: if "." in s:
s = s.rsplit(".", 1)[0] s = s.rsplit(".", 1)[0]
s = re.sub(r"\s+", "", s)
if s in _AD_FAMILIES: # 家族命中即返回家族名(已是规范 token)
return _AD_FAMILIES[s]
s = _UTM_ALIASES.get(s, s) s = _UTM_ALIASES.get(s, s)
return re.sub(r"[^0-9a-z一-鿿]", "", s) return re.sub(r"[^0-9a-z一-鿿]", "", s)
+27
View File
@@ -145,6 +145,33 @@ def test_utm_chinese_alias_baidu_matches():
assert r["detail"].get("utm_source") == "百度" assert r["detail"].get("utm_source") == "百度"
# ------------------------------------------- 广告家族归一(付费点击参数 ↔ UTM 同家族)
def test_fbclid_with_utm_fb_same_meta_family_combined():
r = classify(landing_path="/?fbclid=IwAR0", utm={"utm_source": "fb"})
assert r["channel"] == "paid_search" and r["platform"] == "Meta Ads"
assert r["resolution"] == "resolved" and r["evidence_kind"] == "combined"
assert r["detail"].get("utm_source") == "fb" # fb ↔ Meta Ads 同 meta 家族
def test_fbclid_with_utm_ig_combined():
r = classify(landing_path="/?fbclid=IwAR0", utm={"utm_source": "ig"})
assert r["platform"] == "Meta Ads" # ig 也在 meta 家族
assert r["resolution"] != "conflict" and r["evidence_kind"] == "combined"
def test_gclid_with_utm_google_combined():
r = classify(landing_path="/offer?gclid=EAIaIQ", utm={"utm_source": "google"})
assert r["channel"] == "paid_search" and r["platform"] == "Google Ads"
assert r["resolution"] != "conflict" and r["evidence_kind"] == "combined"
def test_fbclid_with_utm_tiktok_still_conflict():
r = classify(landing_path="/?fbclid=IwAR0", utm={"utm_source": "tiktok"})
assert r["resolution"] == "conflict" # 跨家族(meta vs tiktok)= 真矛盾
assert r["detail"]["referrer_evidence"]["platform"] == "Meta Ads"
assert r["detail"]["utm_evidence"]["platform"] == "tiktok"
def test_utm_only_is_label_only(): def test_utm_only_is_label_only():
r = classify(utm={"utm_source": "newsletter"}) r = classify(utm={"utm_source": "newsletter"})
assert r["channel"] == "referral" # 未知标签 → 有名 referral,但状态诚实标注 assert r["channel"] == "referral" # 未知标签 → 有名 referral,但状态诚实标注