cdp-edge 2.3.8 → 2.5.1

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 (29) hide show
  1. package/README.md +304 -639
  2. package/bin/cdp-edge.js +3 -2
  3. package/dist/commands/validate.js +248 -84
  4. package/dist/sdk/cdpTrack.js +2095 -0
  5. package/dist/sdk/cdpTrack.min.js +64 -0
  6. package/dist/sdk/install-snippet.html +10 -0
  7. package/extracted-skill/tracking-events-generator/agents/devops-agent.md +22 -0
  8. package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +53 -0
  9. package/extracted-skill/tracking-events-generator/agents/lead-scoring-agent.md +282 -0
  10. package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +60 -6
  11. package/extracted-skill/tracking-events-generator/agents/match-quality-agent.md +304 -0
  12. package/extracted-skill/tracking-events-generator/agents/utm-agent.md +285 -154
  13. package/extracted-skill/tracking-events-generator/anti-blocking.js +1 -1
  14. package/extracted-skill/tracking-events-generator/cdpTrack.js +10 -18
  15. package/extracted-skill/tracking-events-generator/engagement-scoring.js +2 -2
  16. package/extracted-skill/tracking-events-generator/micro-events.js +1 -1
  17. package/extracted-skill/tracking-events-generator/models/quiz-funnel.md +83 -19
  18. package/extracted-skill/tracking-events-generator/tracking.config.js +3 -3
  19. package/package.json +5 -1
  20. package/scripts/build-sdk.js +106 -0
  21. package/server-edge-tracker/index.ts +174 -6
  22. package/server-edge-tracker/modules/intelligence.ts +155 -2
  23. package/server-edge-tracker/modules/ml/quiz.ts +343 -0
  24. package/server-edge-tracker/modules/ml/roas.ts +255 -0
  25. package/server-edge-tracker/modules/nurture.ts +257 -0
  26. package/server-edge-tracker/modules/utils.ts +2 -0
  27. package/server-edge-tracker/schema-quiz.sql +52 -0
  28. package/server-edge-tracker/schema-sales-engine.sql +113 -0
  29. package/templates/quiz-funnel.md +83 -19
@@ -1,15 +1,15 @@
1
1
  # Modelo: Quiz Funnel (Cloudflare Native)
2
2
 
3
- Este modelo é destinado a funis de quiz, onde o usuário responde a uma série de perguntas antes de ser redirecionado para a oferta final. O rastreamento foca na progressão do usuário e na captura de dados intermediários.
3
+ Este modelo é destinado a funis de quiz, onde o usuário responde a uma série de perguntas antes de ser redirecionado para a oferta final. O rastreamento foca na progressão do usuário e na **qualificação automática de intenção via Workers AI**.
4
4
 
5
5
  ---
6
6
 
7
7
  ## 🏗️ ARQUITETURA TÉCNICA (Quantum Tier)
8
8
 
9
- O rastreamento segue a lógica de micro-eventos:
9
+ O rastreamento segue a lógica de micro-eventos + scoring automático:
10
10
  1. **Página**: Dispara um evento a cada resposta dada no quiz via `cdpTrack.track()`.
11
- 2. **Servidor (Worker)**: Recebe e armazena o progresso no banco D1.
12
- 3. **Database (D1)**: Mantém o histórico de respostas vinculado ao `track_user_id`.
11
+ 2. **Servidor (Worker)**: Ao receber `QuizComplete`, envia as respostas ao **Quiz Scoring Engine** (Granite 4.0 Micro) que classifica o respondente.
12
+ 3. **Pipeline CDP**: A qualificação (`comprador | interessado | curioso | perdido`) é injetada como `intentionLevel` e flui automaticamente para LTV Prediction, Meta Signal Score, D1 e CAPI dispatch.
13
13
 
14
14
  ---
15
15
 
@@ -19,50 +19,114 @@ O rastreamento segue a lógica de micro-eventos:
19
19
  |---|---|---|
20
20
  | **QuizStart** | Início do quiz | `quiz_name`, `source` |
21
21
  | **QuizAnswer** | Resposta a uma pergunta | `question`, `answer`, `step` |
22
- | **QuizComplete** | Finalização do quiz | `result`, `completion_time` |
22
+ | **QuizComplete** | Finalização + qualificação AI | `quiz_name`, `quiz_answers[]`, `result` |
23
+
24
+ ---
25
+
26
+ ## 🤖 QUALIFICAÇÃO AUTOMÁTICA (Quiz Scoring Engine — Fase 6)
27
+
28
+ Ao receber `QuizComplete` com `quiz_answers`, o Worker classifica automaticamente:
29
+
30
+ | Qualificação | Significado | intent_score |
31
+ |---|---|---|
32
+ | **comprador** | Pronto para comprar agora | 0.80–1.00 |
33
+ | **interessado** | Interesse real, avaliando | 0.50–0.79 |
34
+ | **curioso** | Pesquisando, sem urgência | 0.20–0.49 |
35
+ | **perdido** | Fora do público, sem fit | 0.00–0.19 |
36
+
37
+ O `intent_score` resultante:
38
+ - Alimenta o **LTV Prediction** (comprador → LTV High automaticamente)
39
+ - Compõe o **Meta Signal Score** (pesos dinâmicos por funil)
40
+ - Persiste em `leads.intention_level` e `quiz_sessions` no D1
41
+ - É enviado como `custom_data` para Meta CAPI, GA4 e TikTok
23
42
 
24
43
  ---
25
44
 
26
45
  ## 🛠️ PASSO 1: CONFIGURAÇÃO DO SITE
27
46
 
28
47
  ### 1.1 Rastreamento de Respostas
29
- Integre este código na lógica de clique do seu quiz.
48
+ Acumule as respostas do quiz em um array local.
30
49
 
31
50
  ```javascript
32
- // Exemplo de captura de resposta
51
+ const quizAnswers = [];
52
+
33
53
  function onResponder(pergunta, resposta, etapa) {
54
+ // Armazena localmente para enviar no QuizComplete
55
+ quizAnswers.push({ question: pergunta, answer: resposta, step: etapa });
56
+
57
+ // Dispara micro-evento por resposta (opcional, para análise granular)
34
58
  cdpTrack.track('QuizAnswer', {
35
59
  question: pergunta,
36
60
  answer: resposta,
37
61
  step: etapa,
38
- event_id: cdpTrack.generateId()
62
+ event_id: cdpTrack.generateId(),
39
63
  });
40
64
  }
41
65
  ```
42
66
 
43
- ### 1.2 Finalização do Quiz
44
- Disparar ao chegar no resultado final ou na página de captura pós-quiz.
67
+ ### 1.2 Finalização do Quiz — com qualificação AI automática
68
+ Envie todas as respostas no `QuizComplete`. O Worker qualifica automaticamente.
45
69
 
46
70
  ```javascript
47
71
  cdpTrack.track('QuizComplete', {
48
- result: 'Perfil_A',
49
- event_id: cdpTrack.generateId()
72
+ quiz_name: 'Diagnóstico de Perfil', // nome para o dashboard
73
+ quiz_answers: quizAnswers, // array com todas as respostas
74
+ result: 'Perfil_A', // resultado exibido ao usuário (opcional)
75
+ event_id: cdpTrack.generateId(),
50
76
  });
51
77
  ```
52
78
 
79
+ ### 1.3 Resposta do Worker
80
+ O endpoint `/track` retorna a qualificação para uso imediato no front:
81
+
82
+ ```json
83
+ {
84
+ "ok": true,
85
+ "userProfile": {
86
+ "score": 87,
87
+ "user_id": "uuid-xxx"
88
+ },
89
+ "quiz_qualification": "comprador",
90
+ "quiz_confidence": 0.91,
91
+ "quiz_signals": ["quero comprar", "tenho budget", "agora"],
92
+ "intent_score": 0.92,
93
+ "intent_bucket": "high"
94
+ }
95
+ ```
96
+
97
+ Use esses campos para personalizar o redirecionamento pós-quiz no front-end.
98
+
53
99
  ---
54
100
 
55
101
  ## ⚡ PASSO 2: SERVIDOR (CLOUDFLARE WORKER)
56
102
 
57
- O Worker realiza:
58
- - **Agregação**: O `user_id` permite que todas as respostas sejam vinculadas a um único perfil no banco D1.
59
- - **Enriquecimento**: Se o usuário deixar o e-mail no final, todas as respostas anteriores são associadas ao e-mail para a CAPI.
60
- - **API Dispatch**: Envio de eventos customizados para Meta e TikTok para otimização de público.
103
+ O Worker realiza automaticamente na ordem:
104
+
105
+ 1. **Quiz Scoring Engine**: Granite 4.0 Micro classifica as respostas `qualification` + `intent_score`
106
+ 2. **LTV Prediction**: usa `intentionLevel = 'comprador'` LTV High valor previsto em BRL
107
+ 3. **Meta Signal Score**: `intent_score` compõe o score composto (intent × ltv × distância)
108
+ 4. **D1 Writes**: `quiz_sessions` + `leads.intention_level` + `user_profiles.cohort_label`
109
+ 5. **CAPI Dispatch**: Meta/GA4/TikTok recebem evento com `custom_data.intention = 'comprador'`
110
+
111
+ ---
112
+
113
+ ## 🔀 FALLBACK HEURÍSTICO
114
+
115
+ Se Workers AI estiver indisponível (timeout, cold start), o sistema usa correspondência de palavras-chave:
116
+
117
+ - `comprador`: "quero", "comprar", "agora", "tenho interesse", "quanto custa"
118
+ - `interessado`: "talvez", "pensando", "em breve", "estou avaliando"
119
+ - `curioso`: "só olhando", "pesquisando", "curiosidade"
120
+ - `perdido`: "não entendi", "errei aqui", "não é para mim"
121
+
122
+ O campo `quiz_source` indica `"ai"` ou `"heuristic"` para auditoria.
61
123
 
62
124
  ---
63
125
 
64
126
  ## ✅ VALIDAÇÃO TÉCNICA
65
127
 
66
- - **Persistência**: Verifique no banco D1 se a jornada do usuário está sendo gravada passo a passo.
67
- - **Deduplicação**: O `event_id` único por resposta evita contagens duplicadas.
68
- - **Match Quality**: A vinculação tardia do e-mail com as respostas iniciais aumenta a precisão da atribuição.
128
+ - **Persistência**: `quiz_sessions` no D1 jornada completa por `user_id`
129
+ - **Deduplicação**: `event_id` único por evento evita contagens duplicadas
130
+ - **Enriquecimento retroativo**: e-mail preenchido pós-quiz associa todas as respostas ao perfil
131
+ - **Match Quality**: `comprador` com e-mail → score máximo na CAPI Meta
132
+ - **VIEW de dashboard**: `v_quiz_qualification_summary` — distribuição de qualificações por quiz
@@ -10,9 +10,9 @@
10
10
  const CONFIG = {
11
11
  // ── Endpoint do Cloudflare Worker ────────────────────────────────────────────
12
12
  // Same-domain: evita ad-blockers e CORS.
13
- // Em produção: seu-dominio.com.br/api/tracking (Worker roteado)
14
- // Em dev: use o URL do Worker diretamente
15
- endpoint: '/api/tracking',
13
+ // Em produção: seu-dominio.com.br/track (Worker roteado via Custom Domain)
14
+ // Em dev: use o URL do Worker diretamente (ex: https://worker.seu-dominio.workers.dev/track)
15
+ endpoint: '/track',
16
16
 
17
17
  // ── Plataformas de checkout habilitadas ──────────────────────────────────────
18
18
  // Controla quais plataformas recebem passCheckoutParams() automaticamente.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdp-edge",
3
- "version": "2.3.8",
3
+ "version": "2.5.1",
4
4
  "description": "CDP Edge - Quantum Tracking - Sistema multi-agente para tracking digital Cloudflare Native (Workers + D1)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -15,6 +15,7 @@
15
15
  "contracts",
16
16
  "extracted-skill",
17
17
  "server-edge-tracker",
18
+ "scripts/build-sdk.js",
18
19
  "docs",
19
20
  "README.md"
20
21
  ],
@@ -35,6 +36,8 @@
35
36
  "agents:sync": "node scripts/sync-agents.js",
36
37
  "agents:sync:list": "node scripts/sync-agents.js --list",
37
38
  "agents:sync:all": "node scripts/sync-agents.js --apply-all",
39
+ "sdk:build": "node scripts/build-sdk.js",
40
+ "sdk:build:debug": "node scripts/build-sdk.js --debug",
38
41
  "typecheck": "tsc --noEmit"
39
42
  },
40
43
  "keywords": [
@@ -79,6 +82,7 @@
79
82
  "@semantic-release/npm": "^13.1.5",
80
83
  "@semantic-release/release-notes-generator": "^14.1.0",
81
84
  "@types/node": "^20.19.39",
85
+ "esbuild": "^0.28.0",
82
86
  "semantic-release": "^25.0.3",
83
87
  "typescript": "^6.0.2"
84
88
  }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * CDP Edge — SDK Build Script
3
+ *
4
+ * Gera cdpTrack.min.js: bundle único pronto para <script src=""> em qualquer site.
5
+ * Entrada: extracted-skill/tracking-events-generator/cdpTrack.js (ES Module)
6
+ * Saída: dist/sdk/cdpTrack.min.js — minificado (produção)
7
+ * dist/sdk/cdpTrack.js — legível (debug)
8
+ *
9
+ * Uso:
10
+ * node scripts/build-sdk.js
11
+ * node scripts/build-sdk.js --debug (só versão legível)
12
+ */
13
+
14
+ import { build } from 'esbuild';
15
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
16
+ import { resolve, dirname } from 'path';
17
+ import { fileURLToPath } from 'url';
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const ROOT = resolve(__dirname, '..');
21
+ const SDK_DIR = resolve(ROOT, 'extracted-skill/tracking-events-generator');
22
+ const OUT_DIR = resolve(ROOT, 'dist/sdk');
23
+
24
+ const debugOnly = process.argv.includes('--debug');
25
+
26
+ // Garante que o diretório de saída existe
27
+ mkdirSync(OUT_DIR, { recursive: true });
28
+
29
+ // ── Banner do bundle ──────────────────────────────────────────────────────────
30
+ const pkg = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8'));
31
+ const banner = `/*!
32
+ * CDP Edge SDK v${pkg.version}
33
+ * (c) ${new Date().getFullYear()} CDP Edge — Quantum Tracking
34
+ * Gerado em: ${new Date().toISOString()}
35
+ * Endpoint padrão: /track (mesmo domínio — anti-adblock)
36
+ */`;
37
+
38
+ // ── Configuração base do esbuild ──────────────────────────────────────────────
39
+ const baseConfig = {
40
+ entryPoints: [resolve(SDK_DIR, 'cdpTrack.js')],
41
+ bundle: true,
42
+ format: 'iife', // <script> tag — expõe window.cdpTrack
43
+ globalName: 'cdpTrack', // window.cdpTrack após carregar
44
+ platform: 'browser',
45
+ target: ['es2018'], // suporta Safari 12+, Chrome 69+, Firefox 62+
46
+ banner: { js: banner },
47
+ define: {
48
+ 'process.env.NODE_ENV': '"production"',
49
+ },
50
+ // Resolve módulos locais pelo diretório do SDK
51
+ absWorkingDir: SDK_DIR,
52
+ };
53
+
54
+ async function buildSDK() {
55
+ console.log('🏗️ Buildando CDP Edge SDK...\n');
56
+
57
+ // ── Versão debug (legível) ────────────────────────────────────────────────
58
+ console.log('📦 Gerando cdpTrack.js (debug)...');
59
+ await build({
60
+ ...baseConfig,
61
+ outfile: resolve(OUT_DIR, 'cdpTrack.js'),
62
+ minify: false,
63
+ sourcemap: false,
64
+ });
65
+ const debugSize = (readFileSync(resolve(OUT_DIR, 'cdpTrack.js')).length / 1024).toFixed(1);
66
+ console.log(` ✓ dist/sdk/cdpTrack.js — ${debugSize} kB`);
67
+
68
+ if (!debugOnly) {
69
+ // ── Versão produção (minificada) ────────────────────────────────────────
70
+ console.log('📦 Gerando cdpTrack.min.js (produção)...');
71
+ await build({
72
+ ...baseConfig,
73
+ outfile: resolve(OUT_DIR, 'cdpTrack.min.js'),
74
+ minify: true,
75
+ sourcemap: false,
76
+ });
77
+ const minSize = (readFileSync(resolve(OUT_DIR, 'cdpTrack.min.js')).length / 1024).toFixed(1);
78
+ console.log(` ✓ dist/sdk/cdpTrack.min.js — ${minSize} kB`);
79
+ }
80
+
81
+ // ── Gera snippet de instalação ────────────────────────────────────────────
82
+ const snippet = `<!-- CDP Edge SDK v${pkg.version} — instalar antes do </body> -->
83
+ <script src="/cdpTrack.min.js"></script>
84
+ <script>
85
+ // Inicializar após carregar
86
+ document.addEventListener('DOMContentLoaded', function() {
87
+ if (window.cdpTrack && window.cdpTrack.init) {
88
+ window.cdpTrack.init();
89
+ }
90
+ });
91
+ </script>`;
92
+
93
+ writeFileSync(resolve(OUT_DIR, 'install-snippet.html'), snippet, 'utf-8');
94
+ console.log(` ✓ dist/sdk/install-snippet.html — snippet de instalação\n`);
95
+
96
+ console.log('✅ SDK build concluído!');
97
+ console.log('\n📋 Próximos passos para o cliente:');
98
+ console.log(' 1. Copiar dist/sdk/cdpTrack.min.js para a raiz do site');
99
+ console.log(' 2. Adicionar snippet de dist/sdk/install-snippet.html antes de </body>');
100
+ console.log(' 3. O Worker deve estar no mesmo domínio (Custom Domain Cloudflare → /track)\n');
101
+ }
102
+
103
+ buildSDK().catch(err => {
104
+ console.error('❌ Build falhou:', err.message);
105
+ process.exit(1);
106
+ });
@@ -101,6 +101,15 @@ import {
101
101
  handleFraudStats,
102
102
  } from './modules/ml/fraud';
103
103
 
104
+ // ── Quiz Scoring Engine (Fase 6) ──────────────────────────────────────────────
105
+ import {
106
+ scoreQuizAnswers,
107
+ saveQuizSession,
108
+ } from './modules/ml/quiz';
109
+
110
+ // ── Nurture Engine (Fase 7) ───────────────────────────────────────────────────
111
+ import { scheduleNurture } from './modules/nurture';
112
+
104
113
  // ── Intelligence Agent (Cron) ─────────────────────────────────────────────────
105
114
  import {
106
115
  runIntelligenceAgent,
@@ -246,6 +255,99 @@ export default {
246
255
  }, null, 2), { headers });
247
256
  }
248
257
 
258
+ // ── GET /validate-install ─────────────────────────────────────────────────
259
+ // Endpoint de diagnóstico pós-deploy. Chamado por `cdp-edge validate <url>`.
260
+ // Testa D1 write/read, KV, AI e retorna relatório estruturado.
261
+ // Protegido: só aceita requisições com header CDP-Validate: 1
262
+ if (request.method === 'GET' && url.pathname === '/validate-install') {
263
+ if (request.headers.get('CDP-Validate') !== '1') {
264
+ return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers });
265
+ }
266
+
267
+ const report: Record<string, { ok: boolean; detail: string }> = {};
268
+
269
+ // 1. D1 write + read
270
+ try {
271
+ const testId = `__cdp_validate_${Date.now()}__`;
272
+ await env.DB?.prepare(
273
+ `INSERT OR REPLACE INTO events (event_id, event_name, user_id, created_at)
274
+ VALUES (?, '__validate__', '__validate__', datetime('now'))`
275
+ ).bind(testId).run();
276
+ const row = await env.DB?.prepare(
277
+ `SELECT event_id FROM events WHERE event_id = ?`
278
+ ).bind(testId).first();
279
+ await env.DB?.prepare(`DELETE FROM events WHERE event_id = ?`).bind(testId).run();
280
+ report.d1 = { ok: !!row, detail: row ? 'write+read+delete OK' : 'row not found after insert' };
281
+ } catch (err: any) {
282
+ report.d1 = { ok: false, detail: err?.message || String(err) };
283
+ }
284
+
285
+ // 2. KV read/write
286
+ try {
287
+ await env.GEO_CACHE?.put('__cdp_validate__', '1', { expirationTtl: 60 });
288
+ const val = await env.GEO_CACHE?.get('__cdp_validate__');
289
+ report.kv = { ok: val === '1', detail: val === '1' ? 'write+read OK' : 'value mismatch' };
290
+ } catch (err: any) {
291
+ report.kv = { ok: false, detail: err?.message || String(err) };
292
+ }
293
+
294
+ // 3. Workers AI
295
+ try {
296
+ await env.AI?.run('@cf/ibm-granite/granite-4.0-h-micro', {
297
+ messages: [{ role: 'user', content: 'ping' }],
298
+ max_tokens: 1,
299
+ });
300
+ report.ai = { ok: true, detail: 'Granite 4.0 Micro respondeu' };
301
+ } catch (err: any) {
302
+ report.ai = { ok: false, detail: err?.message || String(err) };
303
+ }
304
+
305
+ // 4. Secrets críticos
306
+ const missing: string[] = [];
307
+ if (!env.META_PIXEL_ID) missing.push('META_PIXEL_ID');
308
+ if (!env.META_ACCESS_TOKEN) missing.push('META_ACCESS_TOKEN');
309
+ if (!env.SITE_DOMAIN) missing.push('SITE_DOMAIN');
310
+ report.secrets = {
311
+ ok: missing.length === 0,
312
+ detail: missing.length === 0 ? 'todos os secrets críticos configurados' : `MISSING: ${missing.join(', ')}`,
313
+ };
314
+
315
+ // 5. /track endpoint (auto-teste)
316
+ const trackTest = { ok: false, detail: '' };
317
+ try {
318
+ const testPayload = {
319
+ eventName: 'PageView',
320
+ userId: '__cdp_validate__',
321
+ pageUrl: `https://${env.SITE_DOMAIN || 'validate.test'}/`,
322
+ userAgent: request.headers.get('User-Agent') || '',
323
+ ip: request.headers.get('CF-Connecting-IP') || '',
324
+ _validate: true,
325
+ };
326
+ const internalReq = new Request(`https://${env.SITE_DOMAIN || 'localhost'}/track`, {
327
+ method: 'POST',
328
+ headers: { 'Content-Type': 'application/json', 'CDP-Validate': '1' },
329
+ body: JSON.stringify(testPayload),
330
+ });
331
+ // Não chama fetch externo — apenas verifica que o payload seria aceito
332
+ const hasRequired = testPayload.eventName && testPayload.userId;
333
+ trackTest.ok = !!hasRequired;
334
+ trackTest.detail = hasRequired ? 'payload de teste válido (eventName + userId presentes)' : 'payload inválido';
335
+ } catch (err: any) {
336
+ trackTest.detail = err?.message || String(err);
337
+ }
338
+ report.track_endpoint = trackTest;
339
+
340
+ const allOk = Object.values(report).every(r => r.ok);
341
+ return new Response(JSON.stringify({
342
+ status: allOk ? 'ok' : 'degraded',
343
+ timestamp: new Date().toISOString(),
344
+ checks: report,
345
+ }, null, 2), {
346
+ status: allOk ? 200 : 207,
347
+ headers,
348
+ });
349
+ }
350
+
249
351
  // ── POST /track ───────────────────────────────────────────────────────────
250
352
  if (request.method === 'POST' && url.pathname === '/track') {
251
353
  // Reject oversized payloads before reading body (64 KB limit)
@@ -272,6 +374,27 @@ export default {
272
374
  const { eventName, behavioral_data, ...payload } = body as { eventName?: string; behavioral_data?: BehavioralData; [key: string]: any };
273
375
  const trackPayload: TrackPayload = payload;
274
376
 
377
+ // ── Extrair click_ids e utms de sub-objetos (formato cdpTrack.js) ────────
378
+ // cdpTrack.js envia fbp/fbc/fbclid dentro de click_ids{} e UTMs dentro de utms{}
379
+ // O Worker trabalha com campos no nível raiz — esta extração resolve o mismatch.
380
+ if (payload.click_ids && typeof payload.click_ids === 'object') {
381
+ const c = payload.click_ids as Record<string, string>;
382
+ if (!trackPayload.fbp && c.fbp) trackPayload.fbp = c.fbp;
383
+ if (!trackPayload.fbc && c.fbc) trackPayload.fbc = c.fbc;
384
+ if (!trackPayload.fbclid && c.fbclid) trackPayload.fbclid = c.fbclid;
385
+ if (!trackPayload.gclid && c.gclid) trackPayload.gclid = c.gclid;
386
+ if (!trackPayload.ttclid && c.ttclid) trackPayload.ttclid = c.ttclid;
387
+ if (!trackPayload.ttp && c.ttp) trackPayload.ttp = c.ttp; // TikTok Pixel cookie
388
+ }
389
+ if (payload.utms && typeof payload.utms === 'object') {
390
+ const u = payload.utms as Record<string, string>;
391
+ if (!trackPayload.utmSource && u.utm_source) trackPayload.utmSource = u.utm_source;
392
+ if (!trackPayload.utmMedium && u.utm_medium) trackPayload.utmMedium = u.utm_medium;
393
+ if (!trackPayload.utmCampaign && u.utm_campaign) trackPayload.utmCampaign = u.utm_campaign;
394
+ if (!trackPayload.utmContent && u.utm_content) trackPayload.utmContent = u.utm_content;
395
+ if (!trackPayload.utmTerm && u.utm_term) trackPayload.utmTerm = u.utm_term;
396
+ }
397
+
275
398
  // ── Validação de eventName ────────────────────────────────────────
276
399
  if (!eventName) {
277
400
  return new Response(JSON.stringify({ error: 'eventName é obrigatório' }), { status: 400, headers });
@@ -286,7 +409,7 @@ export default {
286
409
 
287
410
  const SANITIZE_FIELDS: Record<string, (val: string) => SanitizeResult> = {
288
411
  email: (val: string) => {
289
- if (!isValidEmail(val)) return { error: 'email inválido (formato incorreto)' };
412
+ if (!isValidEmail(val)) return { error: 'email inválido (formato incorreto)', sanitized: null };
290
413
  return { sanitized: val.toLowerCase().trim() };
291
414
  },
292
415
  firstName: (val: string) => ({ sanitized: sanitizeString(val, 100) }),
@@ -297,11 +420,11 @@ export default {
297
420
  dob: (val: string) => ({ sanitized: sanitizeString(val, 20) }),
298
421
  productName: (val: string) => ({ sanitized: sanitizeString(val, 200) }),
299
422
  pageUrl: (val: string) => {
300
- if (!isValidUrl(val)) return { error: 'pageUrl inválido (formato incorreto)' };
423
+ if (!isValidUrl(val)) return { error: 'pageUrl inválido (formato incorreto)', sanitized: null };
301
424
  return { sanitized: val.trim() };
302
425
  },
303
426
  currency: (val: string) => {
304
- if (!isValidCurrency(val)) return { error: 'currency inválido (deve ser código ISO 4217)' };
427
+ if (!isValidCurrency(val)) return { error: 'currency inválido (deve ser código ISO 4217)', sanitized: null };
305
428
  return { sanitized: val.trim().toUpperCase() };
306
429
  },
307
430
  };
@@ -360,6 +483,14 @@ export default {
360
483
  }
361
484
  }
362
485
 
486
+ // ── fbc derivado de fbclid ───────────────────────────────────────────
487
+ // Quando o usuário vem de um clique Meta, fbclid chega na URL e o cdpTrack.js
488
+ // o envia como campo. O CAPI espera fbc no formato fb.1.{ms}.{fbclid}.
489
+ // Sem essa conversão, o sinal de clique some — EMQ cai e deduplicação piora.
490
+ if (trackPayload.fbclid && !trackPayload.fbc) {
491
+ trackPayload.fbc = `fb.1.${Date.now()}.${trackPayload.fbclid}`;
492
+ }
493
+
363
494
  // ── Validação de Valor Numérico ───────────────────────────────────
364
495
  if (trackPayload.value !== undefined && trackPayload.value !== null) {
365
496
  if (!isValidValue(trackPayload.value)) {
@@ -519,8 +650,46 @@ export default {
519
650
  distKm < 60 ? 'moderate' : 'far';
520
651
  }
521
652
 
653
+ // ── Quiz Scoring Engine (Fase 6) ─────────────────────────────────────
654
+ // Roda antes do LTV para que intentionLevel qualificado alimente a predição.
655
+ // O front envia quiz_answers: [{question, answer, step}] junto com QuizComplete.
656
+ if (eventName === 'QuizComplete' && Array.isArray(payload.quiz_answers) && payload.quiz_answers.length > 0) {
657
+ try {
658
+ const quizResult = await scoreQuizAnswers(env, payload.quiz_answers, payload.quiz_name ?? null);
659
+
660
+ // Injeta qualificação no payload — flui para LTV, Meta Signal, D1 e CAPI
661
+ payload.intentionLevel = quizResult.qualification;
662
+ payload.intent_score = quizResult.intent_score;
663
+ payload.intentScoreNum = quizResult.intent_score;
664
+ payload.intent_bucket = quizResult.intent_score >= 0.8 ? 'high'
665
+ : quizResult.intent_score >= 0.5 ? 'medium' : 'low';
666
+
667
+ // Campos extras para auditoria e dashboard
668
+ (payload as any).quiz_qualification = quizResult.qualification;
669
+ (payload as any).quiz_confidence = quizResult.confidence;
670
+ (payload as any).quiz_weighted_score = quizResult.weighted_score;
671
+ (payload as any).quiz_dominant_dimension = quizResult.dominant_dimension;
672
+ (payload as any).quiz_signals = quizResult.dimensions.map((d: any) => d.signal).filter(Boolean);
673
+ (payload as any).quiz_source = quizResult.source;
674
+
675
+ // utm_term — injetado pelo Worker após qualificação (nunca configurado nos anúncios)
676
+ // Flui para Meta CAPI custom_data e D1 user_profiles para cruzar com ROAS Feedback
677
+ payload.utmTerm = quizResult.qualification; // comprador | interessado | curioso | perdido
678
+
679
+ // Persiste sessão no D1 em background
680
+ if (env.DB) ctx.waitUntil(saveQuizSession(env, payload.userId, payload, quizResult));
681
+
682
+ // Agenda nurture sequence baseada na qualificação (background)
683
+ ctx.waitUntil(scheduleNurture(env, payload, quizResult.qualification));
684
+
685
+ } catch (err: any) {
686
+ console.error('[QuizScoring] erro ao qualificar respostas:', err?.message || String(err));
687
+ // Fail-safe: continua sem qualificação
688
+ }
689
+ }
690
+
522
691
  // ── LTV Prediction (+ A/B Testing de Prompts) ─────────────────────────
523
- const LTV_EVENTS = ['Lead', 'Contact', 'Schedule', 'CompleteRegistration'];
692
+ const LTV_EVENTS = ['Lead', 'Contact', 'Schedule', 'CompleteRegistration', 'QuizComplete'];
524
693
  if (LTV_EVENTS.includes(eventName) && !payload.value) {
525
694
  const abVariation = await getLtvAbVariation(env);
526
695
  const ltv = await predictLtv(env, payload, request, abVariation?.system_prompt || null);
@@ -727,6 +896,7 @@ export default {
727
896
  return new Response(JSON.stringify({ skipped: `status ${wh.order_status}` }), { status: 200, headers });
728
897
  }
729
898
 
899
+ const customer = wh.Customer || {};
730
900
  const kwTxId = String(wh.order_id || '');
731
901
  const dupCheck = await processWebhookDuplicateCheck(env, 'kiwify', kwTxId, JSON.stringify(wh), {
732
902
  email: customer.email,
@@ -735,8 +905,6 @@ export default {
735
905
  if (dupCheck.duplicate) {
736
906
  return new Response(JSON.stringify({ ok: true, skipped: 'duplicate' }), { status: 200, headers });
737
907
  }
738
-
739
- const customer = wh.Customer || {};
740
908
  const product = wh.Product || {};
741
909
  const profile = await getProfileByEmail(env, customer.email || '');
742
910