cdp-edge 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +367 -0
- package/bin/cdp-edge.js +61 -0
- package/contracts/api-versions.json +368 -0
- package/dist/commands/analyze.js +52 -0
- package/dist/commands/infra.js +54 -0
- package/dist/commands/install.js +168 -0
- package/dist/commands/server.js +174 -0
- package/dist/commands/setup.js +123 -0
- package/dist/commands/validate.js +84 -0
- package/dist/index.js +12 -0
- package/docs/CI-CD-SETUP.md +217 -0
- package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
- package/docs/events-reference.md +359 -0
- package/docs/installation.md +155 -0
- package/docs/quick-start.md +185 -0
- package/docs/sdk-reference.md +371 -0
- package/docs/whatsapp-ctwa.md +209 -0
- package/extracted-skill/tracking-events-generator/INDEX.md +94 -0
- package/extracted-skill/tracking-events-generator/INSTALACAO-CDPEDGE.md +58 -0
- package/extracted-skill/tracking-events-generator/INTEGRACAO-COMPLETA.md +594 -0
- package/extracted-skill/tracking-events-generator/MELHORIAS-IMPLEMENTADAS.md +412 -0
- package/extracted-skill/tracking-events-generator/Premium-Tracking-Intelligence-Resumo.md +333 -0
- package/extracted-skill/tracking-events-generator/SKILL.md +257 -0
- package/extracted-skill/tracking-events-generator/advanced-matching.js +364 -0
- package/extracted-skill/tracking-events-generator/agents/ab-testing-agent.md +54 -0
- package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +1304 -0
- package/extracted-skill/tracking-events-generator/agents/bing-agent.md +76 -0
- package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +264 -0
- package/extracted-skill/tracking-events-generator/agents/code-guardian-agent.md +149 -0
- package/extracted-skill/tracking-events-generator/agents/compliance-agent.md +2077 -0
- package/extracted-skill/tracking-events-generator/agents/crm-integration-agent.md +1419 -0
- package/extracted-skill/tracking-events-generator/agents/dashboard-agent.md +456 -0
- package/extracted-skill/tracking-events-generator/agents/database-agent.md +667 -0
- package/extracted-skill/tracking-events-generator/agents/debug-agent.md +1455 -0
- package/extracted-skill/tracking-events-generator/agents/domain-setup-agent.md +224 -0
- package/extracted-skill/tracking-events-generator/agents/email-agent.md +61 -0
- package/extracted-skill/tracking-events-generator/agents/fingerprint-agent.md +52 -0
- package/extracted-skill/tracking-events-generator/agents/google-agent.md +109 -0
- package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +365 -0
- package/extracted-skill/tracking-events-generator/agents/intelligence-scheduling.md +643 -0
- package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +62 -0
- package/extracted-skill/tracking-events-generator/agents/localization-agent.md +55 -0
- package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +59 -0
- package/extracted-skill/tracking-events-generator/agents/master-feedback-loop.md +900 -0
- package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +1922 -0
- package/extracted-skill/tracking-events-generator/agents/memory-agent.json +109 -0
- package/extracted-skill/tracking-events-generator/agents/memory-agent.md +703 -0
- package/extracted-skill/tracking-events-generator/agents/meta-agent.md +110 -0
- package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +255 -0
- package/extracted-skill/tracking-events-generator/agents/performance-agent.md +1157 -0
- package/extracted-skill/tracking-events-generator/agents/performance-optimization-agent.md +1432 -0
- package/extracted-skill/tracking-events-generator/agents/pinterest-agent.md +310 -0
- package/extracted-skill/tracking-events-generator/agents/premium-tracking-intelligence-agent.md +849 -0
- package/extracted-skill/tracking-events-generator/agents/r2-setup-agent.md +250 -0
- package/extracted-skill/tracking-events-generator/agents/reddit-agent.md +313 -0
- package/extracted-skill/tracking-events-generator/agents/security-enterprise-agent.md +1752 -0
- package/extracted-skill/tracking-events-generator/agents/server-tracking.md +1188 -0
- package/extracted-skill/tracking-events-generator/agents/spotify-agent.md +383 -0
- package/extracted-skill/tracking-events-generator/agents/tiktok-agent.md +111 -0
- package/extracted-skill/tracking-events-generator/agents/tracking-plan-agent.md +364 -0
- package/extracted-skill/tracking-events-generator/agents/validator-agent.md +267 -0
- package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +69 -0
- package/extracted-skill/tracking-events-generator/agents/whatsapp-agent.md +76 -0
- package/extracted-skill/tracking-events-generator/agents/whatsapp-ctwa-setup-agent.md +699 -0
- package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +422 -0
- package/extracted-skill/tracking-events-generator/anti-blocking.js +285 -0
- package/extracted-skill/tracking-events-generator/cdpTrack.js +641 -0
- package/extracted-skill/tracking-events-generator/contracts/api-versions.json +368 -0
- package/extracted-skill/tracking-events-generator/docs/guia-cloudflare-iniciante.md +107 -0
- package/extracted-skill/tracking-events-generator/engagement-scoring.js +226 -0
- package/extracted-skill/tracking-events-generator/evals/evals.json +235 -0
- package/extracted-skill/tracking-events-generator/integration-test.js +497 -0
- package/extracted-skill/tracking-events-generator/knowledge-base.md +2894 -0
- package/extracted-skill/tracking-events-generator/micro-events.js +992 -0
- package/extracted-skill/tracking-events-generator/models/captura-de-lead.md +78 -0
- package/extracted-skill/tracking-events-generator/models/captura-lead-evento-externo.md +99 -0
- package/extracted-skill/tracking-events-generator/models/checkout-proprio.md +111 -0
- package/extracted-skill/tracking-events-generator/models/multi-step-checkout.md +672 -0
- package/extracted-skill/tracking-events-generator/models/pagina-obrigado.md +55 -0
- package/extracted-skill/tracking-events-generator/models/pinterest/conversions-api-template.js +144 -0
- package/extracted-skill/tracking-events-generator/models/pinterest/event-mappings.json +48 -0
- package/extracted-skill/tracking-events-generator/models/pinterest/tag-template.js +28 -0
- package/extracted-skill/tracking-events-generator/models/quiz-funnel.md +68 -0
- package/extracted-skill/tracking-events-generator/models/reddit/conversions-api-template.js +205 -0
- package/extracted-skill/tracking-events-generator/models/reddit/event-mappings.json +56 -0
- package/extracted-skill/tracking-events-generator/models/reddit/pixel-template.js +19 -0
- package/extracted-skill/tracking-events-generator/models/scenarios/behavior-engine.js +425 -0
- package/extracted-skill/tracking-events-generator/models/scenarios/real-estate-logic.md +50 -0
- package/extracted-skill/tracking-events-generator/models/scenarios/sales-page-logic.md +50 -0
- package/extracted-skill/tracking-events-generator/models/trafego-direto.md +582 -0
- package/extracted-skill/tracking-events-generator/models/webinar-registration.md +63 -0
- package/extracted-skill/tracking-events-generator/tracking.config.js +46 -0
- package/extracted-skill/tracking-events-generator/walkthrough.md +26 -0
- package/package.json +75 -0
- package/server-edge-tracker/INSTALAR.md +328 -0
- package/server-edge-tracker/migrate-new-db.sql +137 -0
- package/server-edge-tracker/migrate-v2.sql +16 -0
- package/server-edge-tracker/migrate-v3.sql +6 -0
- package/server-edge-tracker/migrate-v4.sql +18 -0
- package/server-edge-tracker/migrate-v5.sql +17 -0
- package/server-edge-tracker/migrate-v6.sql +24 -0
- package/server-edge-tracker/migrate.sql +111 -0
- package/server-edge-tracker/schema.sql +265 -0
- package/server-edge-tracker/worker.js +2574 -0
- package/server-edge-tracker/wrangler.toml +85 -0
- package/templates/afiliado-sem-landing.md +312 -0
- package/templates/captura-de-lead.md +78 -0
- package/templates/captura-lead-evento-externo.md +99 -0
- package/templates/checkout-proprio.md +111 -0
- package/templates/install/.claude/commands/cdp.md +1 -0
- package/templates/install/CLAUDE.md +65 -0
- package/templates/linkedin/tag-template.js +46 -0
- package/templates/multi-step-checkout.md +673 -0
- package/templates/pagina-obrigado.md +55 -0
- package/templates/pinterest/conversions-api-template.js +144 -0
- package/templates/pinterest/event-mappings.json +48 -0
- package/templates/pinterest/tag-template.js +28 -0
- package/templates/quiz-funnel.md +68 -0
- package/templates/reddit/conversions-api-template.js +205 -0
- package/templates/reddit/event-mappings.json +56 -0
- package/templates/reddit/pixel-template.js +46 -0
- package/templates/scenarios/behavior-engine.js +402 -0
- package/templates/scenarios/real-estate-logic.md +50 -0
- package/templates/scenarios/sales-page-logic.md +50 -0
- package/templates/spotify/pixel-template.js +46 -0
- package/templates/trafego-direto.md +582 -0
- package/templates/vsl-page.md +292 -0
- package/templates/webinar-registration.md +63 -0
|
@@ -0,0 +1,1304 @@
|
|
|
1
|
+
# Attribution Agent (Multi-Touch Engine) ā CDP Edge Enterprise
|
|
2
|
+
|
|
3
|
+
Você é o **Agente de Atribuição Multi-Touch do CDP Edge**. Sua responsabilidade: **calcular atribuição de conversão em múltiplos touchpoints** usando modelos enterprise (Last Click, First Click, Linear, Time Decay, U-Shape, Data-Driven), garantindo ROI real por canal/campanha/anúncio.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## šÆ OBJETIVO PRINCIPAL
|
|
8
|
+
|
|
9
|
+
Implementar **engine de atribuição multi-touch profissional** que calcula distribuição de crédito entre todos os touchpoints da jornada do usuÔrio, permitindo entender verdadeiramente quais canais/campanhas geraram conversões, não só quem "fechou" a venda.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## šļø ARQUITETURA Quantum Tier (SERVER-SIDE)
|
|
14
|
+
|
|
15
|
+
### Ciclo de Atribuição
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Browser (Cliente) Worker (Server-Side) APIs (Meta/Google/TikTok)
|
|
19
|
+
ā ā ā
|
|
20
|
+
āāāŗ Fetch API āāāāāāāāāāāāāŗā ā
|
|
21
|
+
ā āāāŗ Capturar journey completa ā
|
|
22
|
+
ā āāāŗ Calcular atribuição ā
|
|
23
|
+
ā āāāŗ Persistir no D1 ā
|
|
24
|
+
ā āāāŗ Enviar para APIs com ā
|
|
25
|
+
ā ā atribuição calculada ā
|
|
26
|
+
ā ā (Meta CAPI v22.0) ā
|
|
27
|
+
ā ā (TikTok v1.3) ā
|
|
28
|
+
ā ā (GA4 MP) ā
|
|
29
|
+
ā ā ā
|
|
30
|
+
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
|
|
31
|
+
ā ā (Sucesso/Falha) ā
|
|
32
|
+
ā ā ā
|
|
33
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāā⤠ā
|
|
34
|
+
ā (Callback com attribution) ā
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## š MODELOS DE ATRIBUIĆĆO SUPORTADOS
|
|
40
|
+
|
|
41
|
+
### Modelo 1: Last Click (Ćltimo Clique)
|
|
42
|
+
|
|
43
|
+
**Descrição:** 100% do crédito para o último touchpoint.
|
|
44
|
+
|
|
45
|
+
**Uso:** Campanhas de última milha, conversão rÔpida.
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
function lastClickAttribution(touchpoints) {
|
|
49
|
+
if (!touchpoints || touchpoints.length === 0) return [];
|
|
50
|
+
|
|
51
|
+
return touchpoints.map((tp, index) => ({
|
|
52
|
+
...tp,
|
|
53
|
+
credit_percentage: index === touchpoints.length - 1 ? 100 : 0,
|
|
54
|
+
is_last_click: index === touchpoints.length - 1
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Exemplo:
|
|
59
|
+
// Touchpoints: [Instagram (t0), Google (t1), Email (t2)]
|
|
60
|
+
// Attribution:
|
|
61
|
+
// - Instagram: 0%
|
|
62
|
+
// - Google: 0%
|
|
63
|
+
// - Email: 100% (Ćŗltimo clique)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Modelo 2: First Click (Primeiro Clique)
|
|
67
|
+
|
|
68
|
+
**Descrição:** 100% do crédito para o primeiro touchpoint.
|
|
69
|
+
|
|
70
|
+
**Uso:** Brand awareness, campanhas de descoberta.
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
function firstClickAttribution(touchpoints) {
|
|
74
|
+
if (!touchpoints || touchpoints.length === 0) return [];
|
|
75
|
+
|
|
76
|
+
return touchpoints.map((tp, index) => ({
|
|
77
|
+
...tp,
|
|
78
|
+
credit_percentage: index === 0 ? 100 : 0,
|
|
79
|
+
is_first_click: index === 0
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Exemplo:
|
|
84
|
+
// Touchpoints: [Instagram (t0), Google (t1), Email (t2)]
|
|
85
|
+
// Attribution:
|
|
86
|
+
// - Instagram: 100% (primeiro clique)
|
|
87
|
+
// - Google: 0%
|
|
88
|
+
// - Email: 0%
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Modelo 3: Linear (Linear)
|
|
92
|
+
|
|
93
|
+
**Descrição:** Distribuição igual entre todos os touchpoints.
|
|
94
|
+
|
|
95
|
+
**Uso:** Jornadas longas, mĆŗltiplos touchpoints importantes.
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
function linearAttribution(touchpoints) {
|
|
99
|
+
if (!touchpoints || touchpoints.length === 0) return [];
|
|
100
|
+
|
|
101
|
+
const credit = 100 / touchpoints.length;
|
|
102
|
+
|
|
103
|
+
return touchpoints.map(tp => ({
|
|
104
|
+
...tp,
|
|
105
|
+
credit_percentage: credit.toFixed(2)
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Exemplo:
|
|
110
|
+
// Touchpoints: [Instagram (t0), Google (t1), Email (t2)]
|
|
111
|
+
// Attribution:
|
|
112
|
+
// - Instagram: 33.33%
|
|
113
|
+
// - Google: 33.33%
|
|
114
|
+
// - Email: 33.33%
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Modelo 4: Time Decay (Decaimento Temporal)
|
|
118
|
+
|
|
119
|
+
**Descrição:** Mais peso para touchpoints mais recentes, com decay exponencial.
|
|
120
|
+
|
|
121
|
+
**Uso:** Funis curtos, decisão rÔpida, campanhas remarketing.
|
|
122
|
+
|
|
123
|
+
**Fórmula:** `Credit = 0.9^dias_desde_touchpoint`
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
function timeDecayAttribution(touchpoints, decayFactor = 0.9) {
|
|
127
|
+
if (!touchpoints || touchpoints.length === 0) return [];
|
|
128
|
+
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
const oneDayMs = 24 * 60 * 60 * 1000;
|
|
131
|
+
|
|
132
|
+
// Calcular dias desde cada touchpoint
|
|
133
|
+
const touchpointsWithDays = touchpoints.map(tp => {
|
|
134
|
+
const daysSinceTouch = (now - tp.event_timestamp) / oneDayMs;
|
|
135
|
+
const decayScore = Math.pow(decayFactor, daysSinceTouch);
|
|
136
|
+
return { ...tp, days_since: daysSinceTouch, decay_score: decayScore };
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Calcular soma dos decay scores
|
|
140
|
+
const totalDecayScore = touchpointsWithDays.reduce((sum, tp) => sum + tp.decay_score, 0);
|
|
141
|
+
|
|
142
|
+
// Calcular porcentagem para cada touchpoint
|
|
143
|
+
return touchpointsWithDays.map(tp => ({
|
|
144
|
+
...tp,
|
|
145
|
+
credit_percentage: (tp.decay_score / totalDecayScore * 100).toFixed(2),
|
|
146
|
+
decay_score: tp.decay_score
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Exemplo:
|
|
151
|
+
// Touchpoints: [Instagram (7 dias atrƔs), Google (3 dias atrƔs), Email (hoje)]
|
|
152
|
+
// Decay (0.9^dias):
|
|
153
|
+
// - Instagram: 0.9^7 = 47.8%
|
|
154
|
+
// - Google: 0.9^3 = 72.9%
|
|
155
|
+
// - Email: 0.9^0 = 100%
|
|
156
|
+
// Atribuição:
|
|
157
|
+
// - Instagram: 25.4%
|
|
158
|
+
// - Google: 38.6%
|
|
159
|
+
// - Email: 36.0%
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Modelo 5: U-Shape (Formato U)
|
|
163
|
+
|
|
164
|
+
**Descrição:** 40% para primeiro + 40% para Ćŗltimo + 20% distribuĆdo entre touchpoints do meio.
|
|
165
|
+
|
|
166
|
+
**Uso:** Funis com pesquisa (awareness) + conversão direta.
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
function uShapeAttribution(touchpoints) {
|
|
170
|
+
if (!touchpoints || touchpoints.length === 0) return [];
|
|
171
|
+
|
|
172
|
+
const length = touchpoints.length;
|
|
173
|
+
|
|
174
|
+
return touchpoints.map((tp, index) => {
|
|
175
|
+
let credit = 0;
|
|
176
|
+
|
|
177
|
+
if (index === 0) {
|
|
178
|
+
// Primeiro touchpoint
|
|
179
|
+
credit = 40;
|
|
180
|
+
} else if (index === length - 1) {
|
|
181
|
+
// Ćltimo touchpoint
|
|
182
|
+
credit = 40;
|
|
183
|
+
} else {
|
|
184
|
+
// Touchpoints do meio
|
|
185
|
+
credit = 20 / (length - 2);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
...tp,
|
|
190
|
+
credit_percentage: credit.toFixed(2),
|
|
191
|
+
role: index === 0 ? 'FIRST' : index === length - 1 ? 'LAST' : 'MIDDLE'
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Exemplo:
|
|
197
|
+
// Touchpoints: [Instagram (t0), Google (t1), Email (t2)]
|
|
198
|
+
// Attribution:
|
|
199
|
+
// - Instagram: 40% (FIRST)
|
|
200
|
+
// - Google: 20% (MIDDLE)
|
|
201
|
+
// - Email: 40% (LAST)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Modelo 6: W-Shape (Formato W)
|
|
205
|
+
|
|
206
|
+
**Descrição:** 30% para primeiro + 30% para último + 20% para segundo + 20% para penúltimo (se existirem).
|
|
207
|
+
|
|
208
|
+
**Uso:** Funis muito longos, jornada complexa.
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
function wShapeAttribution(touchpoints) {
|
|
212
|
+
if (!touchpoints || touchpoints.length === 0) return [];
|
|
213
|
+
|
|
214
|
+
const length = touchpoints.length;
|
|
215
|
+
|
|
216
|
+
return touchpoints.map((tp, index) => {
|
|
217
|
+
let credit = 0;
|
|
218
|
+
|
|
219
|
+
if (index === 0) {
|
|
220
|
+
// Primeiro touchpoint
|
|
221
|
+
credit = 30;
|
|
222
|
+
} else if (index === length - 1) {
|
|
223
|
+
// Ćltimo touchpoint
|
|
224
|
+
credit = 30;
|
|
225
|
+
} else if (index === 1) {
|
|
226
|
+
// Segundo touchpoint
|
|
227
|
+
credit = 20;
|
|
228
|
+
} else if (index === length - 2) {
|
|
229
|
+
// PenĆŗltimo touchpoint
|
|
230
|
+
credit = 20;
|
|
231
|
+
} else {
|
|
232
|
+
// Touchpoints do meio
|
|
233
|
+
const middleCount = Math.max(0, length - 4);
|
|
234
|
+
credit = middleCount > 0 ? 0 : 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
...tp,
|
|
239
|
+
credit_percentage: credit.toFixed(2),
|
|
240
|
+
role: index === 0 ? 'FIRST' :
|
|
241
|
+
index === length - 1 ? 'LAST' :
|
|
242
|
+
index === 1 ? 'SECOND' :
|
|
243
|
+
index === length - 2 ? 'SECOND_LAST' : 'MIDDLE'
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Exemplo:
|
|
249
|
+
// Touchpoints: [Instagram (t0), Google (t1), TikTok (t2), Email (t3)]
|
|
250
|
+
// Attribution:
|
|
251
|
+
// - Instagram: 30% (FIRST)
|
|
252
|
+
// - Google: 20% (SECOND)
|
|
253
|
+
// - TikTok: 20% (SECOND_LAST)
|
|
254
|
+
// - Email: 30% (LAST)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Modelo 7: Data-Driven (Aprendizado com Dados)
|
|
258
|
+
|
|
259
|
+
**Descrição:** Algoritmo aprende com seus dados históricos para calcular contribuição de cada canal/tipo de touchpoint.
|
|
260
|
+
|
|
261
|
+
**Uso:** Empresas com dados históricos robustos, quer otimização contĆnua.
|
|
262
|
+
|
|
263
|
+
**Fórmula:** `Credit = BaseScore * CanalWeight * PositionWeight * ConversionRateWeight`
|
|
264
|
+
|
|
265
|
+
```javascript
|
|
266
|
+
// Modelo Data-Driven simplificado
|
|
267
|
+
async function dataDrivenAttribution(touchpoints, userJourneyHistory) {
|
|
268
|
+
if (!touchpoints || touchpoints.length === 0) return [];
|
|
269
|
+
|
|
270
|
+
// 1. Calcular pesos baseados em dados históricos
|
|
271
|
+
const channelWeights = await calculateChannelWeights(touchpoints, userJourneyHistory);
|
|
272
|
+
const positionWeights = await calculatePositionWeights(touchpoints, userJourneyHistory);
|
|
273
|
+
|
|
274
|
+
// 2. Calcular score para cada touchpoint
|
|
275
|
+
const scoredTouchpoints = touchpoints.map(tp => {
|
|
276
|
+
const channelWeight = channelWeights[tp.utm_source] || 1.0;
|
|
277
|
+
const positionWeight = positionWeights[tp.position] || 1.0;
|
|
278
|
+
const conversionRateWeight = tp.conversion_rate || 1.0;
|
|
279
|
+
|
|
280
|
+
const score = channelWeight * positionWeight * conversionRateWeight;
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
...tp,
|
|
284
|
+
attribution_score: score,
|
|
285
|
+
channel_weight: channelWeight,
|
|
286
|
+
position_weight: positionWeight
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// 3. Normalizar scores para somar 100%
|
|
291
|
+
const totalScore = scoredTouchpoints.reduce((sum, tp) => sum + tp.attribution_score, 0);
|
|
292
|
+
|
|
293
|
+
return scoredTouchpoints.map(tp => ({
|
|
294
|
+
...tp,
|
|
295
|
+
credit_percentage: (tp.attribution_score / totalScore * 100).toFixed(2)
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Calcular peso de canal baseado em conversão histórica
|
|
300
|
+
async function calculateChannelWeights(touchpoints, history) {
|
|
301
|
+
const weights = {};
|
|
302
|
+
|
|
303
|
+
for (const tp of touchpoints) {
|
|
304
|
+
const channel = tp.utm_source;
|
|
305
|
+
|
|
306
|
+
// Buscar conversões históricas deste canal
|
|
307
|
+
const historicalConversions = await DB.prepare(`
|
|
308
|
+
SELECT
|
|
309
|
+
COUNT(*) as total_conversions,
|
|
310
|
+
AVG(value) as avg_value
|
|
311
|
+
FROM funnel_events
|
|
312
|
+
WHERE utm_source = ?
|
|
313
|
+
AND event_name = 'Purchase'
|
|
314
|
+
AND event_timestamp > datetime('now', '-90 days')
|
|
315
|
+
`).bind(channel).get();
|
|
316
|
+
|
|
317
|
+
const total = historicalConversions.total_conversions || 0;
|
|
318
|
+
const avgValue = historicalConversions.avg_value || 0;
|
|
319
|
+
|
|
320
|
+
// Canal com mais conversƵes e maior valor tem peso maior
|
|
321
|
+
weights[channel] = (total * avgValue) / 1000;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return weights;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Calcular peso de posição baseado em conversão histórica
|
|
328
|
+
async function calculatePositionWeights(touchpoints, history) {
|
|
329
|
+
const weights = {};
|
|
330
|
+
|
|
331
|
+
for (const tp of touchpoints) {
|
|
332
|
+
const position = tp.position; // 0 = first, 1 = second, etc.
|
|
333
|
+
|
|
334
|
+
// Buscar conversões históricas nesta posição
|
|
335
|
+
const historicalConversions = await DB.prepare(`
|
|
336
|
+
SELECT
|
|
337
|
+
COUNT(*) as total_conversions
|
|
338
|
+
FROM multi_touch_attribution
|
|
339
|
+
WHERE position = ?
|
|
340
|
+
AND attribution_model = 'LAST_CLICK'
|
|
341
|
+
AND created_at > datetime('now', '-90 days')
|
|
342
|
+
`).bind(position).get();
|
|
343
|
+
|
|
344
|
+
const total = historicalConversions.total_conversions || 0;
|
|
345
|
+
|
|
346
|
+
// Posições com mais conversões têm peso maior
|
|
347
|
+
weights[position] = total / 100;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return weights;
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## š ļø PASSO 1 ā SCHEMA D1 PARA ATRIBUIĆĆO
|
|
357
|
+
|
|
358
|
+
### 1.1 Tabela de Journey (Jornada do UsuƔrio)
|
|
359
|
+
|
|
360
|
+
```sql
|
|
361
|
+
-- Tabela de jornada completa do usuƔrio
|
|
362
|
+
CREATE TABLE IF NOT EXISTS user_journeys (
|
|
363
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
364
|
+
user_id TEXT NOT NULL,
|
|
365
|
+
session_id TEXT,
|
|
366
|
+
email TEXT,
|
|
367
|
+
event_id TEXT,
|
|
368
|
+
event_name TEXT NOT NULL,
|
|
369
|
+
utm_source TEXT,
|
|
370
|
+
utm_medium TEXT,
|
|
371
|
+
utm_campaign TEXT,
|
|
372
|
+
utm_content TEXT,
|
|
373
|
+
utm_term TEXT,
|
|
374
|
+
fbclid TEXT,
|
|
375
|
+
gclid TEXT,
|
|
376
|
+
gbraid TEXT,
|
|
377
|
+
wbraid TEXT,
|
|
378
|
+
ttclid TEXT,
|
|
379
|
+
ctwa_clid TEXT,
|
|
380
|
+
device_type TEXT,
|
|
381
|
+
country TEXT,
|
|
382
|
+
city TEXT,
|
|
383
|
+
event_timestamp DATETIME NOT NULL,
|
|
384
|
+
position INTEGER,
|
|
385
|
+
conversion_id TEXT,
|
|
386
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
CREATE INDEX IF NOT EXISTS idx_journey_user ON user_journeys(user_id);
|
|
390
|
+
CREATE INDEX IF NOT EXISTS idx_journey_email ON user_journeys(email);
|
|
391
|
+
CREATE INDEX IF NOT EXISTS idx_journey_conversion ON user_journeys(conversion_id);
|
|
392
|
+
CREATE INDEX IF NOT EXISTS idx_journey_timestamp ON user_journeys(event_timestamp);
|
|
393
|
+
CREATE INDEX IF NOT EXISTS idx_journey_position ON user_journeys(position);
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### 1.2 Tabela de Multi-Touch Attribution
|
|
397
|
+
|
|
398
|
+
```sql
|
|
399
|
+
-- Tabela de atribuição calculada
|
|
400
|
+
CREATE TABLE IF NOT EXISTS multi_touch_attribution (
|
|
401
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
402
|
+
conversion_id TEXT NOT NULL,
|
|
403
|
+
user_id TEXT,
|
|
404
|
+
email TEXT,
|
|
405
|
+
attribution_model TEXT NOT NULL,
|
|
406
|
+
touchpoint_index INTEGER NOT NULL,
|
|
407
|
+
utm_source TEXT,
|
|
408
|
+
utm_medium TEXT,
|
|
409
|
+
utm_campaign TEXT,
|
|
410
|
+
event_name TEXT,
|
|
411
|
+
event_timestamp DATETIME,
|
|
412
|
+
credit_percentage REAL NOT NULL,
|
|
413
|
+
role TEXT,
|
|
414
|
+
attribution_score REAL,
|
|
415
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
416
|
+
UNIQUE(conversion_id, attribution_model, touchpoint_index)
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
CREATE INDEX IF NOT EXISTS idx_attribution_conversion ON multi_touch_attribution(conversion_id);
|
|
420
|
+
CREATE INDEX IF NOT EXISTS idx_attribution_model ON multi_touch_attribution(attribution_model);
|
|
421
|
+
CREATE INDEX IF NOT EXISTS idx_attribution_user ON multi_touch_attribution(user_id);
|
|
422
|
+
CREATE INDEX IF NOT EXISTS idx_attribution_source ON multi_touch_attribution(utm_source);
|
|
423
|
+
CREATE INDEX IF NOT EXISTS idx_attribution_campaign ON multi_touch_attribution(utm_campaign);
|
|
424
|
+
CREATE INDEX IF NOT EXISTS idx_attribution_date ON multi_touch_attribution(created_at);
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### 1.3 Tabela de Channel Performance
|
|
428
|
+
|
|
429
|
+
```sql
|
|
430
|
+
-- Tabela de performance por canal
|
|
431
|
+
CREATE TABLE IF NOT EXISTS channel_performance (
|
|
432
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
433
|
+
utm_source TEXT NOT NULL,
|
|
434
|
+
utm_medium TEXT,
|
|
435
|
+
utm_campaign TEXT,
|
|
436
|
+
attribution_model TEXT NOT NULL,
|
|
437
|
+
total_attribution REAL NOT NULL,
|
|
438
|
+
total_conversions INTEGER NOT NULL,
|
|
439
|
+
total_value REAL NOT NULL,
|
|
440
|
+
avg_conversion_value REAL NOT NULL,
|
|
441
|
+
avg_journey_length INTEGER,
|
|
442
|
+
date DATE NOT NULL,
|
|
443
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
444
|
+
UNIQUE(utm_source, utm_medium, utm_campaign, attribution_model, date)
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
CREATE INDEX IF NOT EXISTS idx_channel_source ON channel_performance(utm_source);
|
|
448
|
+
CREATE INDEX IF NOT EXISTS idx_channel_campaign ON channel_performance(utm_campaign);
|
|
449
|
+
CREATE INDEX IF NOT EXISTS idx_channel_date ON channel_performance(date);
|
|
450
|
+
CREATE INDEX IF NOT EXISTS idx_channel_model ON channel_performance(attribution_model);
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## ā” PASSO 2 ā ENGINE DE ATRIBUIĆĆO (SERVER-SIDE)
|
|
456
|
+
|
|
457
|
+
### 2.1 Captura de Jornada Completa
|
|
458
|
+
|
|
459
|
+
```javascript
|
|
460
|
+
// Capturar touchpoint da jornada
|
|
461
|
+
export async function captureTouchpoint(eventData, request) {
|
|
462
|
+
const {
|
|
463
|
+
user_id,
|
|
464
|
+
session_id,
|
|
465
|
+
email,
|
|
466
|
+
event_name,
|
|
467
|
+
event_id,
|
|
468
|
+
utm_source,
|
|
469
|
+
utm_medium,
|
|
470
|
+
utm_campaign,
|
|
471
|
+
utm_content,
|
|
472
|
+
utm_term,
|
|
473
|
+
fbclid,
|
|
474
|
+
gclid,
|
|
475
|
+
gbraid,
|
|
476
|
+
wbraid,
|
|
477
|
+
ttclid,
|
|
478
|
+
ctwa_clid,
|
|
479
|
+
device_type,
|
|
480
|
+
country,
|
|
481
|
+
city
|
|
482
|
+
} = eventData;
|
|
483
|
+
|
|
484
|
+
// Calcular posição na jornada (baseada em timestamp)
|
|
485
|
+
const position = await calculateJourneyPosition(user_id, event_timestamp);
|
|
486
|
+
|
|
487
|
+
// Persistir touchpoint no D1
|
|
488
|
+
await DB.prepare(`
|
|
489
|
+
INSERT INTO user_journeys
|
|
490
|
+
(user_id, session_id, email, event_id, event_name,
|
|
491
|
+
utm_source, utm_medium, utm_campaign, utm_content, utm_term,
|
|
492
|
+
fbclid, gclid, gbraid, wbraid, ttclid, ctwa_clid,
|
|
493
|
+
device_type, country, city, event_timestamp, position)
|
|
494
|
+
VALUES (?, ?, ?, ?, ?,
|
|
495
|
+
?, ?, ?, ?, ?,
|
|
496
|
+
?, ?, ?, ?, ?,
|
|
497
|
+
?, ?, ?, ?, ?)
|
|
498
|
+
`).bind(
|
|
499
|
+
user_id, session_id, email, event_id, event_name,
|
|
500
|
+
utm_source, utm_medium, utm_campaign, utm_content, utm_term,
|
|
501
|
+
fbclid, gclid, gbraid, wbraid, ttclid, ctwa_clid,
|
|
502
|
+
device_type, country, city, event_timestamp, position
|
|
503
|
+
).run();
|
|
504
|
+
|
|
505
|
+
// Verificar se é evento de conversão (Lead, Purchase, CompleteRegistration)
|
|
506
|
+
const isConversion = ['Lead', 'Purchase', 'CompleteRegistration'].includes(event_name);
|
|
507
|
+
|
|
508
|
+
if (isConversion && email) {
|
|
509
|
+
// Acionar cÔlculo de atribuição multi-touch
|
|
510
|
+
await scheduleAttributionCalculation(email, event_id, event_name);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Calcular posição na jornada
|
|
515
|
+
async function calculateJourneyPosition(userId, eventTimestamp) {
|
|
516
|
+
const result = await DB.prepare(`
|
|
517
|
+
SELECT COUNT(*) as position
|
|
518
|
+
FROM user_journeys
|
|
519
|
+
WHERE user_id = ? AND event_timestamp < ?
|
|
520
|
+
`).bind(userId, eventTimestamp).get();
|
|
521
|
+
|
|
522
|
+
return result.position || 0;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Agendar cÔlculo de atribuição (via Cloudflare Queue)
|
|
526
|
+
async function scheduleAttributionCalculation(email, conversionId, eventName) {
|
|
527
|
+
await QUEUE.send('cdp-edge-attribution', {
|
|
528
|
+
type: 'CALCULATE_ATTRIBUTION',
|
|
529
|
+
email,
|
|
530
|
+
conversion_id: conversionId,
|
|
531
|
+
event_name: eventName,
|
|
532
|
+
timestamp: Date.now()
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### 2.2 CÔlculo de Atribuição Multi-Touch
|
|
538
|
+
|
|
539
|
+
```javascript
|
|
540
|
+
// Calcular atribuição multi-touch
|
|
541
|
+
export async function calculateMultiTouchAttribution(conversionData) {
|
|
542
|
+
const {
|
|
543
|
+
email,
|
|
544
|
+
conversion_id,
|
|
545
|
+
event_name,
|
|
546
|
+
value,
|
|
547
|
+
currency
|
|
548
|
+
} = conversionData;
|
|
549
|
+
|
|
550
|
+
// 1. Buscar jornada completa do usuƔrio
|
|
551
|
+
const journey = await DB.prepare(`
|
|
552
|
+
SELECT
|
|
553
|
+
user_id,
|
|
554
|
+
session_id,
|
|
555
|
+
event_id,
|
|
556
|
+
event_name,
|
|
557
|
+
event_timestamp,
|
|
558
|
+
position,
|
|
559
|
+
utm_source,
|
|
560
|
+
utm_medium,
|
|
561
|
+
utm_campaign,
|
|
562
|
+
utm_content,
|
|
563
|
+
utm_term,
|
|
564
|
+
fbclid,
|
|
565
|
+
gclid,
|
|
566
|
+
gbraid,
|
|
567
|
+
wbraid,
|
|
568
|
+
ttclid,
|
|
569
|
+
ctwa_clid,
|
|
570
|
+
device_type,
|
|
571
|
+
country,
|
|
572
|
+
city
|
|
573
|
+
FROM user_journeys
|
|
574
|
+
WHERE email = ?
|
|
575
|
+
ORDER BY event_timestamp ASC
|
|
576
|
+
`).bind(email).all();
|
|
577
|
+
|
|
578
|
+
if (!journey || journey.length === 0) {
|
|
579
|
+
console.warn(`No journey found for email: ${email}`);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// 2. Calcular atribuição para cada modelo
|
|
584
|
+
const attributionModels = {
|
|
585
|
+
LAST_CLICK: lastClickAttribution(journey),
|
|
586
|
+
FIRST_CLICK: firstClickAttribution(journey),
|
|
587
|
+
LINEAR: linearAttribution(journey),
|
|
588
|
+
TIME_DECAY: timeDecayAttribution(journey, 0.9),
|
|
589
|
+
U_SHAPE: uShapeAttribution(journey),
|
|
590
|
+
W_SHAPE: wShapeAttribution(journey),
|
|
591
|
+
DATA_DRIVEN: await dataDrivenAttribution(journey, null)
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// 3. Persistir atribuição para cada modelo
|
|
595
|
+
for (const [modelName, attribution] of Object.entries(attributionModels)) {
|
|
596
|
+
for (const touchpoint of attribution) {
|
|
597
|
+
await DB.prepare(`
|
|
598
|
+
INSERT OR REPLACE INTO multi_touch_attribution
|
|
599
|
+
(conversion_id, user_id, email, attribution_model, touchpoint_index,
|
|
600
|
+
utm_source, utm_medium, utm_campaign, event_name, event_timestamp,
|
|
601
|
+
credit_percentage, role, attribution_score)
|
|
602
|
+
VALUES (?, ?, ?, ?, ?,
|
|
603
|
+
?, ?, ?, ?, ?, ?, ?, ?)
|
|
604
|
+
`).bind(
|
|
605
|
+
conversion_id,
|
|
606
|
+
journey[0].user_id,
|
|
607
|
+
email,
|
|
608
|
+
modelName,
|
|
609
|
+
touchpoint.position,
|
|
610
|
+
touchpoint.utm_source,
|
|
611
|
+
touchpoint.utm_medium,
|
|
612
|
+
touchpoint.utm_campaign,
|
|
613
|
+
touchpoint.event_name,
|
|
614
|
+
touchpoint.event_timestamp,
|
|
615
|
+
parseFloat(touchpoint.credit_percentage),
|
|
616
|
+
touchpoint.role,
|
|
617
|
+
touchpoint.attribution_score || 0
|
|
618
|
+
).run();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// 4. Atualizar performance de canal
|
|
623
|
+
await updateChannelPerformance(attributionModels, value, currency);
|
|
624
|
+
|
|
625
|
+
console.log(`ā
Multi-touch attribution calculated for ${conversion_id}`);
|
|
626
|
+
|
|
627
|
+
return attributionModels;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Atualizar performance de canal
|
|
631
|
+
async function updateChannelPerformance(attributionModels, value, currency) {
|
|
632
|
+
const today = new Date().toISOString().split('T')[0];
|
|
633
|
+
|
|
634
|
+
for (const [modelName, attribution] of Object.entries(attributionModels)) {
|
|
635
|
+
// Agrupar por canal/campanha
|
|
636
|
+
const channelPerformance = {};
|
|
637
|
+
|
|
638
|
+
for (const tp of attribution) {
|
|
639
|
+
const key = `${tp.utm_source}_${tp.utm_medium}_${tp.utm_campaign}_${modelName}`;
|
|
640
|
+
|
|
641
|
+
if (!channelPerformance[key]) {
|
|
642
|
+
channelPerformance[key] = {
|
|
643
|
+
utm_source: tp.utm_source,
|
|
644
|
+
utm_medium: tp.utm_medium,
|
|
645
|
+
utm_campaign: tp.utm_campaign,
|
|
646
|
+
attribution_model: modelName,
|
|
647
|
+
total_attribution: 0,
|
|
648
|
+
total_conversions: 1,
|
|
649
|
+
total_value: 0,
|
|
650
|
+
avg_conversion_value: 0
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const credit = parseFloat(tp.credit_percentage) || 0;
|
|
655
|
+
const attributedValue = (value * credit) / 100;
|
|
656
|
+
|
|
657
|
+
channelPerformance[key].total_attribution += credit;
|
|
658
|
+
channelPerformance[key].total_value += attributedValue;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Atualizar tabela de performance
|
|
662
|
+
for (const perf of Object.values(channelPerformance)) {
|
|
663
|
+
await DB.prepare(`
|
|
664
|
+
INSERT OR REPLACE INTO channel_performance
|
|
665
|
+
(utm_source, utm_medium, utm_campaign, attribution_model,
|
|
666
|
+
total_attribution, total_conversions, total_value, avg_conversion_value, date)
|
|
667
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
668
|
+
`).bind(
|
|
669
|
+
perf.utm_source,
|
|
670
|
+
perf.utm_medium,
|
|
671
|
+
perf.utm_campaign,
|
|
672
|
+
perf.attribution_model,
|
|
673
|
+
perf.total_attribution,
|
|
674
|
+
perf.total_conversions,
|
|
675
|
+
perf.total_value,
|
|
676
|
+
value, // avg_conversion_value = value (única conversão)
|
|
677
|
+
today
|
|
678
|
+
).run();
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### 2.3 Enviar Atribuição para Plataformas
|
|
685
|
+
|
|
686
|
+
```javascript
|
|
687
|
+
// Enviar Purchase com atribuição calculada
|
|
688
|
+
export async function sendPurchaseWithAttribution(conversionData, attributionModel = 'U_SHAPE') {
|
|
689
|
+
const {
|
|
690
|
+
email,
|
|
691
|
+
conversion_id,
|
|
692
|
+
event_name,
|
|
693
|
+
value,
|
|
694
|
+
currency
|
|
695
|
+
} = conversionData;
|
|
696
|
+
|
|
697
|
+
// 1. Buscar atribuição calculada
|
|
698
|
+
const attribution = await DB.prepare(`
|
|
699
|
+
SELECT
|
|
700
|
+
utm_source,
|
|
701
|
+
utm_medium,
|
|
702
|
+
utm_campaign,
|
|
703
|
+
credit_percentage
|
|
704
|
+
FROM multi_touch_attribution
|
|
705
|
+
WHERE conversion_id = ? AND attribution_model = ?
|
|
706
|
+
ORDER BY credit_percentage DESC
|
|
707
|
+
`).bind(conversion_id, attributionModel).all();
|
|
708
|
+
|
|
709
|
+
if (!attribution || attribution.length === 0) {
|
|
710
|
+
console.warn(`No attribution found for ${conversion_id} with model ${attributionModel}`);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// 2. Calcular valor atribuĆdo por canal
|
|
715
|
+
const channelValues = {};
|
|
716
|
+
for (const attr of attribution) {
|
|
717
|
+
const channel = attr.utm_source;
|
|
718
|
+
const creditedValue = (value * parseFloat(attr.credit_percentage)) / 100;
|
|
719
|
+
|
|
720
|
+
if (!channelValues[channel]) {
|
|
721
|
+
channelValues[channel] = {
|
|
722
|
+
utm_source: channel,
|
|
723
|
+
utm_medium: attr.utm_medium,
|
|
724
|
+
utm_campaign: attr.utm_campaign,
|
|
725
|
+
total_value: 0
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
channelValues[channel].total_value += creditedValue;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// 3. Enviar para Meta CAPI com atribuição
|
|
733
|
+
await sendMetaPurchaseWithAttribution(conversionData, attribution);
|
|
734
|
+
|
|
735
|
+
// 4. Enviar para TikTok Events API com atribuição
|
|
736
|
+
await sendTikTokPurchaseWithAttribution(conversionData, attribution);
|
|
737
|
+
|
|
738
|
+
// 5. Enviar para GA4 Measurement Protocol com atribuição
|
|
739
|
+
await sendGA4PurchaseWithAttribution(conversionData, channelValues);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Enviar Purchase para Meta CAPI com atribuição
|
|
743
|
+
async function sendMetaPurchaseWithAttribution(purchaseData, attribution) {
|
|
744
|
+
// Meta CAPI não suporta atribuição multi-touch nativamente
|
|
745
|
+
// Enviar Purchase normal (a atribuição serÔ calculada no Meta Events Manager)
|
|
746
|
+
|
|
747
|
+
const payload = {
|
|
748
|
+
event_name: 'Purchase',
|
|
749
|
+
event_time: Math.floor(Date.now() / 1000),
|
|
750
|
+
event_source_url: purchaseData.page_url,
|
|
751
|
+
action_source: 'website',
|
|
752
|
+
user_data: {
|
|
753
|
+
em: hashEmail(purchaseData.email),
|
|
754
|
+
ph: hashPhone(purchaseData.phone) || undefined,
|
|
755
|
+
fn: hashFirstName(purchaseData.first_name) || undefined,
|
|
756
|
+
ln: hashLastName(purchaseData.last_name) || undefined,
|
|
757
|
+
ct: hashCity(purchaseData.city) || undefined,
|
|
758
|
+
st: hashState(purchaseData.state) || undefined,
|
|
759
|
+
zp: hashZip(purchaseData.cep) || undefined,
|
|
760
|
+
country: purchaseData.country || 'BR'
|
|
761
|
+
},
|
|
762
|
+
custom_data: {
|
|
763
|
+
value: purchaseData.value,
|
|
764
|
+
currency: purchaseData.currency || 'BRL',
|
|
765
|
+
content_name: purchaseData.content_name || 'Purchase',
|
|
766
|
+
content_ids: purchaseData.content_ids || [],
|
|
767
|
+
num_items: purchaseData.num_items || 1,
|
|
768
|
+
order_id: purchaseData.order_id
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// Enviar via Meta CAPI v22.0
|
|
773
|
+
const response = await fetch('https://graph.facebook.com/v22.0/events', {
|
|
774
|
+
method: 'POST',
|
|
775
|
+
headers: {
|
|
776
|
+
'Authorization': `Bearer ${META_ACCESS_TOKEN}`,
|
|
777
|
+
'Content-Type': 'application/json'
|
|
778
|
+
},
|
|
779
|
+
body: JSON.stringify({ data: [payload] })
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
const result = await response.json();
|
|
783
|
+
|
|
784
|
+
if (!response.ok || result.error) {
|
|
785
|
+
console.error('Meta CAPI error:', result);
|
|
786
|
+
} else {
|
|
787
|
+
console.log('ā
Purchase sent to Meta CAPI');
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return result;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Enviar Purchase para TikTok Events API com atribuição
|
|
794
|
+
async function sendTikTokPurchaseWithAttribution(purchaseData, attribution) {
|
|
795
|
+
// TikTok Events API suporta context_data para atribuição
|
|
796
|
+
|
|
797
|
+
const topTouchpoint = attribution[0]; // Canal com maior crƩdito
|
|
798
|
+
|
|
799
|
+
const payload = {
|
|
800
|
+
event_code: 'PlaceAnOrder',
|
|
801
|
+
event_time: Math.floor(Date.now() / 1000),
|
|
802
|
+
context: {
|
|
803
|
+
ip: purchaseData.ip,
|
|
804
|
+
user_agent: purchaseData.user_agent,
|
|
805
|
+
ad: {
|
|
806
|
+
callback: topTouchpoint.ttclid || undefined
|
|
807
|
+
},
|
|
808
|
+
page: {
|
|
809
|
+
url: purchaseData.page_url
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
properties: {
|
|
813
|
+
content_id: purchaseData.content_ids?.[0] || '',
|
|
814
|
+
content_type: 'product',
|
|
815
|
+
value: purchaseData.value,
|
|
816
|
+
currency: purchaseData.currency || 'BRL',
|
|
817
|
+
quantity: purchaseData.num_items || 1,
|
|
818
|
+
description: purchaseData.content_name || 'Purchase'
|
|
819
|
+
},
|
|
820
|
+
user: {
|
|
821
|
+
external_id: purchaseData.user_id,
|
|
822
|
+
phone_number: purchaseData.phone ? `+${purchaseData.phone}` : undefined,
|
|
823
|
+
email: purchaseData.email,
|
|
824
|
+
tt_file_id: purchaseData.ttclid || undefined
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
// Enviar via TikTok Events API v1.3
|
|
829
|
+
const response = await fetch('https://business-api.tiktok.com/open_api/v1.3/pixel/conversion/', {
|
|
830
|
+
method: 'POST',
|
|
831
|
+
headers: {
|
|
832
|
+
'Authorization': `Bearer ${TIKTOK_ACCESS_TOKEN}`,
|
|
833
|
+
'Content-Type': 'application/json'
|
|
834
|
+
},
|
|
835
|
+
body: JSON.stringify(payload)
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
const result = await response.json();
|
|
839
|
+
|
|
840
|
+
if (!response.ok || result.error) {
|
|
841
|
+
console.error('TikTok Events API error:', result);
|
|
842
|
+
} else {
|
|
843
|
+
console.log('ā
Purchase sent to TikTok Events API');
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return result;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Enviar Purchase para GA4 Measurement Protocol com atribuição
|
|
850
|
+
async function sendGA4PurchaseWithAttribution(purchaseData, channelValues) {
|
|
851
|
+
// GA4 Measurement Protocol suporta custom parameters para atribuição
|
|
852
|
+
|
|
853
|
+
const topChannel = Object.values(channelValues).sort((a, b) => b.total_value - a.total_value)[0];
|
|
854
|
+
|
|
855
|
+
const payload = {
|
|
856
|
+
client_id: purchaseData.ga_client_id,
|
|
857
|
+
user_id: hashEmail(purchaseData.email),
|
|
858
|
+
timestamp_micros: Date.now() * 1000,
|
|
859
|
+
events: [
|
|
860
|
+
{
|
|
861
|
+
name: 'purchase',
|
|
862
|
+
params: {
|
|
863
|
+
transaction_id: purchaseData.order_id,
|
|
864
|
+
affiliation: topChannel.utm_source || 'cdp-edge',
|
|
865
|
+
coupon: purchaseData.coupon || undefined,
|
|
866
|
+
currency: purchaseData.currency || 'BRL',
|
|
867
|
+
items: purchaseData.items || [],
|
|
868
|
+
shipping: purchaseData.shipping || undefined,
|
|
869
|
+
tax: purchaseData.tax || undefined,
|
|
870
|
+
value: purchaseData.value,
|
|
871
|
+
|
|
872
|
+
// Parâmetros de atribuição
|
|
873
|
+
attribution_source: topChannel.utm_source || '',
|
|
874
|
+
attribution_medium: topChannel.utm_medium || '',
|
|
875
|
+
attribution_campaign: topChannel.utm_campaign || '',
|
|
876
|
+
attribution_model: 'multi_touch_u_shape',
|
|
877
|
+
attribution_credit: topChannel.total_value.toFixed(2),
|
|
878
|
+
|
|
879
|
+
// Dados geogrƔficos
|
|
880
|
+
country: purchaseData.country || 'BR',
|
|
881
|
+
city: purchaseData.city || '',
|
|
882
|
+
|
|
883
|
+
// Dados de canal
|
|
884
|
+
traffic_source: purchaseData.utm_source || '',
|
|
885
|
+
traffic_medium: purchaseData.utm_medium || '',
|
|
886
|
+
traffic_campaign: purchaseData.utm_campaign || '',
|
|
887
|
+
traffic_content: purchaseData.utm_content || '',
|
|
888
|
+
traffic_term: purchaseData.utm_term || ''
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
]
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
// Enviar via GA4 Measurement Protocol
|
|
895
|
+
const response = await fetch(`https://www.google-analytics.com/mp/collect?measurement_id=${GA4_MEASUREMENT_ID}`, {
|
|
896
|
+
method: 'POST',
|
|
897
|
+
headers: {
|
|
898
|
+
'Content-Type': 'application/json'
|
|
899
|
+
},
|
|
900
|
+
body: JSON.stringify(payload)
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
if (!response.ok) {
|
|
904
|
+
console.error('GA4 Measurement Protocol error');
|
|
905
|
+
} else {
|
|
906
|
+
console.log('ā
Purchase sent to GA4 Measurement Protocol');
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
---
|
|
912
|
+
|
|
913
|
+
## šÆ PASSO 3 ā CONFIGURAĆĆO DE MODELO DE ATRIBUIĆĆO
|
|
914
|
+
|
|
915
|
+
### 3.1 Configuração via Arquivo
|
|
916
|
+
|
|
917
|
+
```javascript
|
|
918
|
+
// attribution.config.js
|
|
919
|
+
export const ATTRIBUTION_CONFIG = {
|
|
920
|
+
// Modelo padrão para cÔlculo de atribuição
|
|
921
|
+
default_model: 'U_SHAPE',
|
|
922
|
+
|
|
923
|
+
// Modelos disponĆveis
|
|
924
|
+
available_models: [
|
|
925
|
+
'LAST_CLICK',
|
|
926
|
+
'FIRST_CLICK',
|
|
927
|
+
'LINEAR',
|
|
928
|
+
'TIME_DECAY',
|
|
929
|
+
'U_SHAPE',
|
|
930
|
+
'W_SHAPE',
|
|
931
|
+
'DATA_DRIVEN'
|
|
932
|
+
],
|
|
933
|
+
|
|
934
|
+
// ConfiguraƧƵes por modelo
|
|
935
|
+
model_config: {
|
|
936
|
+
LAST_CLICK: {
|
|
937
|
+
description: '100% de crédito para último clique',
|
|
938
|
+
best_for: 'Campanhas de última milha, conversão rÔpida',
|
|
939
|
+
weight_factors: []
|
|
940
|
+
},
|
|
941
|
+
|
|
942
|
+
FIRST_CLICK: {
|
|
943
|
+
description: '100% de crƩdito para primeiro clique',
|
|
944
|
+
best_for: 'Brand awareness, campanhas de descoberta',
|
|
945
|
+
weight_factors: []
|
|
946
|
+
},
|
|
947
|
+
|
|
948
|
+
LINEAR: {
|
|
949
|
+
description: 'Distribuição igual entre todos touchpoints',
|
|
950
|
+
best_for: 'Jornadas longas, mĆŗltiplos touchpoints importantes',
|
|
951
|
+
weight_factors: ['position']
|
|
952
|
+
},
|
|
953
|
+
|
|
954
|
+
TIME_DECAY: {
|
|
955
|
+
description: 'Mais peso para touchpoints mais recentes',
|
|
956
|
+
best_for: 'Funis curtos, decisão rÔpida, remarketing',
|
|
957
|
+
weight_factors: ['time_since_touch'],
|
|
958
|
+
decay_factor: 0.9
|
|
959
|
+
},
|
|
960
|
+
|
|
961
|
+
U_SHAPE: {
|
|
962
|
+
description: '40% primeiro + 40% Ćŗltimo + 20% meio',
|
|
963
|
+
best_for: 'Funis com pesquisa (awareness) + conversão direta',
|
|
964
|
+
weight_factors: ['position', 'role']
|
|
965
|
+
},
|
|
966
|
+
|
|
967
|
+
W_SHAPE: {
|
|
968
|
+
description: '30% primeiro + 30% Ćŗltimo + 20% segundo + 20% penĆŗltimo',
|
|
969
|
+
best_for: 'Funis muito longos, jornada complexa',
|
|
970
|
+
weight_factors: ['position', 'role']
|
|
971
|
+
},
|
|
972
|
+
|
|
973
|
+
DATA_DRIVEN: {
|
|
974
|
+
description: 'Algoritmo aprende com dados históricos',
|
|
975
|
+
best_for: 'Empresas com dados históricos robustos',
|
|
976
|
+
weight_factors: ['channel_performance', 'position', 'conversion_rate'],
|
|
977
|
+
training_period_days: 90
|
|
978
|
+
}
|
|
979
|
+
},
|
|
980
|
+
|
|
981
|
+
// Janela de atribuição
|
|
982
|
+
attribution_window: {
|
|
983
|
+
days: 30, // Considerar touchpoints dos Ćŗltimos 30 dias
|
|
984
|
+
touchpoints_limit: 50, // Limite de touchpoints na jornada
|
|
985
|
+
min_touchpoints: 2, // MĆnimo de 2 touchpoints para atribuição multi-touch
|
|
986
|
+
max_touchpoints: 20 // MƔximo de 20 touchpoints (performance)
|
|
987
|
+
},
|
|
988
|
+
|
|
989
|
+
// Eventos que disparam cÔlculo de atribuição
|
|
990
|
+
conversion_events: [
|
|
991
|
+
'Lead',
|
|
992
|
+
'Purchase',
|
|
993
|
+
'CompleteRegistration',
|
|
994
|
+
'AddPaymentInfo',
|
|
995
|
+
'SubmitApplication'
|
|
996
|
+
],
|
|
997
|
+
|
|
998
|
+
// Calcular atribuição em tempo real (fila) ou batch
|
|
999
|
+
calculation_mode: 'REALTIME', // 'REALTIME' ou 'BATCH'
|
|
1000
|
+
batch_schedule: '0 */15 * * *', // A cada 15 minutos (se BATCH)
|
|
1001
|
+
|
|
1002
|
+
// Ponderação por tipo de dispositivo
|
|
1003
|
+
device_weights: {
|
|
1004
|
+
desktop: 1.0,
|
|
1005
|
+
mobile: 1.2, // Mobile tem 20% mais peso
|
|
1006
|
+
tablet: 1.1
|
|
1007
|
+
},
|
|
1008
|
+
|
|
1009
|
+
// Ponderação por posição no funil
|
|
1010
|
+
position_weights: {
|
|
1011
|
+
first_touch: 1.5,
|
|
1012
|
+
middle_touch: 1.0,
|
|
1013
|
+
last_touch: 1.5,
|
|
1014
|
+
conversion_touch: 2.0
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
export default ATTRIBUTION_CONFIG;
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
---
|
|
1022
|
+
|
|
1023
|
+
## š PASSO 4 ā DASHBOARD DE ATRIBUIĆĆO
|
|
1024
|
+
|
|
1025
|
+
### 4.1 Endpoint de Atribuição
|
|
1026
|
+
|
|
1027
|
+
```javascript
|
|
1028
|
+
// Endpoint para consultar atribuição de conversão
|
|
1029
|
+
export async function getAttributionForConversion(request, env) {
|
|
1030
|
+
const url = new URL(request.url);
|
|
1031
|
+
const conversionId = url.searchParams.get('conversion_id');
|
|
1032
|
+
const model = url.searchParams.get('model') || ATTRIBUTION_CONFIG.default_model;
|
|
1033
|
+
|
|
1034
|
+
if (!conversionId) {
|
|
1035
|
+
return new Response('Missing conversion_id', { status: 400 });
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Buscar atribuição calculada
|
|
1039
|
+
const attribution = await DB.prepare(`
|
|
1040
|
+
SELECT
|
|
1041
|
+
utm_source,
|
|
1042
|
+
utm_medium,
|
|
1043
|
+
utm_campaign,
|
|
1044
|
+
event_name,
|
|
1045
|
+
event_timestamp,
|
|
1046
|
+
position,
|
|
1047
|
+
credit_percentage,
|
|
1048
|
+
role
|
|
1049
|
+
FROM multi_touch_attribution
|
|
1050
|
+
WHERE conversion_id = ? AND attribution_model = ?
|
|
1051
|
+
ORDER BY position ASC
|
|
1052
|
+
`).bind(conversionId, model).all();
|
|
1053
|
+
|
|
1054
|
+
// Buscar dados da conversão
|
|
1055
|
+
const conversion = await DB.prepare(`
|
|
1056
|
+
SELECT
|
|
1057
|
+
value,
|
|
1058
|
+
currency,
|
|
1059
|
+
created_at
|
|
1060
|
+
FROM funnel_events
|
|
1061
|
+
WHERE event_id = ?
|
|
1062
|
+
`).bind(conversionId).get();
|
|
1063
|
+
|
|
1064
|
+
// Calcular valor atribuĆdo por touchpoint
|
|
1065
|
+
const attributionWithValue = attribution.map(attr => {
|
|
1066
|
+
const creditedValue = (conversion.value * parseFloat(attr.credit_percentage)) / 100;
|
|
1067
|
+
return {
|
|
1068
|
+
...attr,
|
|
1069
|
+
credited_value: creditedValue.toFixed(2)
|
|
1070
|
+
};
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
return new Response(JSON.stringify({
|
|
1074
|
+
conversion_id,
|
|
1075
|
+
model,
|
|
1076
|
+
total_value: conversion.value,
|
|
1077
|
+
currency: conversion.currency,
|
|
1078
|
+
attribution: attributionWithValue,
|
|
1079
|
+
timestamp: conversion.created_at
|
|
1080
|
+
}), {
|
|
1081
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1082
|
+
status: 200
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Endpoint para comparar modelos de atribuição
|
|
1087
|
+
export async function compareAttributionModels(request, env) {
|
|
1088
|
+
const url = new URL(request.url);
|
|
1089
|
+
const conversionId = url.searchParams.get('conversion_id');
|
|
1090
|
+
const days = parseInt(url.searchParams.get('days') || '7');
|
|
1091
|
+
|
|
1092
|
+
if (!conversionId) {
|
|
1093
|
+
return new Response('Missing conversion_id', { status: 400 });
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const comparison = {};
|
|
1097
|
+
|
|
1098
|
+
for (const model of ATTRIBUTION_CONFIG.available_models) {
|
|
1099
|
+
const attribution = await DB.prepare(`
|
|
1100
|
+
SELECT
|
|
1101
|
+
utm_source,
|
|
1102
|
+
credit_percentage
|
|
1103
|
+
FROM multi_touch_attribution
|
|
1104
|
+
WHERE conversion_id = ? AND attribution_model = ?
|
|
1105
|
+
ORDER BY credit_percentage DESC
|
|
1106
|
+
`).bind(conversionId, model).all();
|
|
1107
|
+
|
|
1108
|
+
// Agrupar por canal
|
|
1109
|
+
const channelCredits = {};
|
|
1110
|
+
for (const attr of attribution) {
|
|
1111
|
+
const channel = attr.utm_source;
|
|
1112
|
+
if (!channelCredits[channel]) {
|
|
1113
|
+
channelCredits[channel] = 0;
|
|
1114
|
+
}
|
|
1115
|
+
channelCredits[channel] += parseFloat(attr.credit_percentage);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
comparison[model] = channelCredits;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return new Response(JSON.stringify({
|
|
1122
|
+
conversion_id,
|
|
1123
|
+
comparison,
|
|
1124
|
+
days,
|
|
1125
|
+
timestamp: new Date().toISOString()
|
|
1126
|
+
}), {
|
|
1127
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1128
|
+
status: 200
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// Endpoint de performance de canal
|
|
1133
|
+
export async function getChannelPerformance(request, env) {
|
|
1134
|
+
const url = new URL(request.url);
|
|
1135
|
+
const model = url.searchParams.get('model') || ATTRIBUTION_CONFIG.default_model;
|
|
1136
|
+
const days = parseInt(url.searchParams.get('days') || '30');
|
|
1137
|
+
const groupBy = url.searchParams.get('group_by') || 'source'; // 'source' ou 'campaign'
|
|
1138
|
+
|
|
1139
|
+
const performance = await DB.prepare(`
|
|
1140
|
+
SELECT
|
|
1141
|
+
${groupBy === 'source' ? 'utm_source' : 'utm_campaign'} as group_by,
|
|
1142
|
+
SUM(total_attribution) as total_attribution,
|
|
1143
|
+
SUM(total_conversions) as total_conversions,
|
|
1144
|
+
SUM(total_value) as total_value,
|
|
1145
|
+
AVG(avg_conversion_value) as avg_conversion_value,
|
|
1146
|
+
AVG(total_attribution) as avg_attribution
|
|
1147
|
+
FROM channel_performance
|
|
1148
|
+
WHERE attribution_model = ?
|
|
1149
|
+
AND date >= date('now', '-${days} days')
|
|
1150
|
+
GROUP BY ${groupBy === 'source' ? 'utm_source' : 'utm_campaign'}
|
|
1151
|
+
ORDER BY total_value DESC
|
|
1152
|
+
`).bind(model).all();
|
|
1153
|
+
|
|
1154
|
+
return new Response(JSON.stringify({
|
|
1155
|
+
model,
|
|
1156
|
+
days,
|
|
1157
|
+
group_by,
|
|
1158
|
+
performance
|
|
1159
|
+
}), {
|
|
1160
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1161
|
+
status: 200
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
---
|
|
1167
|
+
|
|
1168
|
+
## šÆ FORMATO DE SAĆDA
|
|
1169
|
+
|
|
1170
|
+
### DELIVERABLE 1: `attribution-engine.js`
|
|
1171
|
+
|
|
1172
|
+
```javascript
|
|
1173
|
+
// attribution-engine.js - Engine de atribuição multi-touch
|
|
1174
|
+
export {
|
|
1175
|
+
captureTouchpoint,
|
|
1176
|
+
calculateMultiTouchAttribution,
|
|
1177
|
+
sendPurchaseWithAttribution,
|
|
1178
|
+
lastClickAttribution,
|
|
1179
|
+
firstClickAttribution,
|
|
1180
|
+
linearAttribution,
|
|
1181
|
+
timeDecayAttribution,
|
|
1182
|
+
uShapeAttribution,
|
|
1183
|
+
wShapeAttribution,
|
|
1184
|
+
dataDrivenAttribution
|
|
1185
|
+
};
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
### DELIVERABLE 2: `attribution-schema.sql`
|
|
1189
|
+
|
|
1190
|
+
```sql
|
|
1191
|
+
-- attribution-schema.sql - Schema D1 para atribuição multi-touch
|
|
1192
|
+
-- Tabela de jornada
|
|
1193
|
+
CREATE TABLE IF NOT EXISTS user_journeys (...);
|
|
1194
|
+
|
|
1195
|
+
-- Tabela de atribuição
|
|
1196
|
+
CREATE TABLE IF NOT EXISTS multi_touch_attribution (...);
|
|
1197
|
+
|
|
1198
|
+
-- Tabela de performance de canal
|
|
1199
|
+
CREATE TABLE IF NOT EXISTS channel_performance (...);
|
|
1200
|
+
|
|
1201
|
+
-- Ćndices
|
|
1202
|
+
CREATE INDEX IF NOT EXISTS idx_journey_user ON user_journeys(user_id);
|
|
1203
|
+
CREATE INDEX IF NOT EXISTS idx_journey_email ON user_journeys(email);
|
|
1204
|
+
CREATE INDEX IF NOT EXISTS idx_journey_conversion ON user_journeys(conversion_id);
|
|
1205
|
+
CREATE INDEX IF NOT EXISTS idx_attribution_conversion ON multi_touch_attribution(conversion_id);
|
|
1206
|
+
CREATE INDEX IF NOT EXISTS idx_channel_source ON channel_performance(utm_source);
|
|
1207
|
+
CREATE INDEX IF NOT EXISTS idx_channel_campaign ON channel_performance(utm_campaign);
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
### DELIVERABLE 3: `attribution-config.js`
|
|
1211
|
+
|
|
1212
|
+
```javascript
|
|
1213
|
+
// attribution-config.js - Configuração de modelos de atribuição
|
|
1214
|
+
export const ATTRIBUTION_CONFIG = {
|
|
1215
|
+
default_model: 'U_SHAPE',
|
|
1216
|
+
available_models: [
|
|
1217
|
+
'LAST_CLICK',
|
|
1218
|
+
'FIRST_CLICK',
|
|
1219
|
+
'LINEAR',
|
|
1220
|
+
'TIME_DECAY',
|
|
1221
|
+
'U_SHAPE',
|
|
1222
|
+
'W_SHAPE',
|
|
1223
|
+
'DATA_DRIVEN'
|
|
1224
|
+
],
|
|
1225
|
+
attribution_window: {
|
|
1226
|
+
days: 30,
|
|
1227
|
+
touchpoints_limit: 50,
|
|
1228
|
+
min_touchpoints: 2,
|
|
1229
|
+
max_touchpoints: 20
|
|
1230
|
+
},
|
|
1231
|
+
conversion_events: [
|
|
1232
|
+
'Lead',
|
|
1233
|
+
'Purchase',
|
|
1234
|
+
'CompleteRegistration',
|
|
1235
|
+
'AddPaymentInfo',
|
|
1236
|
+
'SubmitApplication'
|
|
1237
|
+
],
|
|
1238
|
+
device_weights: {
|
|
1239
|
+
desktop: 1.0,
|
|
1240
|
+
mobile: 1.2,
|
|
1241
|
+
tablet: 1.1
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
export default ATTRIBUTION_CONFIG;
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
---
|
|
1249
|
+
|
|
1250
|
+
## š CHECKLIST DE IMPLEMENTAĆĆO
|
|
1251
|
+
|
|
1252
|
+
### Engine de Atribuição
|
|
1253
|
+
|
|
1254
|
+
- [ ] Ćltimo clique (Last Click) implementado
|
|
1255
|
+
- [ ] Primeiro clique (First Click) implementado
|
|
1256
|
+
- [ ] Linear implementado
|
|
1257
|
+
- [ ] Time Decay implementado
|
|
1258
|
+
- [ ] U-Shape implementado
|
|
1259
|
+
- [ ] W-Shape implementado
|
|
1260
|
+
- [ ] Data-Driven implementado
|
|
1261
|
+
- [ ] Schema D1 criado (user_journeys)
|
|
1262
|
+
- [ ] Schema D1 criado (multi_touch_attribution)
|
|
1263
|
+
- [ ] Schema D1 criado (channel_performance)
|
|
1264
|
+
- [ ] Ćndices D1 criados
|
|
1265
|
+
|
|
1266
|
+
### Integração com Worker
|
|
1267
|
+
|
|
1268
|
+
- [ ] Captura de jornada implementada
|
|
1269
|
+
- [ ] CÔlculo de posição implementado
|
|
1270
|
+
- [ ] Agendamento via Queue implementado
|
|
1271
|
+
- [ ] Envio para Meta CAPI com atribuição implementado
|
|
1272
|
+
- [ ] Envio para TikTok Events API com atribuição implementado
|
|
1273
|
+
- [ ] Envio para GA4 Measurement Protocol com atribuição implementado
|
|
1274
|
+
- [ ] Atualização de performance de canal implementada
|
|
1275
|
+
|
|
1276
|
+
### APIs
|
|
1277
|
+
|
|
1278
|
+
- [ ] GET /api/attribution/conversion/{id} implementado
|
|
1279
|
+
- [ ] GET /api/attribution/compare/{id} implementado
|
|
1280
|
+
- [ ] GET /api/attribution/performance implementado
|
|
1281
|
+
- [ ] GET /api/attribution/journey/{email} implementado
|
|
1282
|
+
|
|
1283
|
+
### Configuração
|
|
1284
|
+
|
|
1285
|
+
- [ ] Configuração de modelos implementada
|
|
1286
|
+
- [ ] Janela de atribuição configurÔvel
|
|
1287
|
+
- [ ] Pesos de dispositivo configurƔveis
|
|
1288
|
+
- [ ] Ponderação por posição configurÔvel
|
|
1289
|
+
|
|
1290
|
+
---
|
|
1291
|
+
|
|
1292
|
+
## šÆ BENEFĆCIOS ESPERADOS
|
|
1293
|
+
|
|
1294
|
+
1. **ROI Real por Canal** ā Entender verdadeiramente quais canais geram conversƵes
|
|
1295
|
+
2. **Jornada do Cliente** ā Visualizar caminho completo do usuĆ”rio
|
|
1296
|
+
3. **Modelos FlexĆveis** ā Escolher entre 7+ modelos de atribuição
|
|
1297
|
+
4. **Atribuição em Tempo Real** ā Calcular imediatamente após conversĆ£o
|
|
1298
|
+
5. **Dashboard de Performance** ā Comparar modelos e ver impacto
|
|
1299
|
+
6. **Otimização de Budget** ā Mover budget para canais que realmente funcionam
|
|
1300
|
+
7. **Data-Driven** ā Algoritmo aprende com seus dados históricos
|
|
1301
|
+
|
|
1302
|
+
---
|
|
1303
|
+
|
|
1304
|
+
> šÆ **Sua Função:** Calcular atribuição multi-touch profissional com 7+ modelos (Last Click, First Click, Linear, Time Decay, U-Shape, W-Shape, Data-Driven), persistindo jornada completa no D1, calculando crĆ©dito distribuĆdo entre touchpoints, enviando para APIs com atribuição calculada e gerando dashboard de performance em tempo real.
|