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.
Files changed (29) hide show
  1. package/README.md +179 -459
  2. package/contracts/api-versions.json +6 -6
  3. package/dist/sdk/cdpTrack.js +2095 -0
  4. package/dist/sdk/cdpTrack.min.js +64 -0
  5. package/dist/sdk/install-snippet.html +10 -0
  6. package/extracted-skill/tracking-events-generator/agents/devops-agent.md +22 -0
  7. package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +53 -0
  8. package/extracted-skill/tracking-events-generator/agents/lead-scoring-agent.md +282 -0
  9. package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +60 -6
  10. package/extracted-skill/tracking-events-generator/agents/match-quality-agent.md +304 -0
  11. package/extracted-skill/tracking-events-generator/agents/utm-agent.md +285 -154
  12. package/extracted-skill/tracking-events-generator/anti-blocking.js +1 -1
  13. package/extracted-skill/tracking-events-generator/cdpTrack.js +10 -18
  14. package/extracted-skill/tracking-events-generator/contracts/api-versions.json +6 -6
  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 +81 -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
@@ -0,0 +1,257 @@
1
+ /**
2
+ * CDP Edge — Nurture Engine (Fase 7)
3
+ *
4
+ * Sequências de follow-up automáticas baseadas na qualificação do quiz:
5
+ * comprador → contato imediato (já tratado pelo hot lead)
6
+ * interessado → D+1, D+3, D+7 (WhatsApp ou email)
7
+ * curioso → D+2, D+5 (conteúdo/isca)
8
+ * perdido → exclusão do remarketing (cohort_label = excluded)
9
+ *
10
+ * Arquitetura:
11
+ * 1. scheduleNurture() — chamado no QuizComplete, insere sequência no D1
12
+ * 2. runNurtureQueue() — chamado pelo Intelligence Agent (cron diário)
13
+ * envia as mensagens com send_at <= now()
14
+ */
15
+
16
+ import { Env, TrackPayload } from '../types.js';
17
+
18
+ // ── Tipos ─────────────────────────────────────────────────────────────────────
19
+
20
+ export type NurtureQualification = 'comprador' | 'interessado' | 'curioso' | 'perdido';
21
+
22
+ export interface NurtureStep {
23
+ delay_days: number;
24
+ channel: 'whatsapp' | 'email';
25
+ message: string; // suporta {{name}}, {{quiz_name}}, {{qualification}}
26
+ subject?: string; // apenas email
27
+ }
28
+
29
+ export interface NurtureResult {
30
+ scheduled: number;
31
+ skipped: string | null;
32
+ }
33
+
34
+ export interface NurtureRunResult {
35
+ processed: number;
36
+ sent: number;
37
+ failed: number;
38
+ excluded: number;
39
+ }
40
+
41
+ // ── Sequências por qualificação ───────────────────────────────────────────────
42
+ // Mensagens genéricas — o cliente personaliza via automation_rules no D1.
43
+ // O Nurture Engine usa estas como fallback quando não há regra cadastrada.
44
+
45
+ const NURTURE_SEQUENCES: Record<NurtureQualification, NurtureStep[]> = {
46
+ comprador: [
47
+ // comprador já dispara hot lead imediato no /track — sem sequência adicional aqui
48
+ ],
49
+ interessado: [
50
+ {
51
+ delay_days: 1,
52
+ channel: 'whatsapp',
53
+ message: 'Olá {{name}}! Vi que você completou nosso diagnóstico e ficou entre os mais qualificados. Posso te enviar mais detalhes sobre como podemos ajudar?',
54
+ },
55
+ {
56
+ delay_days: 3,
57
+ channel: 'whatsapp',
58
+ message: 'Oi {{name}}, tudo bem? Separei um conteúdo exclusivo baseado nas suas respostas no quiz "{{quiz_name}}". Posso compartilhar?',
59
+ },
60
+ {
61
+ delay_days: 7,
62
+ channel: 'whatsapp',
63
+ message: '{{name}}, última oportunidade esta semana! Muitos que fizeram o mesmo diagnóstico que você já estão obtendo resultados. Que tal conversarmos 15 minutos?',
64
+ },
65
+ ],
66
+ curioso: [
67
+ {
68
+ delay_days: 2,
69
+ channel: 'whatsapp',
70
+ message: 'Olá {{name}}! Você completou nosso diagnóstico. Preparei um material gratuito baseado no seu perfil. Posso enviar?',
71
+ },
72
+ {
73
+ delay_days: 5,
74
+ channel: 'whatsapp',
75
+ message: 'Oi {{name}}! Vi que você está pesquisando sobre o assunto. Tenho uma aula gratuita que pode te ajudar muito. Interesse?',
76
+ },
77
+ ],
78
+ perdido: [
79
+ // perdido não recebe mensagens — apenas é excluído do remarketing
80
+ ],
81
+ };
82
+
83
+ // ── scheduleNurture — agenda sequência após QuizComplete ─────────────────────
84
+
85
+ export async function scheduleNurture(
86
+ env: Env,
87
+ payload: TrackPayload,
88
+ qualification: NurtureQualification,
89
+ ): Promise<NurtureResult> {
90
+ if (!env.DB) return { scheduled: 0, skipped: 'DB não disponível' };
91
+
92
+ // perdido → só atualiza cohort_label para excluir do remarketing
93
+ if (qualification === 'perdido') {
94
+ if (payload.userId) {
95
+ try {
96
+ await env.DB.prepare(`
97
+ UPDATE user_profiles
98
+ SET cohort_label = 'excluded', updated_at = datetime('now')
99
+ WHERE user_id = ?
100
+ `).bind(payload.userId).run();
101
+ } catch { /* não-crítico */ }
102
+ }
103
+ return { scheduled: 0, skipped: 'perdido — excluído do remarketing' };
104
+ }
105
+
106
+ // comprador → contato imediato já gerenciado pelo hot lead em index.ts
107
+ if (qualification === 'comprador') {
108
+ return { scheduled: 0, skipped: 'comprador — contato imediato via hot lead' };
109
+ }
110
+
111
+ const steps = NURTURE_SEQUENCES[qualification];
112
+ if (!steps || steps.length === 0) {
113
+ return { scheduled: 0, skipped: `sem sequência para ${qualification}` };
114
+ }
115
+
116
+ // Verifica se já tem sequência ativa para este usuário (evita duplicar)
117
+ if (payload.userId) {
118
+ try {
119
+ const existing = await env.DB.prepare(`
120
+ SELECT id FROM nurture_sequences
121
+ WHERE user_id = ? AND status = 'pending'
122
+ LIMIT 1
123
+ `).bind(payload.userId).first();
124
+ if (existing) return { scheduled: 0, skipped: 'sequência já existe para este usuário' };
125
+ } catch { /* continua */ }
126
+ }
127
+
128
+ let scheduled = 0;
129
+ for (const step of steps) {
130
+ try {
131
+ const sendAt = new Date();
132
+ sendAt.setDate(sendAt.getDate() + step.delay_days);
133
+
134
+ await env.DB.prepare(`
135
+ INSERT INTO nurture_sequences (
136
+ user_id, qualification, delay_days, channel,
137
+ message, subject, send_at, status,
138
+ quiz_name, phone, email, first_name, created_at
139
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'))
140
+ `).bind(
141
+ payload.userId || null,
142
+ qualification,
143
+ step.delay_days,
144
+ step.channel,
145
+ step.message,
146
+ step.subject || null,
147
+ sendAt.toISOString().slice(0, 19).replace('T', ' '),
148
+ 'pending',
149
+ String((payload as any).quiz_name || ''),
150
+ payload.phone || null,
151
+ payload.email || null,
152
+ payload.firstName || null,
153
+ ).run();
154
+
155
+ scheduled++;
156
+ } catch (err: any) {
157
+ console.error('[Nurture] scheduleNurture insert error:', err?.message || String(err));
158
+ }
159
+ }
160
+
161
+ return { scheduled, skipped: null };
162
+ }
163
+
164
+ // ── runNurtureQueue — processa mensagens pendentes (chamado pelo cron) ────────
165
+
166
+ export async function runNurtureQueue(env: Env): Promise<NurtureRunResult> {
167
+ if (!env.DB) return { processed: 0, sent: 0, failed: 0, excluded: 0 };
168
+
169
+ const result: NurtureRunResult = { processed: 0, sent: 0, failed: 0, excluded: 0 };
170
+
171
+ try {
172
+ // Busca até 50 mensagens pendentes com send_at <= agora
173
+ const pending = await env.DB.prepare(`
174
+ SELECT * FROM nurture_sequences
175
+ WHERE status = 'pending'
176
+ AND send_at <= datetime('now')
177
+ ORDER BY send_at ASC
178
+ LIMIT 50
179
+ `).all();
180
+
181
+ const rows = (pending.results || []) as any[];
182
+ if (rows.length === 0) return result;
183
+
184
+ for (const row of rows) {
185
+ result.processed++;
186
+
187
+ // Interpola variáveis na mensagem
188
+ const vars: Record<string, string> = {
189
+ name: String(row.first_name || 'você'),
190
+ quiz_name: String(row.quiz_name || 'diagnóstico'),
191
+ qualification: String(row.qualification || ''),
192
+ };
193
+ const message = String(row.message || '').replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? '');
194
+ const subject = row.subject ? String(row.subject).replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? '') : null;
195
+
196
+ let success = false;
197
+
198
+ try {
199
+ if (row.channel === 'whatsapp' && row.phone && env.WHATSAPP_ACCESS_TOKEN && env.WHATSAPP_PHONE_NUMBER_ID) {
200
+ const digits = String(row.phone).replace(/\D/g, '');
201
+ const e164 = digits.startsWith('55') ? `+${digits}` : `+55${digits}`;
202
+
203
+ const res = await fetch(
204
+ `https://graph.facebook.com/v22.0/${env.WHATSAPP_PHONE_NUMBER_ID}/messages`,
205
+ {
206
+ method: 'POST',
207
+ headers: { 'Authorization': `Bearer ${env.WHATSAPP_ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
208
+ body: JSON.stringify({
209
+ messaging_product: 'whatsapp',
210
+ recipient_type: 'individual',
211
+ to: e164,
212
+ type: 'text',
213
+ text: { body: message },
214
+ }),
215
+ }
216
+ );
217
+ success = res.ok;
218
+
219
+ } else if (row.channel === 'email' && row.email && env.RESEND_API_KEY) {
220
+ const res = await fetch('https://api.resend.com/emails', {
221
+ method: 'POST',
222
+ headers: { 'Authorization': `Bearer ${env.RESEND_API_KEY}`, 'Content-Type': 'application/json' },
223
+ body: JSON.stringify({
224
+ from: env.RESEND_FROM_EMAIL || 'noreply@cdp-edge.app',
225
+ to: [row.email],
226
+ subject: subject || `Olá, ${vars.name}!`,
227
+ html: `<p>${message.replace(/\n/g, '<br>')}</p>`,
228
+ }),
229
+ });
230
+ success = res.ok;
231
+ }
232
+ } catch (err: any) {
233
+ console.error(`[Nurture] dispatch error (row ${row.id}):`, err?.message || String(err));
234
+ }
235
+
236
+ // Atualiza status no D1
237
+ const newStatus = success ? 'sent' : 'failed';
238
+ const sentAt = success ? `datetime('now')` : 'NULL';
239
+ try {
240
+ await env.DB.prepare(`
241
+ UPDATE nurture_sequences
242
+ SET status = ?, sent_at = ${success ? "datetime('now')" : 'NULL'},
243
+ updated_at = datetime('now')
244
+ WHERE id = ?
245
+ `).bind(newStatus, row.id).run();
246
+ } catch { /* não-crítico */ }
247
+
248
+ if (success) result.sent++;
249
+ else result.failed++;
250
+ }
251
+
252
+ } catch (err: any) {
253
+ console.error('[Nurture] runNurtureQueue error:', err?.message || String(err));
254
+ }
255
+
256
+ return result;
257
+ }
@@ -106,6 +106,8 @@ export const VALID_EVENT_NAMES = new Set([
106
106
  'video_start','video_25','video_50','video_75','video_complete',
107
107
  // Imóveis — intenção de visita física, financiamento e favoritar
108
108
  'FindLocation','CustomizeProduct','AddToWishlist',
109
+ // Quiz Funnel (Fase 6)
110
+ 'QuizStart','QuizAnswer','QuizComplete',
109
111
  ]);
110
112
 
111
113
  // ── Taxonomia de funil (funnel_stage → profundidade semântica) ────────────────
@@ -0,0 +1,52 @@
1
+ -- CDP Edge — Schema Quiz Sessions (Fase 6 v2)
2
+ -- Análise Dimensional Automática por Workers AI
3
+ -- Executar: wrangler d1 execute cdp-edge-db --file=schema-quiz.sql --remote
4
+
5
+ CREATE TABLE IF NOT EXISTS quiz_sessions (
6
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ user_id TEXT, -- _cdp_uid (FK lógica → user_profiles)
8
+ quiz_name TEXT, -- nome do quiz (ex: "Diagnóstico Imóvel")
9
+ answers_json TEXT NOT NULL, -- JSON: [{question, answer, step}]
10
+ qualification TEXT NOT NULL, -- comprador | interessado | curioso | perdido
11
+ intent_score REAL NOT NULL, -- 0.0–1.0 (propagado ao pipeline CDP)
12
+ weighted_score REAL NOT NULL, -- score ponderado bruto Σ(score×weight)/Σ(weight)
13
+ confidence REAL NOT NULL DEFAULT 0.7,
14
+ reason TEXT, -- frase explicativa em português (audit)
15
+ dominant_dimension TEXT, -- dimensão com maior impacto: budget|urgency|etc.
16
+ dimensions_json TEXT, -- JSON: [{step, dimension, score, weight, signal}]
17
+ source TEXT DEFAULT 'ai', -- ai | heuristic
18
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
19
+ );
20
+
21
+ CREATE INDEX IF NOT EXISTS idx_quiz_user_id ON quiz_sessions(user_id);
22
+ CREATE INDEX IF NOT EXISTS idx_quiz_qualification ON quiz_sessions(qualification);
23
+ CREATE INDEX IF NOT EXISTS idx_quiz_created_at ON quiz_sessions(created_at);
24
+ CREATE INDEX IF NOT EXISTS idx_quiz_dominant_dim ON quiz_sessions(dominant_dimension);
25
+
26
+ -- VIEW: distribuição de qualificações por quiz + score médio
27
+ CREATE VIEW IF NOT EXISTS v_quiz_qualification_summary AS
28
+ SELECT
29
+ quiz_name,
30
+ qualification,
31
+ COUNT(*) AS total,
32
+ ROUND(AVG(intent_score) * 100, 1) AS avg_intent_pct,
33
+ ROUND(AVG(weighted_score) * 100, 1) AS avg_weighted_pct,
34
+ ROUND(AVG(confidence) * 100, 1) AS avg_confidence_pct,
35
+ COUNT(CASE WHEN source = 'ai' THEN 1 END) AS ai_scored,
36
+ COUNT(CASE WHEN source = 'heuristic' THEN 1 END) AS heuristic_scored
37
+ FROM quiz_sessions
38
+ GROUP BY quiz_name, qualification
39
+ ORDER BY quiz_name, avg_intent_pct DESC;
40
+
41
+ -- VIEW: qual dimensão mais penaliza/beneficia cada quiz
42
+ CREATE VIEW IF NOT EXISTS v_quiz_dimension_impact AS
43
+ SELECT
44
+ quiz_name,
45
+ dominant_dimension,
46
+ COUNT(*) AS total_sessions,
47
+ ROUND(AVG(weighted_score) * 100, 1) AS avg_weighted_pct,
48
+ qualification
49
+ FROM quiz_sessions
50
+ WHERE dominant_dimension IS NOT NULL
51
+ GROUP BY quiz_name, dominant_dimension, qualification
52
+ ORDER BY quiz_name, total_sessions DESC;
@@ -0,0 +1,113 @@
1
+ -- CDP Edge — Schema Sales Engine (Fase 7)
2
+ -- ROAS Feedback Loop + Nurture Engine + Lookalike Dinâmico
3
+ -- Executar: wrangler d1 execute cdp-edge-db --file=schema-sales-engine.sql --remote
4
+
5
+ -- ── ROAS Reports — histórico de performance por campanha ──────────────────────
6
+ CREATE TABLE IF NOT EXISTS roas_reports (
7
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
8
+ utm_source TEXT NOT NULL,
9
+ utm_campaign TEXT NOT NULL,
10
+ utm_content TEXT NOT NULL DEFAULT 'unknown', -- origem: quiz_*, video_*, landing_*, ctwa_*
11
+ period_days INTEGER NOT NULL DEFAULT 30,
12
+ total_leads INTEGER NOT NULL DEFAULT 0,
13
+ confirmed_buyers INTEGER NOT NULL DEFAULT 0,
14
+ conversion_rate REAL NOT NULL DEFAULT 0, -- 0.0–1.0
15
+ total_revenue REAL NOT NULL DEFAULT 0, -- BRL
16
+ revenue_per_lead REAL NOT NULL DEFAULT 0,
17
+ ltv_accuracy REAL NOT NULL DEFAULT 0, -- % leads High LTV que realmente compraram
18
+ top_qualification TEXT, -- qualificação quiz dominante
19
+ bid_recommendation TEXT NOT NULL DEFAULT 'maintain', -- increase|maintain|decrease|pause
20
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
21
+ );
22
+
23
+ -- Migração para instâncias existentes (idempotente via IF NOT EXISTS não funciona em ALTER TABLE —
24
+ -- execute manualmente se a tabela já existir):
25
+ -- ALTER TABLE roas_reports ADD COLUMN utm_content TEXT NOT NULL DEFAULT 'unknown';
26
+
27
+ CREATE INDEX IF NOT EXISTS idx_roas_source_campaign ON roas_reports(utm_source, utm_campaign);
28
+ CREATE INDEX IF NOT EXISTS idx_roas_content ON roas_reports(utm_content);
29
+ CREATE INDEX IF NOT EXISTS idx_roas_created_at ON roas_reports(created_at);
30
+ CREATE INDEX IF NOT EXISTS idx_roas_bid_rec ON roas_reports(bid_recommendation);
31
+
32
+ -- VIEW: últimos relatórios por campanha com evolução
33
+ CREATE VIEW IF NOT EXISTS v_roas_latest AS
34
+ SELECT
35
+ utm_source,
36
+ utm_campaign,
37
+ conversion_rate,
38
+ total_revenue,
39
+ revenue_per_lead,
40
+ ltv_accuracy,
41
+ top_qualification,
42
+ bid_recommendation,
43
+ created_at
44
+ FROM roas_reports r1
45
+ WHERE created_at = (
46
+ SELECT MAX(r2.created_at)
47
+ FROM roas_reports r2
48
+ WHERE r2.utm_source = r1.utm_source
49
+ AND r2.utm_campaign = r1.utm_campaign
50
+ )
51
+ ORDER BY total_revenue DESC;
52
+
53
+ -- ── Nurture Sequences — filas de mensagens por qualificação ──────────────────
54
+ CREATE TABLE IF NOT EXISTS nurture_sequences (
55
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
56
+ user_id TEXT, -- _cdp_uid
57
+ qualification TEXT NOT NULL, -- interessado | curioso
58
+ delay_days INTEGER NOT NULL, -- D+N após o quiz
59
+ channel TEXT NOT NULL, -- whatsapp | email
60
+ message TEXT NOT NULL, -- mensagem interpolada
61
+ subject TEXT, -- assunto (email only)
62
+ send_at TEXT NOT NULL, -- datetime de envio agendado
63
+ status TEXT NOT NULL DEFAULT 'pending', -- pending | sent | failed | cancelled
64
+ sent_at TEXT, -- quando foi realmente enviado
65
+ quiz_name TEXT, -- nome do quiz (para interpolação)
66
+ phone TEXT, -- telefone do lead
67
+ email TEXT, -- email do lead
68
+ first_name TEXT, -- nome (para personalização)
69
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
70
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
71
+ );
72
+
73
+ CREATE INDEX IF NOT EXISTS idx_nurture_user_id ON nurture_sequences(user_id);
74
+ CREATE INDEX IF NOT EXISTS idx_nurture_status ON nurture_sequences(status);
75
+ CREATE INDEX IF NOT EXISTS idx_nurture_send_at ON nurture_sequences(send_at);
76
+ CREATE INDEX IF NOT EXISTS idx_nurture_qual ON nurture_sequences(qualification);
77
+
78
+ -- VIEW: fila de envio pendente
79
+ CREATE VIEW IF NOT EXISTS v_nurture_pending AS
80
+ SELECT
81
+ id, user_id, qualification, delay_days, channel,
82
+ message, send_at, phone, email, first_name, quiz_name
83
+ FROM nurture_sequences
84
+ WHERE status = 'pending'
85
+ AND send_at <= datetime('now')
86
+ ORDER BY send_at ASC;
87
+
88
+ -- VIEW: taxa de envio por qualificação
89
+ CREATE VIEW IF NOT EXISTS v_nurture_stats AS
90
+ SELECT
91
+ qualification,
92
+ channel,
93
+ COUNT(*) AS total,
94
+ COUNT(CASE WHEN status = 'sent' THEN 1 END) AS sent,
95
+ COUNT(CASE WHEN status = 'failed' THEN 1 END) AS failed,
96
+ COUNT(CASE WHEN status = 'pending' THEN 1 END) AS pending,
97
+ ROUND(COUNT(CASE WHEN status = 'sent' THEN 1 END) * 100.0 / COUNT(*), 1) AS delivery_rate_pct
98
+ FROM nurture_sequences
99
+ GROUP BY qualification, channel;
100
+
101
+ -- ── Lookalike Seeds — histórico de audiences enviadas ao Meta ─────────────────
102
+ CREATE TABLE IF NOT EXISTS lookalike_seeds (
103
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
104
+ audience_id TEXT NOT NULL, -- META_AUDIENCE_ID
105
+ seed_type TEXT NOT NULL, -- 'buyer_confirmed' | 'high_intent' | 'quiz_comprador'
106
+ profiles_sent INTEGER NOT NULL DEFAULT 0,
107
+ profiles_received INTEGER, -- confirmado pela API Meta
108
+ period_days INTEGER NOT NULL DEFAULT 30,
109
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
110
+ );
111
+
112
+ CREATE INDEX IF NOT EXISTS idx_lookalike_seed_type ON lookalike_seeds(seed_type);
113
+ CREATE INDEX IF NOT EXISTS idx_lookalike_created_at ON lookalike_seeds(created_at);
@@ -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