cdp-edge 2.0.2 → 2.0.4

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 (27) hide show
  1. package/README.md +325 -308
  2. package/contracts/agent-versions.json +364 -0
  3. package/dist/commands/install.js +1 -1
  4. package/dist/commands/setup.js +1 -1
  5. package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +2 -2
  6. package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +14 -20
  7. package/extracted-skill/tracking-events-generator/agents/premium-tracking-intelligence-agent.md +4 -4
  8. package/extracted-skill/tracking-events-generator/agents/server-tracking.md +13 -13
  9. package/extracted-skill/tracking-events-generator/agents/spotify-agent.md +1 -1
  10. package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +4 -4
  11. package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +3 -3
  12. package/package.json +81 -76
  13. package/server-edge-tracker/index.js +780 -0
  14. package/server-edge-tracker/modules/db.js +531 -0
  15. package/server-edge-tracker/modules/dispatch/ga4.js +65 -0
  16. package/server-edge-tracker/modules/dispatch/meta.js +103 -0
  17. package/server-edge-tracker/modules/dispatch/platforms.js +237 -0
  18. package/server-edge-tracker/modules/dispatch/tiktok.js +100 -0
  19. package/server-edge-tracker/modules/dispatch/whatsapp.js +233 -0
  20. package/server-edge-tracker/modules/intelligence.js +204 -0
  21. package/server-edge-tracker/modules/ml/bidding.js +245 -0
  22. package/server-edge-tracker/modules/ml/fraud.js +301 -0
  23. package/server-edge-tracker/modules/ml/ltv.js +320 -0
  24. package/server-edge-tracker/modules/ml/segmentation.js +316 -0
  25. package/server-edge-tracker/modules/utils.js +89 -0
  26. package/server-edge-tracker/schema-indexes.sql +67 -0
  27. package/server-edge-tracker/wrangler.toml +2 -0
@@ -0,0 +1,316 @@
1
+ /**
2
+ * CDP Edge — ML Clustering (Fase 1)
3
+ * Handlers das rotas /api/segmentation/*
4
+ */
5
+
6
+ import { tryParseJson } from '../utils.js';
7
+
8
+ // ── POST /api/segmentation/cluster ────────────────────────────────────────────
9
+ export async function handleSegmentationCluster(env, request, headers) {
10
+ if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
11
+ if (!env.AI) return new Response(JSON.stringify({ error: 'Workers AI não configurado (verifique binding AI no wrangler.toml)' }), { status: 503, headers });
12
+
13
+ const url = new URL(request.url);
14
+ const algorithm = url.searchParams.get('algorithm') || 'kmeans';
15
+ const nClusters = Math.min(10, Math.max(3, parseInt(url.searchParams.get('n_clusters') || '5')));
16
+ const clientVertical = url.searchParams.get('vertical') || 'general';
17
+ const forceRecluster = url.searchParams.get('force') === 'true';
18
+
19
+ if (!['kmeans', 'dbscan', 'hierarchical'].includes(algorithm)) {
20
+ return new Response(JSON.stringify({ error: 'algorithm deve ser: kmeans, dbscan ou hierarchical', received: algorithm }), { status: 400, headers });
21
+ }
22
+
23
+ try {
24
+ // 1. Cluster recente? Evitar re-clustering desnecessário (< 7 dias)
25
+ if (!forceRecluster) {
26
+ const existing = await env.DB.prepare(`
27
+ SELECT id, created_at, cluster_name FROM ml_segments
28
+ WHERE clustering_algorithm = ? AND is_active = 1 AND client_vertical = ?
29
+ ORDER BY created_at DESC LIMIT 1
30
+ `).bind(algorithm, clientVertical).first();
31
+
32
+ if (existing) {
33
+ const ageDays = (Date.now() - new Date(existing.created_at).getTime()) / (1000 * 60 * 60 * 24);
34
+ if (ageDays < 7) {
35
+ return new Response(JSON.stringify({
36
+ success: true, message: 'Cluster existente ainda válido (< 7 dias). Use ?force=true para re-clustering.',
37
+ cluster_id: existing.id, cluster_name: existing.cluster_name,
38
+ age_days: Math.round(ageDays * 10) / 10, use_existing: true,
39
+ }), { status: 200, headers });
40
+ }
41
+ }
42
+ }
43
+
44
+ // 2. Extrair leads históricos do D1 (últimos 6 meses, excluindo bots confirmados)
45
+ const leadsRes = await env.DB.prepare(`
46
+ SELECT id, predicted_ltv_class, engagement_score, intention_level,
47
+ country, state, utm_source, utm_medium, bot_score,
48
+ CAST(strftime('%H', created_at) AS INTEGER) AS hour_of_day,
49
+ CAST(julianday('now') - julianday(created_at) AS INTEGER) AS days_since_lead,
50
+ CASE WHEN strftime('%w', created_at) IN ('0','6') THEN 1 ELSE 0 END AS is_weekend
51
+ FROM leads
52
+ WHERE created_at >= datetime('now', '-6 months')
53
+ AND (bot_score IS NULL OR bot_score < 2)
54
+ ORDER BY RANDOM()
55
+ LIMIT 2000
56
+ `).all();
57
+
58
+ const leads = leadsRes.results || [];
59
+
60
+ if (leads.length < 50) {
61
+ return new Response(JSON.stringify({
62
+ error: 'Dados insuficientes para clustering. Mínimo: 50 leads nos últimos 6 meses.',
63
+ leads_found: leads.length, required: 50,
64
+ }), { status: 400, headers });
65
+ }
66
+
67
+ // 3. Feature Engineering — normalização 0–1
68
+ const features = leads.map(l => ({
69
+ id: l.id,
70
+ ltv: l.predicted_ltv_class === 'High' ? 1 : (l.predicted_ltv_class === 'Medium' ? 0.5 : 0),
71
+ engagement: Math.min((l.engagement_score || 0) / 100, 1),
72
+ intention: l.intention_level === 'comprador' || l.intention_level === 'high_intent' ? 1
73
+ : l.intention_level === 'interessado' ? 0.6
74
+ : l.intention_level === 'curioso' ? 0.3 : 0,
75
+ recency: Math.max(0, 1 - (l.days_since_lead || 0) / 180),
76
+ hour: (l.hour_of_day || 12) / 23,
77
+ is_weekend: l.is_weekend || 0,
78
+ is_br: l.country === 'BR' ? 1 : 0,
79
+ is_paid: ['facebook','google','tiktok','instagram','youtube'].includes((l.utm_source || '').toLowerCase()) ? 1 : 0,
80
+ }));
81
+
82
+ // 4. Prompt para Workers AI
83
+ const sampleSize = Math.min(features.length, 100);
84
+ const sample = features.slice(0, sampleSize);
85
+
86
+ const clusteringPrompt =
87
+ `You are a customer segmentation ML expert. Perform ${algorithm} clustering on ${sampleSize} customers into ${nClusters} segments.
88
+
89
+ Customer features (all normalized 0-1):
90
+ - ltv: predicted lifetime value (0=Low, 0.5=Medium, 1=High)
91
+ - engagement: browser engagement score
92
+ - intention: purchase intention (0=none, 0.3=curious, 0.6=interested, 1=buyer)
93
+ - recency: lead recency (1=today, 0=6 months ago)
94
+ - hour: conversion hour of day
95
+ - is_weekend: converted on weekend (0/1)
96
+ - is_br: lead from Brazil (0/1)
97
+ - is_paid: from paid traffic channel (0/1)
98
+
99
+ Data (${sampleSize} customers): ${JSON.stringify(sample.slice(0, 50))}
100
+
101
+ Return ONLY valid JSON, zero explanation:
102
+ {
103
+ "clusters": [
104
+ {
105
+ "cluster_id": 0,
106
+ "name": "[Nome Descritivo em Português]",
107
+ "size": ${Math.round(sampleSize / nClusters)},
108
+ "percentage": ${Math.round(100 / nClusters)},
109
+ "characteristics": {
110
+ "avg_ltv_class": 0.5,
111
+ "avg_behavior_score": 0.5,
112
+ "avg_engagement_score": 0.5,
113
+ "avg_intention_level": 0.5,
114
+ "avg_days_since_lead": 30,
115
+ "dominant_countries": ["BR"],
116
+ "dominant_states": ["SP", "RJ"],
117
+ "dominant_utm_sources": ["facebook"],
118
+ "top_features": ["ltv", "engagement"]
119
+ },
120
+ "centroid": { "ltv": 0.5, "engagement": 0.5, "intention": 0.5 },
121
+ "action_recommendation": "[Recomendação de campanha específica para este segmento]"
122
+ }
123
+ ],
124
+ "silhouette_score": 0.65,
125
+ "total_processed": ${sampleSize}
126
+ }`;
127
+
128
+ // 5. Workers AI
129
+ const startTime = Date.now();
130
+ const aiRes = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
131
+ messages: [{ role: 'user', content: clusteringPrompt }],
132
+ max_tokens: 2000,
133
+ });
134
+ const duration = Date.now() - startTime;
135
+
136
+ if (!aiRes?.response) throw new Error('Workers AI não retornou resposta');
137
+
138
+ const jsonMatch = aiRes.response.trim().match(/\{[\s\S]*\}/);
139
+ if (!jsonMatch) throw new Error('Resposta do Workers AI não contém JSON válido');
140
+ const mlResult = JSON.parse(jsonMatch[0]);
141
+
142
+ if (!Array.isArray(mlResult.clusters) || mlResult.clusters.length === 0) {
143
+ throw new Error('Workers AI não retornou clusters válidos');
144
+ }
145
+
146
+ // 6. Inativar clusters anteriores
147
+ await env.DB.prepare(`UPDATE ml_segments SET is_active = 0 WHERE clustering_algorithm = ? AND client_vertical = ? AND is_active = 1`).bind(algorithm, clientVertical).run();
148
+
149
+ // 7. Persistir novos clusters
150
+ const now = new Date().toISOString();
151
+ for (const cluster of mlResult.clusters) {
152
+ const ch = cluster.characteristics || {};
153
+ await env.DB.prepare(`
154
+ INSERT INTO ml_segments (
155
+ cluster_id, cluster_name, clustering_algorithm, client_vertical,
156
+ size, percentage, avg_ltv_class, avg_behavior_score, avg_engagement_score,
157
+ avg_intention_level, avg_days_since_lead,
158
+ dominant_countries, dominant_states, dominant_utm_sources, dominant_features,
159
+ silhouette_score, action_recommendations, bid_recommendations, campaign_recommendations,
160
+ is_active, created_at, updated_at
161
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?)
162
+ `).bind(
163
+ cluster.cluster_id || 0, cluster.name || `Segmento ${cluster.cluster_id}`, algorithm, clientVertical,
164
+ cluster.size || 0, cluster.percentage || 0,
165
+ ch.avg_ltv_class || 0, ch.avg_behavior_score || 0, ch.avg_engagement_score || 0,
166
+ ch.avg_intention_level || 0, ch.avg_days_since_lead || 0,
167
+ JSON.stringify(ch.dominant_countries || ['BR']), JSON.stringify(ch.dominant_states || []),
168
+ JSON.stringify(ch.dominant_utm_sources || []), JSON.stringify(ch.top_features || []),
169
+ mlResult.silhouette_score || 0,
170
+ JSON.stringify([cluster.action_recommendation || '']), JSON.stringify([]), JSON.stringify([]),
171
+ now, now,
172
+ ).run();
173
+ }
174
+
175
+ // 8. Log no histórico
176
+ try {
177
+ await env.DB.prepare(`
178
+ INSERT INTO ml_clustering_history (
179
+ clustering_id, started_at, completed_at, algorithm,
180
+ n_leads_processed, n_clusters_created, total_duration_ms,
181
+ workers_ai_neurons_used, status, parameters, results_summary
182
+ ) VALUES (0, ?, datetime('now'), ?, ?, ?, ?, ?, 'completed', ?, ?)
183
+ `).bind(
184
+ new Date(startTime).toISOString(), algorithm, leads.length, mlResult.clusters.length,
185
+ duration, Math.ceil(duration * 0.01),
186
+ JSON.stringify({ algorithm, n_clusters: nClusters, vertical: clientVertical }),
187
+ JSON.stringify({ clusters: mlResult.clusters.length, silhouette: mlResult.silhouette_score }),
188
+ ).run();
189
+ } catch (e) { console.error('[Segmentation] history log error:', e.message); }
190
+
191
+ return new Response(JSON.stringify({
192
+ success: true, algorithm, n_clusters: mlResult.clusters.length, client_vertical: clientVertical,
193
+ leads_analyzed: leads.length, duration_ms: duration, silhouette_score: mlResult.silhouette_score || null,
194
+ clusters: mlResult.clusters, generated_at: now,
195
+ }), { status: 200, headers });
196
+
197
+ } catch (err) {
198
+ console.error('[Segmentation] cluster error:', err.message);
199
+ try {
200
+ if (env.DB) {
201
+ await env.DB.prepare(`
202
+ INSERT INTO ml_clustering_history (clustering_id, started_at, algorithm, n_leads_processed, n_clusters_created, total_duration_ms, workers_ai_neurons_used, status, error_message, parameters, results_summary)
203
+ VALUES (0, datetime('now'), ?, 0, 0, 0, 0, 'failed', ?, ?, '{}')
204
+ `).bind(algorithm, err.message, JSON.stringify({ algorithm, n_clusters: nClusters })).run();
205
+ }
206
+ } catch { /* não bloquear a resposta de erro */ }
207
+
208
+ return new Response(JSON.stringify({ error: 'Erro ao executar clustering', message: err.message }), { status: 500, headers });
209
+ }
210
+ }
211
+
212
+ // ── GET /api/segmentation/list ────────────────────────────────────────────────
213
+ export async function handleSegmentationList(env, request, headers) {
214
+ if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
215
+
216
+ const url = new URL(request.url);
217
+ const algorithm = url.searchParams.get('algorithm') || null;
218
+ const vertical = url.searchParams.get('vertical') || null;
219
+
220
+ try {
221
+ const conditions = ['is_active = 1'];
222
+ const bindings = [];
223
+ if (algorithm) { conditions.push('clustering_algorithm = ?'); bindings.push(algorithm); }
224
+ if (vertical) { conditions.push('client_vertical = ?'); bindings.push(vertical); }
225
+
226
+ const result = await env.DB.prepare(`
227
+ SELECT id, cluster_id, cluster_name, clustering_algorithm, client_vertical,
228
+ size, percentage, avg_ltv_class, avg_behavior_score, avg_engagement_score,
229
+ avg_intention_level, avg_days_since_lead, silhouette_score,
230
+ dominant_countries, dominant_states, dominant_utm_sources, dominant_features,
231
+ action_recommendations, bid_recommendations, campaign_recommendations,
232
+ is_active, created_at, updated_at
233
+ FROM ml_segments
234
+ WHERE ${conditions.join(' AND ')}
235
+ ORDER BY created_at DESC
236
+ LIMIT 50
237
+ `).bind(...bindings).all();
238
+
239
+ const segments = (result.results || []).map(s => ({
240
+ ...s,
241
+ dominant_countries: tryParseJson(s.dominant_countries, []),
242
+ dominant_states: tryParseJson(s.dominant_states, []),
243
+ dominant_utm_sources: tryParseJson(s.dominant_utm_sources, []),
244
+ dominant_features: tryParseJson(s.dominant_features, []),
245
+ action_recommendations: tryParseJson(s.action_recommendations, []),
246
+ bid_recommendations: tryParseJson(s.bid_recommendations, []),
247
+ campaign_recommendations: tryParseJson(s.campaign_recommendations, []),
248
+ }));
249
+
250
+ return new Response(JSON.stringify({ success: true, total: segments.length, segments }), { status: 200, headers });
251
+ } catch (err) {
252
+ console.error('[Segmentation] list error:', err.message);
253
+ return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
254
+ }
255
+ }
256
+
257
+ // ── GET /api/segmentation/outliers ───────────────────────────────────────────
258
+ export async function handleSegmentationOutliers(env, request, headers) {
259
+ if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
260
+
261
+ const url = new URL(request.url);
262
+ const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 200);
263
+ const days = parseInt(url.searchParams.get('days') || '30');
264
+
265
+ try {
266
+ const result = await env.DB.prepare(`
267
+ SELECT msm.lead_id, msm.cluster_id, msm.confidence, msm.is_outlier, msm.outlier_reason, msm.assigned_at,
268
+ l.email, l.phone, l.country, l.state, l.city, l.utm_source, l.bot_score, l.engagement_score, l.intention_level, l.created_at AS lead_created_at
269
+ FROM ml_segment_members msm
270
+ LEFT JOIN leads l ON CAST(msm.lead_id AS INTEGER) = l.id
271
+ WHERE msm.is_outlier = 1 AND msm.assigned_at >= datetime('now', '-' || ? || ' days')
272
+ ORDER BY msm.assigned_at DESC
273
+ LIMIT ?
274
+ `).bind(days, limit).all();
275
+
276
+ return new Response(JSON.stringify({ success: true, total: (result.results || []).length, period_days: days, outliers: result.results || [] }), { status: 200, headers });
277
+ } catch (err) {
278
+ console.error('[Segmentation] outliers error:', err.message);
279
+ return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
280
+ }
281
+ }
282
+
283
+ // ── PUT /api/segmentation/update ─────────────────────────────────────────────
284
+ export async function handleSegmentationUpdate(env, request, headers) {
285
+ if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
286
+
287
+ let body;
288
+ try { body = await request.json(); }
289
+ catch { return new Response(JSON.stringify({ error: 'JSON inválido no body da requisição' }), { status: 400, headers }); }
290
+
291
+ const { cluster_id, action_recommendations, bid_recommendations, campaign_recommendations } = body;
292
+ if (cluster_id === undefined || cluster_id === null) {
293
+ return new Response(JSON.stringify({ error: 'cluster_id é obrigatório' }), { status: 400, headers });
294
+ }
295
+
296
+ try {
297
+ const sets = [];
298
+ const bindings = [];
299
+ if (action_recommendations !== undefined) { sets.push('action_recommendations = ?'); bindings.push(JSON.stringify(action_recommendations)); }
300
+ if (bid_recommendations !== undefined) { sets.push('bid_recommendations = ?'); bindings.push(JSON.stringify(bid_recommendations)); }
301
+ if (campaign_recommendations !== undefined) { sets.push('campaign_recommendations = ?'); bindings.push(JSON.stringify(campaign_recommendations)); }
302
+
303
+ if (sets.length === 0) {
304
+ return new Response(JSON.stringify({ error: 'Nenhum campo válido para atualizar (action_recommendations, bid_recommendations, campaign_recommendations)' }), { status: 400, headers });
305
+ }
306
+
307
+ sets.push("updated_at = datetime('now')");
308
+ bindings.push(cluster_id);
309
+
310
+ await env.DB.prepare(`UPDATE ml_segments SET ${sets.join(', ')} WHERE id = ?`).bind(...bindings).run();
311
+ return new Response(JSON.stringify({ success: true, cluster_id, fields_updated: sets.length - 1 }), { status: 200, headers });
312
+ } catch (err) {
313
+ console.error('[Segmentation] update error:', err.message);
314
+ return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
315
+ }
316
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * CDP Edge — Utilities
3
+ * Funções puras sem dependências externas.
4
+ * Importadas por todos os outros módulos.
5
+ */
6
+
7
+ // ── CORS ──────────────────────────────────────────────────────────────────────
8
+ export function isAllowedOrigin(origin, siteDomain) {
9
+ if (!origin || !siteDomain) return false;
10
+ return origin === `https://${siteDomain}`
11
+ || origin.endsWith(`.${siteDomain}`)
12
+ || origin === 'http://localhost:3000'
13
+ || origin === 'http://localhost:5173';
14
+ }
15
+
16
+ export function corsHeaders(origin, siteDomain) {
17
+ const allowed = isAllowedOrigin(origin, siteDomain) ? origin : `https://${siteDomain}`;
18
+ return {
19
+ 'Access-Control-Allow-Origin': allowed,
20
+ 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
21
+ 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With',
22
+ 'Access-Control-Max-Age': '86400',
23
+ };
24
+ }
25
+
26
+ // ── SHA-256 via WebCrypto (obrigatório no Cloudflare Workers) ─────────────────
27
+ export async function sha256(value) {
28
+ if (!value) return undefined;
29
+ const clean = String(value).toLowerCase().trim();
30
+ if (!clean) return undefined;
31
+ const buf = await crypto.subtle.digest(
32
+ 'SHA-256',
33
+ new TextEncoder().encode(clean)
34
+ );
35
+ return Array.from(new Uint8Array(buf))
36
+ .map(b => b.toString(16).padStart(2, '0'))
37
+ .join('');
38
+ }
39
+
40
+ // ── Normalização de telefone → somente dígitos + DDI 55 ──────────────────────
41
+ export function normalizePhone(phone) {
42
+ if (!phone) return undefined;
43
+ let digits = String(phone).replace(/\D/g, '');
44
+ if (digits.length === 11 && !digits.startsWith('55')) digits = '55' + digits;
45
+ if (digits.length === 10 && !digits.startsWith('55')) digits = '55' + digits;
46
+ return digits.length >= 10 ? digits : undefined;
47
+ }
48
+
49
+ // ── Normalização de cidade → lowercase sem acentos ────────────────────────────
50
+ export function normalizeCity(city) {
51
+ if (!city) return undefined;
52
+ return String(city)
53
+ .toLowerCase()
54
+ .normalize('NFD')
55
+ .replace(/[\u0300-\u036f]/g, '')
56
+ .replace(/[^a-z0-9]/g, '');
57
+ }
58
+
59
+ // ── Parse seguro de JSON armazenado como TEXT no D1 ───────────────────────────
60
+ export function tryParseJson(str, fallback) {
61
+ if (!str) return fallback !== undefined ? fallback : null;
62
+ try { return JSON.parse(str); } catch { return fallback !== undefined ? fallback : null; }
63
+ }
64
+
65
+ // ── Mapa Meta → GA4 event names ───────────────────────────────────────────────
66
+ export const META_TO_GA4 = {
67
+ PageView: 'page_view',
68
+ ViewContent: 'view_item',
69
+ Lead: 'generate_lead',
70
+ Contact: 'generate_lead',
71
+ Schedule: 'generate_lead',
72
+ InitiateCheckout: 'begin_checkout',
73
+ AddToCart: 'add_to_cart',
74
+ AddPaymentInfo: 'add_payment_info',
75
+ Purchase: 'purchase',
76
+ CompleteRegistration: 'sign_up',
77
+ Subscribe: 'subscribe',
78
+ StartTrial: 'start_trial',
79
+ Search: 'search',
80
+ AddToWishlist: 'add_to_wishlist',
81
+ };
82
+
83
+ // ── Lista canônica de eventos válidos (19 eventos) ────────────────────────────
84
+ export const VALID_EVENT_NAMES = new Set([
85
+ 'PageView','ViewContent','Lead','Purchase','InitiateCheckout',
86
+ 'AddToCart','CompleteRegistration','Contact','Schedule',
87
+ 'StartTrial','Subscribe','SubmitApplication','Search',
88
+ 'video_start','video_25','video_50','video_75','video_complete',
89
+ ]);
@@ -0,0 +1,67 @@
1
+ -- CDP Edge — Composite Indexes para Performance
2
+ -- Executar após todos os outros schemas:
3
+ -- wrangler d1 execute cdp-edge-db --file=schema-indexes.sql --remote
4
+ --
5
+ -- Todos os índices usam IF NOT EXISTS — idempotente, seguro para re-executar.
6
+
7
+ -- ── leads: queries mais comuns ────────────────────────────────────────────────
8
+
9
+ -- Deduplicação por email + janela temporal (upsertProfile, saveLead)
10
+ CREATE INDEX IF NOT EXISTS idx_leads_email_created
11
+ ON leads(email, created_at DESC);
12
+
13
+ -- Relatórios por plataforma + período (Intelligence Agent, Dashboard)
14
+ CREATE INDEX IF NOT EXISTS idx_leads_platform_created
15
+ ON leads(platform, created_at DESC);
16
+
17
+ -- Lookup de conversão por evento + período (auditErrorRates, generateDailyReport)
18
+ CREATE INDEX IF NOT EXISTS idx_leads_event_created
19
+ ON leads(event_name, created_at DESC);
20
+
21
+ -- Identity resolution: user_id + evento para evitar duplicatas
22
+ CREATE INDEX IF NOT EXISTS idx_leads_userid_event
23
+ ON leads(user_id, event_name);
24
+
25
+ -- Exportação Customer Match: event_name + email filtrado por data
26
+ CREATE INDEX IF NOT EXISTS idx_leads_event_email
27
+ ON leads(event_name, email);
28
+
29
+ -- CRM Dashboard: ordenação por status + criação
30
+ CREATE INDEX IF NOT EXISTS idx_leads_crm_created
31
+ ON leads(crm_status, created_at DESC);
32
+
33
+ -- ── user_profiles: enrichment e Advanced Matching ────────────────────────────
34
+
35
+ -- Busca por LTV class + atualização (Bidding ML consome isso)
36
+ CREATE INDEX IF NOT EXISTS idx_profiles_ltv_updated
37
+ ON user_profiles(predicted_ltv_class, updated_at DESC);
38
+
39
+ -- Score + cohort para segmentação ML (handleSegmentationCluster)
40
+ CREATE INDEX IF NOT EXISTS idx_profiles_score_cohort
41
+ ON user_profiles(score DESC, cohort_label);
42
+
43
+ -- Lookup email + updated_at (resolveDeviceGraph, getProfileByEmail)
44
+ CREATE INDEX IF NOT EXISTS idx_profiles_email_updated
45
+ ON user_profiles(email, updated_at DESC);
46
+
47
+ -- ── fraud_signals: dashboard e alertas ───────────────────────────────────────
48
+
49
+ -- handleFraudAlerts: filtra por ip + período
50
+ CREATE INDEX IF NOT EXISTS idx_fraud_ip_created
51
+ ON fraud_signals(ip_address, created_at DESC);
52
+
53
+ -- handleFraudStats: fraud_score >= threshold ordenado por data
54
+ CREATE INDEX IF NOT EXISTS idx_fraud_score_created
55
+ ON fraud_signals(fraud_score DESC, created_at DESC);
56
+
57
+ -- ── ltv_ab_assignments: resultados de A/B test ───────────────────────────────
58
+
59
+ -- handleLtvAbTestResults: test_id + predicted_class para análise
60
+ CREATE INDEX IF NOT EXISTS idx_ab_testid_class
61
+ ON ltv_ab_assignments(test_id, predicted_class);
62
+
63
+ -- ── ml_segment_members: join com leads para bidding ─────────────────────────
64
+
65
+ -- handleBiddingRecommend: segment_id lookup
66
+ CREATE INDEX IF NOT EXISTS idx_seg_members_segid
67
+ ON ml_segment_members(segment_id, joined_at DESC);
@@ -1,4 +1,6 @@
1
1
  name = "server-edge-tracker"
2
+ # Entry point: worker.js (monólito original, 100% compatível)
3
+ # Para usar a versão modular ES Modules: altere para main = "index.js"
2
4
  main = "worker.js"
3
5
  compatibility_date = "2025-01-01"
4
6
  compatibility_flags = ["nodejs_compat"]