cdp-edge 1.19.0 → 1.21.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/dist/commands/setup.js +326 -111
- package/package.json +1 -1
- package/server-edge-tracker/migrate-v7.sql +64 -0
- package/server-edge-tracker/modules/dispatch/meta.js +16 -0
- package/server-edge-tracker/modules/intelligence.js +120 -3
- package/server-edge-tracker/modules/ml/logistic.js +195 -0
- package/server-edge-tracker/modules/ml/ltv.js +100 -0
- package/server-edge-tracker/modules/ml/matchquality.js +176 -0
- package/server-edge-tracker/worker.js +380 -4
package/dist/commands/setup.js
CHANGED
|
@@ -1,138 +1,353 @@
|
|
|
1
|
-
|
|
2
|
-
* Setup Wizard
|
|
1
|
+
/**
|
|
2
|
+
* CDP Edge Setup Wizard — Guiado e não-técnico
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Coleta os dados do projeto do cliente e gera:
|
|
5
|
+
* • Comandos wrangler secret put prontos para copiar
|
|
6
|
+
* • URLs de webhook com domínio preenchido
|
|
7
|
+
* • Checklist de próximos passos para o agente
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
10
|
import inquirer from 'inquirer';
|
|
8
11
|
import chalk from 'chalk';
|
|
9
12
|
import ora from 'ora';
|
|
13
|
+
import { writeFileSync } from 'fs';
|
|
14
|
+
import { join } from 'path';
|
|
10
15
|
|
|
11
16
|
function printBanner() {
|
|
12
17
|
console.log('');
|
|
13
|
-
console.log(chalk.
|
|
14
|
-
console.log('');
|
|
15
|
-
console.log(chalk.cyan('
|
|
16
|
-
console.log(chalk.cyan('
|
|
17
|
-
console.log(chalk.cyan('██║ ██║ ██║██████╔╝ █████╗ ██║ ██║██║ ███╗█████╗ '));
|
|
18
|
-
console.log(chalk.cyan('██║ ██║ ██║██╔═══╝ ██╔══╝ ██║ ██║██║ ██║██╔══╝ '));
|
|
19
|
-
console.log(chalk.cyan('╚██████╗██████╔╝██║ ███████╗██████╔╝╚██████╔╝███████╗'));
|
|
20
|
-
console.log(chalk.cyan(' ╚═════╝╚═════╝ ╚═╝ ╚══════╝╚═════╝ ╚═════╝╚══════╝'));
|
|
21
|
-
console.log('');
|
|
22
|
-
console.log(chalk.gray(' Customer Data Platform on the Edge · Global Edge Tracking · v2.0.3'));
|
|
23
|
-
console.log('');
|
|
24
|
-
console.log(chalk.gray('═'.repeat(68)));
|
|
18
|
+
console.log(chalk.cyan(' ╔═══════════════════════════════════════╗'));
|
|
19
|
+
console.log(chalk.cyan(' ║') + chalk.white.bold(' CDP Edge — Setup Wizard ') + chalk.cyan('║'));
|
|
20
|
+
console.log(chalk.cyan(' ║') + chalk.gray(' Configuração Guiada de Tracking ') + chalk.cyan('║'));
|
|
21
|
+
console.log(chalk.cyan(' ╚═══════════════════════════════════════╝'));
|
|
25
22
|
console.log('');
|
|
26
23
|
}
|
|
27
24
|
|
|
25
|
+
function sep(label = '') {
|
|
26
|
+
if (label) {
|
|
27
|
+
console.log('\n' + chalk.gray('── ') + chalk.cyan.bold(label) + chalk.gray(' ' + '─'.repeat(Math.max(0, 40 - label.length))));
|
|
28
|
+
} else {
|
|
29
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
export async function runSetupWizard(dir = '.') {
|
|
29
34
|
printBanner();
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
console.log(chalk.cyan(' Vou te fazer algumas perguntas simples.'));
|
|
37
|
+
console.log(chalk.cyan(' Não precisa saber programar — é só preencher os dados.\n'));
|
|
38
|
+
console.log(chalk.gray(' Ao final, vou gerar todos os comandos prontos para instalar.\n'));
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
|
|
40
|
+
// ── PASSO 1: Dados do projeto ──────────────────────────────────────────────
|
|
41
|
+
sep('1. Dados do Projeto');
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
const projeto = await inquirer.prompt([
|
|
44
|
+
{
|
|
45
|
+
type: 'input',
|
|
46
|
+
name: 'domain',
|
|
47
|
+
message: 'Qual o domínio do site? (ex: meusite.com.br)',
|
|
48
|
+
validate: v => v.trim() ? true : 'Domínio é obrigatório',
|
|
49
|
+
filter: v => v.trim().replace(/^https?:\/\//, '').replace(/\/$/, ''),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: 'input',
|
|
53
|
+
name: 'projectName',
|
|
54
|
+
message: 'Nome do projeto (para identificação):',
|
|
55
|
+
default: answers => answers.domain.split('.')[0],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'list',
|
|
59
|
+
name: 'productType',
|
|
60
|
+
message: 'Que tipo de produto você vende?',
|
|
61
|
+
choices: [
|
|
62
|
+
{ name: 'Infoproduto / Curso Online', value: 'infoproduto' },
|
|
63
|
+
{ name: 'E-commerce físico', value: 'ecommerce' },
|
|
64
|
+
{ name: 'SaaS / Assinatura', value: 'saas' },
|
|
65
|
+
{ name: 'Serviço / Consultoria', value: 'servico' },
|
|
66
|
+
{ name: 'Lead Generation (captação)', value: 'leadgen' },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
]);
|
|
44
70
|
|
|
45
|
-
//
|
|
71
|
+
// ── PASSO 2: Plataforma de vendas ──────────────────────────────────────────
|
|
72
|
+
sep('2. Plataforma de Vendas / Checkout');
|
|
46
73
|
|
|
47
|
-
const
|
|
74
|
+
const { plataformas } = await inquirer.prompt([
|
|
48
75
|
{
|
|
49
|
-
type:
|
|
50
|
-
name:
|
|
51
|
-
message: '
|
|
76
|
+
type: 'checkbox',
|
|
77
|
+
name: 'plataformas',
|
|
78
|
+
message: 'Quais plataformas de checkout você usa? (espaço para marcar)',
|
|
52
79
|
choices: [
|
|
53
|
-
{ name: '
|
|
54
|
-
{ name: '
|
|
55
|
-
{ name: '
|
|
56
|
-
|
|
57
|
-
|
|
80
|
+
{ name: 'Hotmart', value: 'hotmart', checked: false },
|
|
81
|
+
{ name: 'Kiwify', value: 'kiwify', checked: false },
|
|
82
|
+
{ name: 'Ticto', value: 'ticto', checked: false },
|
|
83
|
+
{ name: 'Eduzz', value: 'eduzz', checked: false },
|
|
84
|
+
{ name: 'Stripe', value: 'stripe', checked: false },
|
|
85
|
+
{ name: 'Nenhuma (apenas formulário de captação)', value: 'none', checked: false },
|
|
86
|
+
],
|
|
87
|
+
validate: v => v.length > 0 ? true : 'Selecione pelo menos uma opção',
|
|
88
|
+
},
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
// ── PASSO 3: Plataformas de anúncios ───────────────────────────────────────
|
|
92
|
+
sep('3. Plataformas de Anúncios');
|
|
93
|
+
|
|
94
|
+
const { adsPlataformas } = await inquirer.prompt([
|
|
95
|
+
{
|
|
96
|
+
type: 'checkbox',
|
|
97
|
+
name: 'adsPlataformas',
|
|
98
|
+
message: 'Onde você faz anúncios? (espaço para marcar)',
|
|
99
|
+
choices: [
|
|
100
|
+
{ name: 'Meta Ads (Facebook / Instagram)', value: 'meta', checked: true },
|
|
101
|
+
{ name: 'Google Ads / YouTube', value: 'google', checked: false },
|
|
102
|
+
{ name: 'TikTok Ads', value: 'tiktok', checked: false },
|
|
103
|
+
{ name: 'Pinterest Ads', value: 'pinterest', checked: false },
|
|
104
|
+
{ name: 'LinkedIn Ads', value: 'linkedin', checked: false },
|
|
105
|
+
],
|
|
106
|
+
validate: v => v.length > 0 ? true : 'Selecione pelo menos uma plataforma',
|
|
107
|
+
},
|
|
58
108
|
]);
|
|
59
109
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
110
|
+
// ── PASSO 4: Credenciais Meta ──────────────────────────────────────────────
|
|
111
|
+
const creds = {};
|
|
112
|
+
|
|
113
|
+
if (adsPlataformas.includes('meta')) {
|
|
114
|
+
sep('4a. Credenciais — Meta Ads');
|
|
115
|
+
console.log(chalk.gray(' Onde encontrar: Meta Events Manager → Configurações → Pixel\n'));
|
|
116
|
+
|
|
117
|
+
const meta = await inquirer.prompt([
|
|
118
|
+
{
|
|
119
|
+
type: 'input',
|
|
120
|
+
name: 'pixelId',
|
|
121
|
+
message: 'Meta Pixel ID:',
|
|
122
|
+
validate: v => v.trim() ? true : 'Pixel ID é obrigatório',
|
|
123
|
+
filter: v => v.trim(),
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
type: 'password',
|
|
127
|
+
name: 'accessToken',
|
|
128
|
+
message: 'Meta Access Token (CAPI):',
|
|
129
|
+
mask: '*',
|
|
130
|
+
validate: v => v.trim() ? true : 'Access Token é obrigatório',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
type: 'input',
|
|
134
|
+
name: 'testCode',
|
|
135
|
+
message: 'Meta Test Event Code (opcional, para homologação):',
|
|
136
|
+
default: '',
|
|
137
|
+
filter: v => v.trim(),
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
creds.meta = meta;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (adsPlataformas.includes('google')) {
|
|
144
|
+
sep('4b. Credenciais — Google Analytics 4');
|
|
145
|
+
console.log(chalk.gray(' Onde encontrar: GA4 → Admin → Data Streams → Measurement Protocol\n'));
|
|
146
|
+
|
|
147
|
+
const google = await inquirer.prompt([
|
|
148
|
+
{
|
|
149
|
+
type: 'input',
|
|
150
|
+
name: 'measurementId',
|
|
151
|
+
message: 'GA4 Measurement ID (G-XXXXXXXX):',
|
|
152
|
+
validate: v => /^G-[A-Z0-9]+$/i.test(v.trim()) ? true : 'Formato: G-XXXXXXXX',
|
|
153
|
+
filter: v => v.trim().toUpperCase(),
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
type: 'password',
|
|
157
|
+
name: 'apiSecret',
|
|
158
|
+
message: 'GA4 API Secret:',
|
|
159
|
+
mask: '*',
|
|
160
|
+
validate: v => v.trim() ? true : 'API Secret é obrigatório',
|
|
161
|
+
},
|
|
162
|
+
]);
|
|
163
|
+
creds.google = google;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (adsPlataformas.includes('tiktok')) {
|
|
167
|
+
sep('4c. Credenciais — TikTok');
|
|
168
|
+
console.log(chalk.gray(' Onde encontrar: TikTok Events Manager → Pixel → Detalhes\n'));
|
|
169
|
+
|
|
170
|
+
const tiktok = await inquirer.prompt([
|
|
171
|
+
{
|
|
172
|
+
type: 'input',
|
|
173
|
+
name: 'pixelId',
|
|
174
|
+
message: 'TikTok Pixel ID:',
|
|
175
|
+
validate: v => v.trim() ? true : 'Pixel ID é obrigatório',
|
|
176
|
+
filter: v => v.trim(),
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
type: 'password',
|
|
180
|
+
name: 'accessToken',
|
|
181
|
+
message: 'TikTok Access Token:',
|
|
182
|
+
mask: '*',
|
|
183
|
+
validate: v => v.trim() ? true : 'Access Token é obrigatório',
|
|
184
|
+
},
|
|
185
|
+
]);
|
|
186
|
+
creds.tiktok = tiktok;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── PASSO 5: Alertas WhatsApp ──────────────────────────────────────────────
|
|
190
|
+
sep('5. Alertas WhatsApp (opcional)');
|
|
191
|
+
console.log(chalk.gray(' Receba alertas de erros e match quality direto no WhatsApp.\n'));
|
|
192
|
+
|
|
193
|
+
const { wantsWhatsapp } = await inquirer.prompt([
|
|
194
|
+
{
|
|
195
|
+
type: 'confirm',
|
|
196
|
+
name: 'wantsWhatsapp',
|
|
197
|
+
message: 'Deseja ativar alertas via WhatsApp?',
|
|
198
|
+
default: true,
|
|
199
|
+
},
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
let whatsapp = {};
|
|
203
|
+
if (wantsWhatsapp) {
|
|
204
|
+
console.log(chalk.gray('\n Como configurar: acesse callmebot.com e adicione o bot no WhatsApp.'));
|
|
205
|
+
console.log(chalk.gray(' Envie "I allow callmebot to send me messages" para +34 644 35 78 48\n'));
|
|
206
|
+
|
|
207
|
+
whatsapp = await inquirer.prompt([
|
|
208
|
+
{
|
|
209
|
+
type: 'input',
|
|
210
|
+
name: 'phone',
|
|
211
|
+
message: 'Seu número com DDI (ex: +5511999999999):',
|
|
212
|
+
validate: v => /^\+\d{10,15}$/.test(v.trim()) ? true : 'Formato: +5511999999999',
|
|
213
|
+
filter: v => v.trim(),
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
type: 'password',
|
|
217
|
+
name: 'apiKey',
|
|
218
|
+
message: 'CallMeBot API Key:',
|
|
219
|
+
mask: '*',
|
|
220
|
+
validate: v => v.trim() ? true : 'API Key é obrigatória',
|
|
221
|
+
},
|
|
222
|
+
]);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── GERAÇÃO DOS OUTPUTS ────────────────────────────────────────────────────
|
|
226
|
+
const spinner = ora('Gerando configurações...').start();
|
|
227
|
+
await sleep(600);
|
|
228
|
+
spinner.succeed('Configurações geradas!');
|
|
229
|
+
|
|
230
|
+
const domain = projeto.domain;
|
|
231
|
+
|
|
232
|
+
// Monta lista de secrets
|
|
233
|
+
const secrets = [];
|
|
234
|
+
|
|
235
|
+
if (creds.meta) {
|
|
236
|
+
secrets.push({ key: 'META_PIXEL_ID', value: creds.meta.pixelId, platform: 'Meta' });
|
|
237
|
+
secrets.push({ key: 'META_ACCESS_TOKEN', value: creds.meta.accessToken, platform: 'Meta' });
|
|
238
|
+
if (creds.meta.testCode) {
|
|
239
|
+
secrets.push({ key: 'META_TEST_CODE', value: creds.meta.testCode, platform: 'Meta (homologação)' });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (creds.google) {
|
|
244
|
+
secrets.push({ key: 'GA4_MEASUREMENT_ID', value: creds.google.measurementId, platform: 'Google' });
|
|
245
|
+
secrets.push({ key: 'GA4_API_SECRET', value: creds.google.apiSecret, platform: 'Google' });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (creds.tiktok) {
|
|
249
|
+
secrets.push({ key: 'TIKTOK_PIXEL_ID', value: creds.tiktok.pixelId, platform: 'TikTok' });
|
|
250
|
+
secrets.push({ key: 'TIKTOK_ACCESS_TOKEN', value: creds.tiktok.accessToken, platform: 'TikTok' });
|
|
135
251
|
}
|
|
252
|
+
|
|
253
|
+
if (whatsapp.phone) {
|
|
254
|
+
secrets.push({ key: 'CALLMEBOT_PHONE', value: whatsapp.phone, platform: 'WhatsApp' });
|
|
255
|
+
secrets.push({ key: 'CALLMEBOT_API_KEY', value: whatsapp.apiKey, platform: 'WhatsApp' });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
secrets.push({ key: 'SITE_DOMAIN', value: domain, platform: 'Worker' });
|
|
259
|
+
|
|
260
|
+
// Webhooks
|
|
261
|
+
const webhookUrls = {};
|
|
262
|
+
const webhookSecrets = [];
|
|
263
|
+
if (plataformas.includes('hotmart')) {
|
|
264
|
+
webhookUrls.hotmart = `https://${domain}/webhook/hotmart`;
|
|
265
|
+
webhookSecrets.push({ key: 'WEBHOOK_SECRET_HOTMART', platform: 'Hotmart', note: 'Gere uma senha forte qualquer' });
|
|
266
|
+
}
|
|
267
|
+
if (plataformas.includes('kiwify')) {
|
|
268
|
+
webhookUrls.kiwify = `https://${domain}/webhook/kiwify`;
|
|
269
|
+
webhookSecrets.push({ key: 'WEBHOOK_SECRET_KIWIFY', platform: 'Kiwify', note: 'Gere uma senha forte qualquer' });
|
|
270
|
+
}
|
|
271
|
+
if (plataformas.includes('ticto')) {
|
|
272
|
+
webhookUrls.ticto = `https://${domain}/webhook/ticto`;
|
|
273
|
+
webhookSecrets.push({ key: 'WEBHOOK_SECRET_TICTO', platform: 'Ticto', note: 'Chave HMAC configurada no painel Ticto' });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── EXIBIR RESULTADO ───────────────────────────────────────────────────────
|
|
277
|
+
console.log('\n');
|
|
278
|
+
sep('CONFIGURAÇÃO PRONTA');
|
|
279
|
+
|
|
280
|
+
console.log(chalk.green.bold('\n Projeto: ') + chalk.white(projeto.projectName));
|
|
281
|
+
console.log(chalk.green.bold(' Domínio: ') + chalk.white(domain));
|
|
282
|
+
console.log(chalk.green.bold(' Produto: ') + chalk.white(projeto.productType));
|
|
283
|
+
console.log(chalk.green.bold(' Ads: ') + chalk.white(adsPlataformas.join(', ')));
|
|
284
|
+
|
|
285
|
+
// Comandos de secrets
|
|
286
|
+
sep('Comandos para configurar os secrets');
|
|
287
|
+
console.log(chalk.gray(' Execute dentro da pasta server-edge-tracker:\n'));
|
|
288
|
+
|
|
289
|
+
for (const s of secrets) {
|
|
290
|
+
const masked = s.value.length > 4 ? s.value.slice(0, 4) + '*'.repeat(Math.min(s.value.length - 4, 8)) : '****';
|
|
291
|
+
console.log(chalk.cyan(` wrangler secret put ${s.key}`));
|
|
292
|
+
console.log(chalk.gray(` # ${s.platform} — valor: ${masked}\n`));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (webhookSecrets.length > 0) {
|
|
296
|
+
console.log(chalk.yellow(' # Secrets de webhook (gere senhas e configure abaixo + no painel da plataforma):'));
|
|
297
|
+
for (const ws of webhookSecrets) {
|
|
298
|
+
console.log(chalk.cyan(` wrangler secret put ${ws.key}`));
|
|
299
|
+
console.log(chalk.gray(` # ${ws.platform}: ${ws.note}\n`));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// URLs de webhook
|
|
304
|
+
if (Object.keys(webhookUrls).length > 0) {
|
|
305
|
+
sep('URLs de Webhook para configurar nas plataformas');
|
|
306
|
+
for (const [plat, url] of Object.entries(webhookUrls)) {
|
|
307
|
+
console.log(chalk.green.bold(` ${plat.charAt(0).toUpperCase() + plat.slice(1)}:`));
|
|
308
|
+
console.log(chalk.white(` ${url}\n`));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Checklist
|
|
313
|
+
sep('Próximos passos');
|
|
314
|
+
const steps = [
|
|
315
|
+
'Executar todos os wrangler secret put acima',
|
|
316
|
+
'Executar as migrations D1 (schema.sql → migrate-v7.sql)',
|
|
317
|
+
'Executar: wrangler deploy',
|
|
318
|
+
...(Object.keys(webhookUrls).length > 0 ? ['Configurar as URLs de webhook nas plataformas de venda'] : []),
|
|
319
|
+
'Configurar Worker Route no Cloudflare: ' + domain + '/*',
|
|
320
|
+
'Testar o endpoint: https://' + domain + '/health',
|
|
321
|
+
...(adsPlataformas.includes('meta') && creds.meta?.testCode ? ['Testar eventos no Meta Events Manager com o Test Event Code'] : []),
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
steps.forEach((step, i) => {
|
|
325
|
+
console.log(chalk.cyan(` [${i + 1}] `) + chalk.white(step));
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Salvar config (sem tokens reais)
|
|
329
|
+
const configOutput = {
|
|
330
|
+
generatedAt: new Date().toISOString(),
|
|
331
|
+
domain,
|
|
332
|
+
projectName: projeto.projectName,
|
|
333
|
+
productType: projeto.productType,
|
|
334
|
+
adsPlataformas,
|
|
335
|
+
checkoutPlataformas: plataformas,
|
|
336
|
+
webhookUrls,
|
|
337
|
+
secretKeys: secrets.map(s => s.key),
|
|
338
|
+
whatsappAlertsEnabled: !!whatsapp.phone,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const outputPath = join(dir, 'cdp-edge-setup.json');
|
|
342
|
+
try {
|
|
343
|
+
writeFileSync(outputPath, JSON.stringify(configOutput, null, 2), 'utf8');
|
|
344
|
+
console.log(chalk.gray(`\n Configuração salva em: ${outputPath} (sem tokens)\n`));
|
|
345
|
+
} catch {
|
|
346
|
+
// não crítico se não conseguir salvar
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
sep();
|
|
350
|
+
console.log(chalk.green.bold('\n Setup concluído! Passe os comandos acima para o agente instalar.\n'));
|
|
136
351
|
}
|
|
137
352
|
|
|
138
353
|
function sleep(ms) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
-- CDP Edge — Migration v7
|
|
2
|
+
-- Tabelas para: LTV Model (regressão logística) + Match Quality Log
|
|
3
|
+
-- Idempotente: todos os CREATE usam IF NOT EXISTS
|
|
4
|
+
|
|
5
|
+
-- ── LTV Model Weights ─────────────────────────────────────────────────────────
|
|
6
|
+
-- Armazena os pesos treinados pelo cron semanal de regressão logística.
|
|
7
|
+
-- O Worker carrega via KV (TTL 7 dias) — query D1 apenas se KV miss.
|
|
8
|
+
CREATE TABLE IF NOT EXISTS ltv_model_weights (
|
|
9
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
trained_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
11
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
12
|
+
is_active INTEGER NOT NULL DEFAULT 0,
|
|
13
|
+
sample_size INTEGER NOT NULL DEFAULT 0,
|
|
14
|
+
positive_rate REAL, -- % de samples com label=1 (compra)
|
|
15
|
+
accuracy REAL, -- acurácia no conjunto de treino
|
|
16
|
+
weights_json TEXT NOT NULL -- JSON: {"bias": n, "features": {"utm_fb": w, ...}}
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_ltv_weights_active ON ltv_model_weights(is_active);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_ltv_weights_trained ON ltv_model_weights(trained_at DESC);
|
|
21
|
+
|
|
22
|
+
-- ── Match Quality Log ─────────────────────────────────────────────────────────
|
|
23
|
+
-- Registra a qualidade dos dados enviados ao Meta CAPI por evento.
|
|
24
|
+
-- Sem PII — apenas flags booleanos.
|
|
25
|
+
CREATE TABLE IF NOT EXISTS match_quality_log (
|
|
26
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
27
|
+
logged_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
28
|
+
event_name TEXT NOT NULL,
|
|
29
|
+
has_email INTEGER NOT NULL DEFAULT 0,
|
|
30
|
+
has_phone INTEGER NOT NULL DEFAULT 0,
|
|
31
|
+
has_fbp INTEGER NOT NULL DEFAULT 0,
|
|
32
|
+
has_fbc INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
has_external_id INTEGER NOT NULL DEFAULT 0,
|
|
34
|
+
was_email_recovered INTEGER NOT NULL DEFAULT 0, -- recuperado do Identity Graph
|
|
35
|
+
was_utm_restored INTEGER NOT NULL DEFAULT 0 -- via UTM Resurrection
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_mq_logged ON match_quality_log(logged_at DESC);
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_mq_event ON match_quality_log(event_name, logged_at DESC);
|
|
40
|
+
|
|
41
|
+
-- ── View: Match Quality Dashboard (últimas 24h) ───────────────────────────────
|
|
42
|
+
CREATE VIEW IF NOT EXISTS v_match_quality_24h AS
|
|
43
|
+
SELECT
|
|
44
|
+
COUNT(*) AS total_events,
|
|
45
|
+
ROUND(AVG(has_email) * 100, 1) AS email_rate_pct,
|
|
46
|
+
ROUND(AVG(has_phone) * 100, 1) AS phone_rate_pct,
|
|
47
|
+
ROUND(AVG(has_fbp) * 100, 1) AS fbp_rate_pct,
|
|
48
|
+
ROUND(AVG(has_fbc) * 100, 1) AS fbc_rate_pct,
|
|
49
|
+
ROUND(AVG(has_external_id) * 100, 1) AS ext_id_rate_pct,
|
|
50
|
+
ROUND(AVG(was_email_recovered) * 100, 1) AS email_recovered_pct,
|
|
51
|
+
ROUND(AVG(was_utm_restored) * 100, 1) AS utm_restored_pct,
|
|
52
|
+
-- Score composto: email(40%) + fbp(30%) + phone(20%) + fbc(10%)
|
|
53
|
+
ROUND((AVG(has_email)*0.4 + AVG(has_fbp)*0.3 + AVG(has_phone)*0.2 + AVG(has_fbc)*0.1) * 100, 1) AS composite_score_pct
|
|
54
|
+
FROM match_quality_log
|
|
55
|
+
WHERE logged_at >= datetime('now', '-24 hours');
|
|
56
|
+
|
|
57
|
+
-- ── ALTER: colunas de auditoria em ltv_ab_tests ───────────────────────────────
|
|
58
|
+
-- Adiciona auto_decided_at se ainda não existir (SQLite não tem IF NOT EXISTS em ALTER)
|
|
59
|
+
-- Protegido por trigger-style: INSERT OR IGNORE não se aplica a ALTER, então usamos
|
|
60
|
+
-- um approach de criação de tabela auxiliar que faz nada se a coluna já existir.
|
|
61
|
+
-- A abordagem mais segura no Cloudflare D1 é tentar e ignorar o erro no app.
|
|
62
|
+
-- Aqui deixamos comentado — o worker.js já lida com campos opcionais:
|
|
63
|
+
-- ALTER TABLE ltv_ab_tests ADD COLUMN auto_decided_at TEXT;
|
|
64
|
+
-- ALTER TABLE ltv_ab_tests ADD COLUMN auto_decided_reason TEXT;
|
|
@@ -5,8 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
import { sha256, normalizePhone, normalizeCity } from '../utils.js';
|
|
7
7
|
import { logApiFailure } from '../db.js';
|
|
8
|
+
import { logMatchQuality, autoEnrichPayload } from '../ml/matchquality.js';
|
|
8
9
|
|
|
9
10
|
export async function sendMetaCapi(env, eventName, payload, request, ctx) {
|
|
11
|
+
// Auto-enriquecer payload com dados do Identity Graph antes do envio
|
|
12
|
+
let recovered = { email: false, utm: false };
|
|
13
|
+
if (env.DB && payload) {
|
|
14
|
+
const enriched = await autoEnrichPayload(env, payload);
|
|
15
|
+
payload = enriched.payload;
|
|
16
|
+
recovered = enriched.recovered;
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
const {
|
|
11
20
|
email, phone, firstName, lastName,
|
|
12
21
|
city, state, country,
|
|
@@ -69,6 +78,13 @@ export async function sendMetaCapi(env, eventName, payload, request, ctx) {
|
|
|
69
78
|
requestBody.test_event_code = env.META_TEST_CODE;
|
|
70
79
|
}
|
|
71
80
|
|
|
81
|
+
// Logar match quality em background (não bloqueia dispatch)
|
|
82
|
+
if (env.DB && ctx) {
|
|
83
|
+
ctx.waitUntil(logMatchQuality(env.DB, eventName, payload, recovered));
|
|
84
|
+
} else if (env.DB) {
|
|
85
|
+
logMatchQuality(env.DB, eventName, payload, recovered).catch(() => {});
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
const endpoint = `https://graph.facebook.com/v22.0/${env.META_PIXEL_ID}/events`;
|
|
73
89
|
|
|
74
90
|
try {
|