Attribution
Status: 🟢 Estável (Sprint 1.A deployada) Code: backend/app/modules/attribution UI: frontend/src/app/(admin)/[slug]/admin/marketing (campaigns dentro do hub) Última revisão deste doc: 2026-05-13 por Felipe + Claude Dependências fortes: crm (popula
crm_contact.attribution_*), marketing (plugins criam campaigns), tracking (eventos)
1. Identidade
O que faz (uma frase)
Persiste campanhas de marketing (paid + organic) e touchpoints cronológicos por contato com canal/source/UTM resolvido — substitui UTM solto na URL com modelo persistido que sobrevive ao primeiro click e permite multi-touch attribution futura.
Por que existe (negócio)
Sem attribution dedicada:
- UTM da URL fica em
crm_contact.notesou perdido. - Sem histórico de touchpoints (lead viu Instagram → email → ad antes de comprar).
- Cross-channel ROI impossível.
Sprint 1.A (memória [[brain-observatory-sprint-1a]]) trouxe:
- Campaign persistida (slug, kind, channel, budget, status).
- Touchpoint por interação (channel, source_module, position=first/middle/last/conversion, weight).
crm_contact.attribution_*populado on-create (resolve UTM → campaign).- Bridge organic (Instagram, YouTube) — plugin Hub.5/6 criam campaign quando detecta novo channel/video.
Status atual
- Sprint 1.A em prod desde 2026-05-13 (commits
09aada4+a3729da, alembic 0094).
Próxima mudança: Multi-touch models (linear, time-decay) em fase L3 do intelligence; Campaign attributed_revenue auto-sync com billing.
2. Cases de uso reais
Case 1: Lead novo via Meta Ads
Fluxo: Click no ad → URL com ?utm_source=meta&utm_medium=cpc&utm_campaign=mentoria-premium-q2 → submete form → CRM cria contato → attribution.service.attach_attribution(contact, utm) → resolve utm_campaign → match com attribution_campaign(slug=mentoria-premium-q2) (criado pelo plugin meta_ads via attribution_bridge) → crm_contact.attribution_campaign_id populado + attribution_first_touch_at=now.
Case 2: Lead via YouTube orgânico
Fluxo: Click em link na descrição de vídeo → URL sem UTM → attach_attribution falha matching → crm_contact.attribution_first_source='direct'. Plugin Hub.6 já criou attribution_campaign(kind=organic_youtube, external_id=channel_id) mas resolve UTM não match. Felipe pode editar manualmente depois.
Case 3: Multi-touch (futuro)
Beat task percorre contact_touchpoint ordenado por occurred_at, atribui weight (linear: 1/N; time-decay: exponencial decreasing) → atualiza attribution_first_source/attribution_last_source + Conselho lê via read_attribution(contact_id).
3. Oportunidades de negócio
- Multi-touch attribution as a service: modelar diferentes attribution models (linear, time-decay, U-shaped, position-based) — premium feature.
- Campaign ROI dashboard: cruzar
attributed_revenue_brlcom spend de Meta Ads/Google Ads → ROAS por campanha. - Cross-channel funnel: "lead viu IG → email → ad antes de comprar" — visualização única.
Riscos: UTM matching é best-effort; sem match = attribution_first_source='direct' (perde info).
4. Arquitetura interna
Arquivos
| Arquivo | Propósito |
|---|---|
models.py | 2 tabelas |
routes.py | 5 endpoints |
service.py | resolve_utm_to_campaign, attach_attribution, append_touchpoint |
schemas.py | Pydantic |
Tasks
Sem Celery. attach_attribution é síncrono on-create contact (chamado por CRM webhook).
5. Tabelas (2)
Naming exception: o nome da tabela é
campaign(sem prefixoattribution_), exceção histórica à convenção<modulo>_<tabela>do CLAUDE.md. Mantida assim porquecrm_contact.attribution_campaign_idreferencia ela e renomear exigiria migração extensa. Quando este doc menciona "attribution_campaign", o nome real no schema écampaign.
campaign
| Coluna chave | Notas |
|---|---|
tenant_id | |
slug | UNIQUE per tenant |
name | |
kind | ad_paid / organic_youtube / organic_instagram / newsletter / affiliate / referral / event / webinar / partnership / direct / other |
channel | |
started_at / ended_at | |
budget_brl | |
target_audience_md | |
utm_tracking | JSONB {source, medium, campaign, content?, term?} |
provider | NULL=manual ou ga4 / youtube / meta_ads / etc. |
external_id | ID do provider (ex: campaign_id Meta) |
metadata_ | JSONB livre |
attributed_revenue_brl | DECIMAL — calculado periodicamente |
attributed_contacts_count | INT |
status | active / paused / concluded / archived |
notes_md |
contact_touchpoint
| Coluna chave | Notas |
|---|---|
tenant_id | |
contact_id | FK lógica → crm_contact |
campaign_id | Nullable |
channel | |
source_module | crm / email / links / instagram / whatsapp / etc. |
event_id | FK lógica → tracking_event |
occurred_at | |
position | first / middle / last / conversion |
weight | DECIMAL(5,4) — 1.0 first touch (default) |
properties | JSONB {utm_*} |
Relacionamentos cross-módulo
| Direção | Outro módulo | Como |
|---|---|---|
| ↘ Escreve | crm_contact | attribution_campaign_id, attribution_first_touch_at, attribution_last_touch_at, attribution_first_source, attribution_first_medium, attribution_first_campaign |
| ↗ Lê | marketing (plugins) | Plugins criam campaigns via attribution_bridge.sync_attribution_campaigns |
| ↗ Lê | tracking | event_id em touchpoint |
6. API / Endpoints (5)
| Método | Rota | O que faz |
|---|---|---|
| GET | /api/attribution/campaigns | Lista (filter status), paginação |
| POST | /api/attribution/campaigns | Criar manual |
| PATCH | /api/attribution/campaigns/{id} | Editar (name, budget, status) |
| DELETE | /api/attribution/campaigns/{id} | Arquivar (não deleta touchpoints) |
| GET | /api/attribution/contacts/{contact_id}/touchpoints | Histórico cronológico |
7. Service principais
resolve_utm_to_campaign(utm)— heurística por score: utm_campaign=100, source=10, medium=5. Pick highest.attach_attribution(contact, utm)— on-create CRM: resolve UTM → campaign, cria first touchpoint, populacrm_contact.attribution_*. Idempotente.append_touchpoint(contact_id, channel, source_module, ...)— registra subsequent touches sem mexer first_*.
8. Configuração
Sem env vars específicas. UTM resolution on-create (não retroativo).
9. Operações + Troubleshooting
Sintoma: Lead com attribution_first_source='direct' mesmo vindo com UTM
Causa: UTM não bateu com nenhuma attribution_campaign (matching score 0).
Fix: Felipe edita manualmente crm_contact.attribution_* ou cria campaign nova com utm_tracking matching.
10. Limitações e débitos técnicos
| # | Item |
|---|---|
| 1 | UTM matching best-effort — sem match = source=direct |
| 2 | Sem multi-touch model implementado — só first/last touch |
| 3 | attributed_revenue_brl calculado fora |
| 4 | Sem FK formal — contact_id UUID solto |
| 5 | Deletion não retroativa |
11. Histórico
- 2026-05-13 (commits
09aada4+a3729da, alembic 0094) — Sprint 1.A deployada. Memória [[brain-observatory-sprint-1a]].