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:
@@ -4,7 +4,7 @@ from django.utils import timezone
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from tracker.api.envelope import EnvelopeMixin, ok
|
||||
from tracker.models import DailyStat, Event, Site, VisitSession, Visitor
|
||||
from tracker.models import DailyStat, Event, Site, SourceClassification, VisitSession, Visitor
|
||||
from tracker.services.stats import daily_counts
|
||||
|
||||
METRICS = ("pv", "uv", "sessions", "new_visitors")
|
||||
@@ -31,6 +31,7 @@ class OverviewView(EnvelopeMixin, APIView):
|
||||
return ok({"today": counts,
|
||||
"trend": self._trend(site, today, counts),
|
||||
"sources": self._today_sources(site, today),
|
||||
"channels": self._today_channels(site, today),
|
||||
"top_pages": self._today_pages(site, today)})
|
||||
|
||||
@staticmethod
|
||||
@@ -75,6 +76,22 @@ class OverviewView(EnvelopeMixin, APIView):
|
||||
.annotate(n=Count("id"))
|
||||
.order_by("-n", "referrer_domain")[:8]] # 平局时次序跨请求稳定
|
||||
|
||||
def _today_channels(self, site, today):
|
||||
"""当天会话按来源渠道聚合(session_entry 分类;sources 的升级版,原始口径保留)。
|
||||
从分类表正向聚合 + Count(distinct session):无唯一约束下分类表偶发同键重复行
|
||||
也不会把同一会话计两次。无分类行的会话(未重算的历史数据)不计入。"""
|
||||
start, end = _day_bounds(today)
|
||||
q = SourceClassification.objects.filter(
|
||||
scope="session_entry",
|
||||
session__visitor__is_bot=False,
|
||||
session__started_at__gte=start, session__started_at__lt=end)
|
||||
if site is not None:
|
||||
q = q.filter(site=site)
|
||||
return [{"channel": r["channel"] or "unknown", "sessions": r["n"]}
|
||||
for r in q.values("channel")
|
||||
.annotate(n=Count("session", distinct=True))
|
||||
.order_by("-n", "channel")[:8]] # 平局时次序跨请求稳定
|
||||
|
||||
def _today_pages(self, site, today):
|
||||
start, end = _day_bounds(today)
|
||||
q = Event.objects.filter(is_bot=False, event_name="page_view",
|
||||
|
||||
Reference in New Issue
Block a user