cdp-edge 1.27.0 → 1.28.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 +179 -459
- package/contracts/api-versions.json +6 -6
- package/dist/sdk/cdpTrack.js +2095 -0
- package/dist/sdk/cdpTrack.min.js +64 -0
- package/dist/sdk/install-snippet.html +10 -0
- package/extracted-skill/tracking-events-generator/agents/devops-agent.md +22 -0
- package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +53 -0
- package/extracted-skill/tracking-events-generator/agents/lead-scoring-agent.md +282 -0
- package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +60 -6
- package/extracted-skill/tracking-events-generator/agents/match-quality-agent.md +304 -0
- package/extracted-skill/tracking-events-generator/agents/utm-agent.md +285 -154
- package/extracted-skill/tracking-events-generator/anti-blocking.js +1 -1
- package/extracted-skill/tracking-events-generator/cdpTrack.js +10 -18
- package/extracted-skill/tracking-events-generator/contracts/api-versions.json +6 -6
- package/extracted-skill/tracking-events-generator/engagement-scoring.js +2 -2
- package/extracted-skill/tracking-events-generator/micro-events.js +1 -1
- package/extracted-skill/tracking-events-generator/models/quiz-funnel.md +83 -19
- package/extracted-skill/tracking-events-generator/tracking.config.js +3 -3
- package/package.json +5 -1
- package/scripts/build-sdk.js +106 -0
- package/server-edge-tracker/index.ts +81 -6
- package/server-edge-tracker/modules/intelligence.ts +155 -2
- package/server-edge-tracker/modules/ml/quiz.ts +343 -0
- package/server-edge-tracker/modules/ml/roas.ts +255 -0
- package/server-edge-tracker/modules/nurture.ts +257 -0
- package/server-edge-tracker/modules/utils.ts +2 -0
- package/server-edge-tracker/schema-quiz.sql +52 -0
- package/server-edge-tracker/schema-sales-engine.sql +113 -0
- 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
|
|
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)**:
|
|
12
|
-
3. **
|
|
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
|
|
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
|
-
|
|
48
|
+
Acumule as respostas do quiz em um array local.
|
|
30
49
|
|
|
31
50
|
```javascript
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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**:
|
|
67
|
-
- **Deduplicação**:
|
|
68
|
-
- **
|
|
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/
|
|
14
|
-
// Em dev: use o URL do Worker diretamente
|
|
15
|
-
endpoint: '/
|
|
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": "1.
|
|
3
|
+
"version": "1.28.0",
|
|
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,
|
|
@@ -272,6 +281,27 @@ export default {
|
|
|
272
281
|
const { eventName, behavioral_data, ...payload } = body as { eventName?: string; behavioral_data?: BehavioralData; [key: string]: any };
|
|
273
282
|
const trackPayload: TrackPayload = payload;
|
|
274
283
|
|
|
284
|
+
// ── Extrair click_ids e utms de sub-objetos (formato cdpTrack.js) ────────
|
|
285
|
+
// cdpTrack.js envia fbp/fbc/fbclid dentro de click_ids{} e UTMs dentro de utms{}
|
|
286
|
+
// O Worker trabalha com campos no nível raiz — esta extração resolve o mismatch.
|
|
287
|
+
if (payload.click_ids && typeof payload.click_ids === 'object') {
|
|
288
|
+
const c = payload.click_ids as Record<string, string>;
|
|
289
|
+
if (!trackPayload.fbp && c.fbp) trackPayload.fbp = c.fbp;
|
|
290
|
+
if (!trackPayload.fbc && c.fbc) trackPayload.fbc = c.fbc;
|
|
291
|
+
if (!trackPayload.fbclid && c.fbclid) trackPayload.fbclid = c.fbclid;
|
|
292
|
+
if (!trackPayload.gclid && c.gclid) trackPayload.gclid = c.gclid;
|
|
293
|
+
if (!trackPayload.ttclid && c.ttclid) trackPayload.ttclid = c.ttclid;
|
|
294
|
+
if (!trackPayload.ttp && c.ttp) trackPayload.ttp = c.ttp; // TikTok Pixel cookie
|
|
295
|
+
}
|
|
296
|
+
if (payload.utms && typeof payload.utms === 'object') {
|
|
297
|
+
const u = payload.utms as Record<string, string>;
|
|
298
|
+
if (!trackPayload.utmSource && u.utm_source) trackPayload.utmSource = u.utm_source;
|
|
299
|
+
if (!trackPayload.utmMedium && u.utm_medium) trackPayload.utmMedium = u.utm_medium;
|
|
300
|
+
if (!trackPayload.utmCampaign && u.utm_campaign) trackPayload.utmCampaign = u.utm_campaign;
|
|
301
|
+
if (!trackPayload.utmContent && u.utm_content) trackPayload.utmContent = u.utm_content;
|
|
302
|
+
if (!trackPayload.utmTerm && u.utm_term) trackPayload.utmTerm = u.utm_term;
|
|
303
|
+
}
|
|
304
|
+
|
|
275
305
|
// ── Validação de eventName ────────────────────────────────────────
|
|
276
306
|
if (!eventName) {
|
|
277
307
|
return new Response(JSON.stringify({ error: 'eventName é obrigatório' }), { status: 400, headers });
|
|
@@ -286,7 +316,7 @@ export default {
|
|
|
286
316
|
|
|
287
317
|
const SANITIZE_FIELDS: Record<string, (val: string) => SanitizeResult> = {
|
|
288
318
|
email: (val: string) => {
|
|
289
|
-
if (!isValidEmail(val)) return { error: 'email inválido (formato incorreto)' };
|
|
319
|
+
if (!isValidEmail(val)) return { error: 'email inválido (formato incorreto)', sanitized: null };
|
|
290
320
|
return { sanitized: val.toLowerCase().trim() };
|
|
291
321
|
},
|
|
292
322
|
firstName: (val: string) => ({ sanitized: sanitizeString(val, 100) }),
|
|
@@ -297,11 +327,11 @@ export default {
|
|
|
297
327
|
dob: (val: string) => ({ sanitized: sanitizeString(val, 20) }),
|
|
298
328
|
productName: (val: string) => ({ sanitized: sanitizeString(val, 200) }),
|
|
299
329
|
pageUrl: (val: string) => {
|
|
300
|
-
if (!isValidUrl(val)) return { error: 'pageUrl inválido (formato incorreto)' };
|
|
330
|
+
if (!isValidUrl(val)) return { error: 'pageUrl inválido (formato incorreto)', sanitized: null };
|
|
301
331
|
return { sanitized: val.trim() };
|
|
302
332
|
},
|
|
303
333
|
currency: (val: string) => {
|
|
304
|
-
if (!isValidCurrency(val)) return { error: 'currency inválido (deve ser código ISO 4217)' };
|
|
334
|
+
if (!isValidCurrency(val)) return { error: 'currency inválido (deve ser código ISO 4217)', sanitized: null };
|
|
305
335
|
return { sanitized: val.trim().toUpperCase() };
|
|
306
336
|
},
|
|
307
337
|
};
|
|
@@ -360,6 +390,14 @@ export default {
|
|
|
360
390
|
}
|
|
361
391
|
}
|
|
362
392
|
|
|
393
|
+
// ── fbc derivado de fbclid ───────────────────────────────────────────
|
|
394
|
+
// Quando o usuário vem de um clique Meta, fbclid chega na URL e o cdpTrack.js
|
|
395
|
+
// o envia como campo. O CAPI espera fbc no formato fb.1.{ms}.{fbclid}.
|
|
396
|
+
// Sem essa conversão, o sinal de clique some — EMQ cai e deduplicação piora.
|
|
397
|
+
if (trackPayload.fbclid && !trackPayload.fbc) {
|
|
398
|
+
trackPayload.fbc = `fb.1.${Date.now()}.${trackPayload.fbclid}`;
|
|
399
|
+
}
|
|
400
|
+
|
|
363
401
|
// ── Validação de Valor Numérico ───────────────────────────────────
|
|
364
402
|
if (trackPayload.value !== undefined && trackPayload.value !== null) {
|
|
365
403
|
if (!isValidValue(trackPayload.value)) {
|
|
@@ -519,8 +557,46 @@ export default {
|
|
|
519
557
|
distKm < 60 ? 'moderate' : 'far';
|
|
520
558
|
}
|
|
521
559
|
|
|
560
|
+
// ── Quiz Scoring Engine (Fase 6) ─────────────────────────────────────
|
|
561
|
+
// Roda antes do LTV para que intentionLevel qualificado alimente a predição.
|
|
562
|
+
// O front envia quiz_answers: [{question, answer, step}] junto com QuizComplete.
|
|
563
|
+
if (eventName === 'QuizComplete' && Array.isArray(payload.quiz_answers) && payload.quiz_answers.length > 0) {
|
|
564
|
+
try {
|
|
565
|
+
const quizResult = await scoreQuizAnswers(env, payload.quiz_answers, payload.quiz_name ?? null);
|
|
566
|
+
|
|
567
|
+
// Injeta qualificação no payload — flui para LTV, Meta Signal, D1 e CAPI
|
|
568
|
+
payload.intentionLevel = quizResult.qualification;
|
|
569
|
+
payload.intent_score = quizResult.intent_score;
|
|
570
|
+
payload.intentScoreNum = quizResult.intent_score;
|
|
571
|
+
payload.intent_bucket = quizResult.intent_score >= 0.8 ? 'high'
|
|
572
|
+
: quizResult.intent_score >= 0.5 ? 'medium' : 'low';
|
|
573
|
+
|
|
574
|
+
// Campos extras para auditoria e dashboard
|
|
575
|
+
(payload as any).quiz_qualification = quizResult.qualification;
|
|
576
|
+
(payload as any).quiz_confidence = quizResult.confidence;
|
|
577
|
+
(payload as any).quiz_weighted_score = quizResult.weighted_score;
|
|
578
|
+
(payload as any).quiz_dominant_dimension = quizResult.dominant_dimension;
|
|
579
|
+
(payload as any).quiz_signals = quizResult.dimensions.map((d: any) => d.signal).filter(Boolean);
|
|
580
|
+
(payload as any).quiz_source = quizResult.source;
|
|
581
|
+
|
|
582
|
+
// utm_term — injetado pelo Worker após qualificação (nunca configurado nos anúncios)
|
|
583
|
+
// Flui para Meta CAPI custom_data e D1 user_profiles para cruzar com ROAS Feedback
|
|
584
|
+
payload.utmTerm = quizResult.qualification; // comprador | interessado | curioso | perdido
|
|
585
|
+
|
|
586
|
+
// Persiste sessão no D1 em background
|
|
587
|
+
if (env.DB) ctx.waitUntil(saveQuizSession(env, payload.userId, payload, quizResult));
|
|
588
|
+
|
|
589
|
+
// Agenda nurture sequence baseada na qualificação (background)
|
|
590
|
+
ctx.waitUntil(scheduleNurture(env, payload, quizResult.qualification));
|
|
591
|
+
|
|
592
|
+
} catch (err: any) {
|
|
593
|
+
console.error('[QuizScoring] erro ao qualificar respostas:', err?.message || String(err));
|
|
594
|
+
// Fail-safe: continua sem qualificação
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
522
598
|
// ── LTV Prediction (+ A/B Testing de Prompts) ─────────────────────────
|
|
523
|
-
const LTV_EVENTS = ['Lead', 'Contact', 'Schedule', 'CompleteRegistration'];
|
|
599
|
+
const LTV_EVENTS = ['Lead', 'Contact', 'Schedule', 'CompleteRegistration', 'QuizComplete'];
|
|
524
600
|
if (LTV_EVENTS.includes(eventName) && !payload.value) {
|
|
525
601
|
const abVariation = await getLtvAbVariation(env);
|
|
526
602
|
const ltv = await predictLtv(env, payload, request, abVariation?.system_prompt || null);
|
|
@@ -727,6 +803,7 @@ export default {
|
|
|
727
803
|
return new Response(JSON.stringify({ skipped: `status ${wh.order_status}` }), { status: 200, headers });
|
|
728
804
|
}
|
|
729
805
|
|
|
806
|
+
const customer = wh.Customer || {};
|
|
730
807
|
const kwTxId = String(wh.order_id || '');
|
|
731
808
|
const dupCheck = await processWebhookDuplicateCheck(env, 'kiwify', kwTxId, JSON.stringify(wh), {
|
|
732
809
|
email: customer.email,
|
|
@@ -735,8 +812,6 @@ export default {
|
|
|
735
812
|
if (dupCheck.duplicate) {
|
|
736
813
|
return new Response(JSON.stringify({ ok: true, skipped: 'duplicate' }), { status: 200, headers });
|
|
737
814
|
}
|
|
738
|
-
|
|
739
|
-
const customer = wh.Customer || {};
|
|
740
815
|
const product = wh.Product || {};
|
|
741
816
|
const profile = await getProfileByEmail(env, customer.email || '');
|
|
742
817
|
|
|
@@ -9,6 +9,8 @@ import { sendCallMeBot } from './dispatch/whatsapp.js';
|
|
|
9
9
|
import { autoDecideAbWinner } from './ml/ltv.js';
|
|
10
10
|
import { analyzeMatchQuality, alertMatchQuality, purgeOldMatchQualityLogs } from './ml/matchquality.js';
|
|
11
11
|
import { trainLogisticRegression, extractFeatures, saveWeights, LTV_WEIGHTS_KV_KEY } from './ml/logistic.js';
|
|
12
|
+
import { computeRoasFeedback, sendRoasAlert } from './ml/roas.js';
|
|
13
|
+
import { runNurtureQueue } from './nurture.js';
|
|
12
14
|
import { Env } from '../types.js';
|
|
13
15
|
|
|
14
16
|
// ── Tipos ───────────────────────────────────────────────────────────────────────
|
|
@@ -57,6 +59,22 @@ export interface IntelligenceAgentResult {
|
|
|
57
59
|
skipped?: string;
|
|
58
60
|
error?: string;
|
|
59
61
|
};
|
|
62
|
+
roasResult?: {
|
|
63
|
+
campaigns: number;
|
|
64
|
+
total_revenue: number;
|
|
65
|
+
best_campaign: string | null;
|
|
66
|
+
skipped?: string;
|
|
67
|
+
};
|
|
68
|
+
nurtureResult?: {
|
|
69
|
+
processed: number;
|
|
70
|
+
sent: number;
|
|
71
|
+
failed: number;
|
|
72
|
+
};
|
|
73
|
+
lookalikeResult?: {
|
|
74
|
+
sent: number;
|
|
75
|
+
seed_type: string;
|
|
76
|
+
skipped?: string;
|
|
77
|
+
};
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
export interface CustomerMatchResult {
|
|
@@ -330,11 +348,50 @@ export async function runIntelligenceAgent(
|
|
|
330
348
|
}
|
|
331
349
|
}
|
|
332
350
|
|
|
333
|
-
// 8. Customer Match sync semanal
|
|
351
|
+
// 8. Customer Match sync semanal (high_intent → Meta Audience)
|
|
334
352
|
const cmResult = await syncMetaCustomAudience(env);
|
|
335
353
|
console.log(`[Intelligence Agent] Customer Match Meta: sent=${cmResult?.sent ?? 0}, received=${cmResult?.received ?? 0}`);
|
|
336
354
|
|
|
337
|
-
|
|
355
|
+
// 9. ROAS Feedback Loop — cruza leads com compras reais por campanha
|
|
356
|
+
let roasResult: IntelligenceAgentResult['roasResult'] = undefined;
|
|
357
|
+
try {
|
|
358
|
+
const report = await computeRoasFeedback(env, 30);
|
|
359
|
+
if (report) {
|
|
360
|
+
roasResult = {
|
|
361
|
+
campaigns: report.campaigns.length,
|
|
362
|
+
total_revenue: report.total_revenue,
|
|
363
|
+
best_campaign: report.best_campaign,
|
|
364
|
+
};
|
|
365
|
+
await sendRoasAlert(env, report);
|
|
366
|
+
console.log(`[Intelligence Agent] ROAS: ${report.campaigns.length} campanhas, R$${report.total_revenue} receita`);
|
|
367
|
+
} else {
|
|
368
|
+
roasResult = { campaigns: 0, total_revenue: 0, best_campaign: null, skipped: 'sem dados suficientes' };
|
|
369
|
+
}
|
|
370
|
+
} catch (err: any) {
|
|
371
|
+
console.error('[Intelligence Agent] ROAS error:', err?.message || String(err));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 10. Nurture Queue — processa mensagens agendadas (D+1, D+3, D+7)
|
|
375
|
+
let nurtureResult: IntelligenceAgentResult['nurtureResult'] = undefined;
|
|
376
|
+
try {
|
|
377
|
+
const nr = await runNurtureQueue(env);
|
|
378
|
+
nurtureResult = { processed: nr.processed, sent: nr.sent, failed: nr.failed };
|
|
379
|
+
console.log(`[Intelligence Agent] Nurture: ${nr.sent}/${nr.processed} mensagens enviadas`);
|
|
380
|
+
} catch (err: any) {
|
|
381
|
+
console.error('[Intelligence Agent] Nurture error:', err?.message || String(err));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 11. Lookalike Dinâmico — compradores confirmados → Meta Audience seed
|
|
385
|
+
let lookalikeResult: IntelligenceAgentResult['lookalikeResult'] = undefined;
|
|
386
|
+
try {
|
|
387
|
+
const lr = await syncMetaLookalikeSeed(env);
|
|
388
|
+
lookalikeResult = lr;
|
|
389
|
+
console.log(`[Intelligence Agent] Lookalike seed: sent=${lr.sent}, type=${lr.seed_type}`);
|
|
390
|
+
} catch (err: any) {
|
|
391
|
+
console.error('[Intelligence Agent] Lookalike error:', err?.message || String(err));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.log(`[Intelligence Agent] ${runType} concluído — LTV, A/B, match quality, customer match, ROAS, nurture, lookalike`);
|
|
338
395
|
|
|
339
396
|
return {
|
|
340
397
|
versionResults,
|
|
@@ -343,6 +400,9 @@ export async function runIntelligenceAgent(
|
|
|
343
400
|
abResult,
|
|
344
401
|
mqAnalysis,
|
|
345
402
|
cmResult,
|
|
403
|
+
roasResult,
|
|
404
|
+
nurtureResult,
|
|
405
|
+
lookalikeResult,
|
|
346
406
|
};
|
|
347
407
|
}
|
|
348
408
|
|
|
@@ -400,6 +460,99 @@ export async function syncMetaCustomAudience(env: Env): Promise<CustomerMatchRes
|
|
|
400
460
|
}
|
|
401
461
|
}
|
|
402
462
|
|
|
463
|
+
// ── syncMetaLookalikeSeed — compradores confirmados → Meta Audience (Fase 7) ──
|
|
464
|
+
// Seed de Lookalike mais preciso: usa quem REALMENTE comprou (Purchase event)
|
|
465
|
+
// em vez de quem só teve intenção (cohort_label = high_intent).
|
|
466
|
+
// Separado do syncMetaCustomAudience para não misturar seeds de qualidade diferente.
|
|
467
|
+
|
|
468
|
+
export async function syncMetaLookalikeSeed(env: Env): Promise<{
|
|
469
|
+
sent: number;
|
|
470
|
+
seed_type: string;
|
|
471
|
+
skipped?: string;
|
|
472
|
+
}> {
|
|
473
|
+
if (!env.META_ACCESS_TOKEN || !env.META_AUDIENCE_ID) {
|
|
474
|
+
return { sent: 0, seed_type: 'buyer_confirmed', skipped: 'META secrets não configurados' };
|
|
475
|
+
}
|
|
476
|
+
if (!env.DB) return { sent: 0, seed_type: 'buyer_confirmed', skipped: 'DB não disponível' };
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
// Busca perfis de compradores confirmados (Purchase event nos últimos 60 dias)
|
|
480
|
+
const confirmed = await env.DB.prepare(`
|
|
481
|
+
SELECT DISTINCT up.email, up.phone, up.first_name, up.last_name
|
|
482
|
+
FROM user_profiles up
|
|
483
|
+
JOIN leads l ON l.user_id = up.user_id
|
|
484
|
+
WHERE l.event_name IN ('Purchase','purchase')
|
|
485
|
+
AND l.created_at >= datetime('now', '-60 days')
|
|
486
|
+
AND up.email IS NOT NULL
|
|
487
|
+
UNION
|
|
488
|
+
SELECT DISTINCT up.email, up.phone, up.first_name, up.last_name
|
|
489
|
+
FROM user_profiles up
|
|
490
|
+
JOIN quiz_sessions qs ON qs.user_id = up.user_id
|
|
491
|
+
WHERE qs.qualification = 'comprador'
|
|
492
|
+
AND qs.created_at >= datetime('now', '-30 days')
|
|
493
|
+
AND up.email IS NOT NULL
|
|
494
|
+
LIMIT 10000
|
|
495
|
+
`).all();
|
|
496
|
+
|
|
497
|
+
if (!confirmed.results?.length) {
|
|
498
|
+
return { sent: 0, seed_type: 'buyer_confirmed', skipped: 'nenhum comprador confirmado no período' };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const data = await Promise.all(
|
|
502
|
+
confirmed.results.map(async (p: any) => [
|
|
503
|
+
p.email ? await sha256(p.email) : '',
|
|
504
|
+
p.phone ? await sha256(p.phone) : '',
|
|
505
|
+
])
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
const body = { payload: { schema: ['EMAIL_SHA256', 'PHONE_SHA256'], data } };
|
|
509
|
+
const endpoint = `https://graph.facebook.com/v22.0/${env.META_AUDIENCE_ID}/users`;
|
|
510
|
+
|
|
511
|
+
const res = await fetch(endpoint, {
|
|
512
|
+
method: 'POST',
|
|
513
|
+
headers: { 'Content-Type': 'application/json' },
|
|
514
|
+
body: JSON.stringify({ ...body, access_token: env.META_ACCESS_TOKEN }),
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const result = await res.json() as any;
|
|
518
|
+
|
|
519
|
+
// Persiste histórico do seed
|
|
520
|
+
if (env.DB) {
|
|
521
|
+
await env.DB.prepare(`
|
|
522
|
+
INSERT INTO lookalike_seeds (audience_id, seed_type, profiles_sent, profiles_received, period_days)
|
|
523
|
+
VALUES (?, 'buyer_confirmed', ?, ?, 60)
|
|
524
|
+
`).bind(
|
|
525
|
+
env.META_AUDIENCE_ID,
|
|
526
|
+
confirmed.results.length,
|
|
527
|
+
result.num_received ?? null,
|
|
528
|
+
).run().catch(() => {});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (!res.ok) {
|
|
532
|
+
console.error('[Lookalike] Meta erro:', result.error?.message);
|
|
533
|
+
return { sent: 0, seed_type: 'buyer_confirmed', skipped: result.error?.message };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Atualiza cohort_label dos compradores para buyer_confirmed
|
|
537
|
+
await env.DB.prepare(`
|
|
538
|
+
UPDATE user_profiles
|
|
539
|
+
SET cohort_label = 'buyer_confirmed', updated_at = datetime('now')
|
|
540
|
+
WHERE user_id IN (
|
|
541
|
+
SELECT DISTINCT user_id FROM leads
|
|
542
|
+
WHERE event_name IN ('Purchase','purchase')
|
|
543
|
+
AND created_at >= datetime('now', '-60 days')
|
|
544
|
+
)
|
|
545
|
+
`).run().catch(() => {});
|
|
546
|
+
|
|
547
|
+
console.log(`[Lookalike] ${confirmed.results.length} compradores confirmados enviados ao Meta`);
|
|
548
|
+
return { sent: confirmed.results.length, seed_type: 'buyer_confirmed' };
|
|
549
|
+
|
|
550
|
+
} catch (err: any) {
|
|
551
|
+
console.error('[Lookalike] syncMetaLookalikeSeed error:', err?.message || String(err));
|
|
552
|
+
return { sent: 0, seed_type: 'buyer_confirmed', skipped: err?.message };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
403
556
|
// ── buildGoogleCustomerMatchExport — gera JSON para Google Ads Customer Match ─
|
|
404
557
|
export async function buildGoogleCustomerMatchExport(env: Env): Promise<GoogleCustomerMatchExport[]> {
|
|
405
558
|
if (!env.DB) return [];
|