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.
Files changed (128) hide show
  1. package/README.md +367 -0
  2. package/bin/cdp-edge.js +61 -0
  3. package/contracts/api-versions.json +368 -0
  4. package/dist/commands/analyze.js +52 -0
  5. package/dist/commands/infra.js +54 -0
  6. package/dist/commands/install.js +168 -0
  7. package/dist/commands/server.js +174 -0
  8. package/dist/commands/setup.js +123 -0
  9. package/dist/commands/validate.js +84 -0
  10. package/dist/index.js +12 -0
  11. package/docs/CI-CD-SETUP.md +217 -0
  12. package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
  13. package/docs/events-reference.md +359 -0
  14. package/docs/installation.md +155 -0
  15. package/docs/quick-start.md +185 -0
  16. package/docs/sdk-reference.md +371 -0
  17. package/docs/whatsapp-ctwa.md +209 -0
  18. package/extracted-skill/tracking-events-generator/INDEX.md +94 -0
  19. package/extracted-skill/tracking-events-generator/INSTALACAO-CDPEDGE.md +58 -0
  20. package/extracted-skill/tracking-events-generator/INTEGRACAO-COMPLETA.md +594 -0
  21. package/extracted-skill/tracking-events-generator/MELHORIAS-IMPLEMENTADAS.md +412 -0
  22. package/extracted-skill/tracking-events-generator/Premium-Tracking-Intelligence-Resumo.md +333 -0
  23. package/extracted-skill/tracking-events-generator/SKILL.md +257 -0
  24. package/extracted-skill/tracking-events-generator/advanced-matching.js +364 -0
  25. package/extracted-skill/tracking-events-generator/agents/ab-testing-agent.md +54 -0
  26. package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +1304 -0
  27. package/extracted-skill/tracking-events-generator/agents/bing-agent.md +76 -0
  28. package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +264 -0
  29. package/extracted-skill/tracking-events-generator/agents/code-guardian-agent.md +149 -0
  30. package/extracted-skill/tracking-events-generator/agents/compliance-agent.md +2077 -0
  31. package/extracted-skill/tracking-events-generator/agents/crm-integration-agent.md +1419 -0
  32. package/extracted-skill/tracking-events-generator/agents/dashboard-agent.md +456 -0
  33. package/extracted-skill/tracking-events-generator/agents/database-agent.md +667 -0
  34. package/extracted-skill/tracking-events-generator/agents/debug-agent.md +1455 -0
  35. package/extracted-skill/tracking-events-generator/agents/domain-setup-agent.md +224 -0
  36. package/extracted-skill/tracking-events-generator/agents/email-agent.md +61 -0
  37. package/extracted-skill/tracking-events-generator/agents/fingerprint-agent.md +52 -0
  38. package/extracted-skill/tracking-events-generator/agents/google-agent.md +109 -0
  39. package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +365 -0
  40. package/extracted-skill/tracking-events-generator/agents/intelligence-scheduling.md +643 -0
  41. package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +62 -0
  42. package/extracted-skill/tracking-events-generator/agents/localization-agent.md +55 -0
  43. package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +59 -0
  44. package/extracted-skill/tracking-events-generator/agents/master-feedback-loop.md +900 -0
  45. package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +1922 -0
  46. package/extracted-skill/tracking-events-generator/agents/memory-agent.json +109 -0
  47. package/extracted-skill/tracking-events-generator/agents/memory-agent.md +703 -0
  48. package/extracted-skill/tracking-events-generator/agents/meta-agent.md +110 -0
  49. package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +255 -0
  50. package/extracted-skill/tracking-events-generator/agents/performance-agent.md +1157 -0
  51. package/extracted-skill/tracking-events-generator/agents/performance-optimization-agent.md +1432 -0
  52. package/extracted-skill/tracking-events-generator/agents/pinterest-agent.md +310 -0
  53. package/extracted-skill/tracking-events-generator/agents/premium-tracking-intelligence-agent.md +849 -0
  54. package/extracted-skill/tracking-events-generator/agents/r2-setup-agent.md +250 -0
  55. package/extracted-skill/tracking-events-generator/agents/reddit-agent.md +313 -0
  56. package/extracted-skill/tracking-events-generator/agents/security-enterprise-agent.md +1752 -0
  57. package/extracted-skill/tracking-events-generator/agents/server-tracking.md +1188 -0
  58. package/extracted-skill/tracking-events-generator/agents/spotify-agent.md +383 -0
  59. package/extracted-skill/tracking-events-generator/agents/tiktok-agent.md +111 -0
  60. package/extracted-skill/tracking-events-generator/agents/tracking-plan-agent.md +364 -0
  61. package/extracted-skill/tracking-events-generator/agents/validator-agent.md +267 -0
  62. package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +69 -0
  63. package/extracted-skill/tracking-events-generator/agents/whatsapp-agent.md +76 -0
  64. package/extracted-skill/tracking-events-generator/agents/whatsapp-ctwa-setup-agent.md +699 -0
  65. package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +422 -0
  66. package/extracted-skill/tracking-events-generator/anti-blocking.js +285 -0
  67. package/extracted-skill/tracking-events-generator/cdpTrack.js +641 -0
  68. package/extracted-skill/tracking-events-generator/contracts/api-versions.json +368 -0
  69. package/extracted-skill/tracking-events-generator/docs/guia-cloudflare-iniciante.md +107 -0
  70. package/extracted-skill/tracking-events-generator/engagement-scoring.js +226 -0
  71. package/extracted-skill/tracking-events-generator/evals/evals.json +235 -0
  72. package/extracted-skill/tracking-events-generator/integration-test.js +497 -0
  73. package/extracted-skill/tracking-events-generator/knowledge-base.md +2894 -0
  74. package/extracted-skill/tracking-events-generator/micro-events.js +992 -0
  75. package/extracted-skill/tracking-events-generator/models/captura-de-lead.md +78 -0
  76. package/extracted-skill/tracking-events-generator/models/captura-lead-evento-externo.md +99 -0
  77. package/extracted-skill/tracking-events-generator/models/checkout-proprio.md +111 -0
  78. package/extracted-skill/tracking-events-generator/models/multi-step-checkout.md +672 -0
  79. package/extracted-skill/tracking-events-generator/models/pagina-obrigado.md +55 -0
  80. package/extracted-skill/tracking-events-generator/models/pinterest/conversions-api-template.js +144 -0
  81. package/extracted-skill/tracking-events-generator/models/pinterest/event-mappings.json +48 -0
  82. package/extracted-skill/tracking-events-generator/models/pinterest/tag-template.js +28 -0
  83. package/extracted-skill/tracking-events-generator/models/quiz-funnel.md +68 -0
  84. package/extracted-skill/tracking-events-generator/models/reddit/conversions-api-template.js +205 -0
  85. package/extracted-skill/tracking-events-generator/models/reddit/event-mappings.json +56 -0
  86. package/extracted-skill/tracking-events-generator/models/reddit/pixel-template.js +19 -0
  87. package/extracted-skill/tracking-events-generator/models/scenarios/behavior-engine.js +425 -0
  88. package/extracted-skill/tracking-events-generator/models/scenarios/real-estate-logic.md +50 -0
  89. package/extracted-skill/tracking-events-generator/models/scenarios/sales-page-logic.md +50 -0
  90. package/extracted-skill/tracking-events-generator/models/trafego-direto.md +582 -0
  91. package/extracted-skill/tracking-events-generator/models/webinar-registration.md +63 -0
  92. package/extracted-skill/tracking-events-generator/tracking.config.js +46 -0
  93. package/extracted-skill/tracking-events-generator/walkthrough.md +26 -0
  94. package/package.json +75 -0
  95. package/server-edge-tracker/INSTALAR.md +328 -0
  96. package/server-edge-tracker/migrate-new-db.sql +137 -0
  97. package/server-edge-tracker/migrate-v2.sql +16 -0
  98. package/server-edge-tracker/migrate-v3.sql +6 -0
  99. package/server-edge-tracker/migrate-v4.sql +18 -0
  100. package/server-edge-tracker/migrate-v5.sql +17 -0
  101. package/server-edge-tracker/migrate-v6.sql +24 -0
  102. package/server-edge-tracker/migrate.sql +111 -0
  103. package/server-edge-tracker/schema.sql +265 -0
  104. package/server-edge-tracker/worker.js +2574 -0
  105. package/server-edge-tracker/wrangler.toml +85 -0
  106. package/templates/afiliado-sem-landing.md +312 -0
  107. package/templates/captura-de-lead.md +78 -0
  108. package/templates/captura-lead-evento-externo.md +99 -0
  109. package/templates/checkout-proprio.md +111 -0
  110. package/templates/install/.claude/commands/cdp.md +1 -0
  111. package/templates/install/CLAUDE.md +65 -0
  112. package/templates/linkedin/tag-template.js +46 -0
  113. package/templates/multi-step-checkout.md +673 -0
  114. package/templates/pagina-obrigado.md +55 -0
  115. package/templates/pinterest/conversions-api-template.js +144 -0
  116. package/templates/pinterest/event-mappings.json +48 -0
  117. package/templates/pinterest/tag-template.js +28 -0
  118. package/templates/quiz-funnel.md +68 -0
  119. package/templates/reddit/conversions-api-template.js +205 -0
  120. package/templates/reddit/event-mappings.json +56 -0
  121. package/templates/reddit/pixel-template.js +46 -0
  122. package/templates/scenarios/behavior-engine.js +402 -0
  123. package/templates/scenarios/real-estate-logic.md +50 -0
  124. package/templates/scenarios/sales-page-logic.md +50 -0
  125. package/templates/spotify/pixel-template.js +46 -0
  126. package/templates/trafego-direto.md +582 -0
  127. package/templates/vsl-page.md +292 -0
  128. 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.