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
|
@@ -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);
|
package/templates/quiz-funnel.md
CHANGED
|
@@ -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
|