cdp-edge 2.2.0 → 2.2.2

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.
@@ -1867,6 +1867,12 @@ async function predictLtv(env, payload, request, customSystemPrompt = null) {
1867
1867
  if (payload.phone) score += 4;
1868
1868
  if (payload.firstName) score += 2;
1869
1869
 
1870
+ // 5b. Tipo de evento imobiliário (0–15) — sinais de intenção de compra física
1871
+ const evType = (payload.eventType || '').toLowerCase();
1872
+ if (evType === 'customizeproduct') score += 15; // simulação de financiamento → intenção máxima
1873
+ else if (evType === 'findlocation') score += 10; // viu mapa/localização → visita física iminente
1874
+ else if (evType === 'addtowishlist') score += 8; // favoritou → interesse persistente
1875
+
1870
1876
  score = Math.min(100, score);
1871
1877
 
1872
1878
  // Classificação
@@ -1901,9 +1907,10 @@ async function predictLtv(env, payload, request, customSystemPrompt = null) {
1901
1907
  country,
1902
1908
  has_email: !!payload.email,
1903
1909
  has_phone: !!payload.phone,
1910
+ event_type: payload.eventType || null,
1904
1911
  })},
1905
1912
  ];
1906
- const aiRes = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages: prompt, max_tokens: 32 });
1913
+ const aiRes = await env.AI.run('@cf/ibm-granite/granite-4.0-h-micro', { messages: prompt, max_tokens: 32 });
1907
1914
  const parsed = JSON.parse(aiRes.response.trim());
1908
1915
  if (typeof parsed.adjustment === 'number') {
1909
1916
  aiAdjustment = Math.max(-10, Math.min(10, parsed.adjustment));
@@ -2415,8 +2422,82 @@ function tryParseJson(str, fallback) {
2415
2422
  try { return JSON.parse(str); } catch { return fallback !== undefined ? fallback : null; }
2416
2423
  }
2417
2424
 
2425
+ // ── Helpers K-means vetorial (usado pelo clustering com embeddings) ───────────
2426
+
2427
+ function _cosDist(a, b) {
2428
+ let dot = 0, na = 0, nb = 0;
2429
+ for (let i = 0; i < a.length; i++) { dot += a[i]*b[i]; na += a[i]*a[i]; nb += b[i]*b[i]; }
2430
+ return 1 - dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-10);
2431
+ }
2432
+
2433
+ function _kmeansRun(vectors, k, maxIter = 25) {
2434
+ const n = vectors.length;
2435
+ const dim = vectors[0].length;
2436
+ // K-means++ init
2437
+ const centroids = [vectors[Math.floor(Math.random() * n)]];
2438
+ while (centroids.length < k) {
2439
+ const dists = vectors.map(v => Math.min(...centroids.map(c => _cosDist(v, c))));
2440
+ const sum = dists.reduce((a, b) => a + b, 0);
2441
+ let r = Math.random() * sum, cumul = 0;
2442
+ for (let i = 0; i < n; i++) { cumul += dists[i]; if (cumul >= r) { centroids.push(vectors[i]); break; } }
2443
+ if (centroids.length < k) centroids.push(vectors[Math.floor(Math.random() * n)]);
2444
+ }
2445
+
2446
+ let assignments = new Array(n).fill(0);
2447
+ for (let iter = 0; iter < maxIter; iter++) {
2448
+ let changed = false;
2449
+ for (let i = 0; i < n; i++) {
2450
+ let best = 0, bestD = Infinity;
2451
+ for (let c = 0; c < k; c++) { const d = _cosDist(vectors[i], centroids[c]); if (d < bestD) { bestD = d; best = c; } }
2452
+ if (assignments[i] !== best) { assignments[i] = best; changed = true; }
2453
+ }
2454
+ if (!changed) break;
2455
+ // Recompute centroids
2456
+ for (let c = 0; c < k; c++) {
2457
+ const members = vectors.filter((_, i) => assignments[i] === c);
2458
+ if (members.length === 0) continue;
2459
+ for (let d = 0; d < dim; d++) centroids[c][d] = members.reduce((s, v) => s + v[d], 0) / members.length;
2460
+ }
2461
+ }
2462
+ return { assignments, centroids };
2463
+ }
2464
+
2465
+ function _silhouette(vectors, assignments, k) {
2466
+ const n = vectors.length;
2467
+ let total = 0;
2468
+ for (let i = 0; i < n; i++) {
2469
+ const ci = assignments[i];
2470
+ const sameCluster = vectors.filter((_, j) => j !== i && assignments[j] === ci);
2471
+ const a = sameCluster.length ? sameCluster.reduce((s, v) => s + _cosDist(vectors[i], v), 0) / sameCluster.length : 0;
2472
+ let b = Infinity;
2473
+ for (let c = 0; c < k; c++) {
2474
+ if (c === ci) continue;
2475
+ const other = vectors.filter((_, j) => assignments[j] === c);
2476
+ if (other.length) b = Math.min(b, other.reduce((s, v) => s + _cosDist(vectors[i], v), 0) / other.length);
2477
+ }
2478
+ total += b === Infinity ? 0 : (b - a) / Math.max(a, b);
2479
+ }
2480
+ return Math.round((total / n) * 1000) / 1000;
2481
+ }
2482
+
2483
+ function _buildLeadProfile(l) {
2484
+ return [
2485
+ `LTV: ${l.predicted_ltv_class || 'desconhecido'}`,
2486
+ `engajamento: ${Math.round(l.engagement_score || 0)}`,
2487
+ `intenção: ${l.intention_level || 'desconhecida'}`,
2488
+ `origem: ${l.utm_source || 'direto'}`,
2489
+ `canal: ${l.utm_medium || 'desconhecido'}`,
2490
+ `país: ${l.country || 'BR'}`,
2491
+ `estado: ${l.state || ''}`,
2492
+ `hora: ${l.hour_of_day || 12}h`,
2493
+ (l.is_weekend ? 'fim-de-semana' : 'dia-útil'),
2494
+ `recência: ${l.days_since_lead || 0} dias`,
2495
+ ].filter(Boolean).join(', ');
2496
+ }
2497
+
2418
2498
  // ── POST /api/segmentation/cluster ───────────────────────────────────────────
2419
- // Executa clustering K-means/DBSCAN/Hierarchical via Workers AI
2499
+ // Clustering real com embeddings (embeddinggemma-300m) + K-means vetorial
2500
+ // Granite usado apenas para nomear segmentos
2420
2501
  // Requer bindings: DB + AI
2421
2502
  async function handleSegmentationCluster(env, request, headers) {
2422
2503
  if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
@@ -2424,7 +2505,7 @@ async function handleSegmentationCluster(env, request, headers) {
2424
2505
 
2425
2506
  const url = new URL(request.url);
2426
2507
  const algorithm = url.searchParams.get('algorithm') || 'kmeans';
2427
- const nClusters = Math.min(10, Math.max(3, parseInt(url.searchParams.get('n_clusters') || '5')));
2508
+ const nClusters = Math.min(10, Math.max(2, parseInt(url.searchParams.get('n_clusters') || '5')));
2428
2509
  const clientVertical = url.searchParams.get('vertical') || 'general';
2429
2510
  const forceRecluster = url.searchParams.get('force') === 'true';
2430
2511
 
@@ -2480,96 +2561,94 @@ async function handleSegmentationCluster(env, request, headers) {
2480
2561
  }), { status: 400, headers });
2481
2562
  }
2482
2563
 
2483
- // 3. Feature Engineering — normalização 0–1
2484
- const features = leads.map(l => ({
2485
- id: l.id,
2486
- ltv: l.predicted_ltv_class === 'High' ? 1 : (l.predicted_ltv_class === 'Medium' ? 0.5 : 0),
2487
- engagement: Math.min((l.engagement_score || 0) / 100, 1),
2488
- intention: l.intention_level === 'comprador' || l.intention_level === 'high_intent' ? 1
2489
- : l.intention_level === 'interessado' ? 0.6
2490
- : l.intention_level === 'curioso' ? 0.3 : 0,
2491
- recency: Math.max(0, 1 - (l.days_since_lead || 0) / 180),
2492
- hour: (l.hour_of_day || 12) / 23,
2493
- is_weekend: l.is_weekend || 0,
2494
- is_br: l.country === 'BR' ? 1 : 0,
2495
- is_paid: ['facebook','google','tiktok','instagram','youtube'].includes(
2496
- (l.utm_source || '').toLowerCase()) ? 1 : 0,
2497
- }));
2564
+ const startTime = Date.now();
2498
2565
 
2499
- // 4. Prompt para Workers AI
2500
- const sampleSize = Math.min(features.length, 100);
2501
- const sample = features.slice(0, sampleSize);
2502
-
2503
- const clusteringPrompt =
2504
- `You are a customer segmentation ML expert. Perform ${algorithm} clustering on ${sampleSize} customers into ${nClusters} segments.
2505
-
2506
- Customer features (all normalized 0-1):
2507
- - ltv: predicted lifetime value (0=Low, 0.5=Medium, 1=High)
2508
- - engagement: browser engagement score
2509
- - intention: purchase intention (0=none, 0.3=curious, 0.6=interested, 1=buyer)
2510
- - recency: lead recency (1=today, 0=6 months ago)
2511
- - hour: conversion hour of day
2512
- - is_weekend: converted on weekend (0/1)
2513
- - is_br: lead from Brazil (0/1)
2514
- - is_paid: from paid traffic channel (0/1)
2515
-
2516
- Data (${sampleSize} customers): ${JSON.stringify(sample.slice(0, 50))}
2517
-
2518
- Return ONLY valid JSON, zero explanation:
2519
- {
2520
- "clusters": [
2521
- {
2522
- "cluster_id": 0,
2523
- "name": "[Nome Descritivo em Português]",
2524
- "size": ${Math.round(sampleSize / nClusters)},
2525
- "percentage": ${Math.round(100 / nClusters)},
2526
- "characteristics": {
2527
- "avg_ltv_class": 0.5,
2528
- "avg_behavior_score": 0.5,
2529
- "avg_engagement_score": 0.5,
2530
- "avg_intention_level": 0.5,
2531
- "avg_days_since_lead": 30,
2532
- "dominant_countries": ["BR"],
2533
- "dominant_states": ["SP", "RJ"],
2534
- "dominant_utm_sources": ["facebook"],
2535
- "top_features": ["ltv", "engagement"]
2536
- },
2537
- "centroid": { "ltv": 0.5, "engagement": 0.5, "intention": 0.5 },
2538
- "action_recommendation": "[Recomendação de campanha específica para este segmento]"
2566
+ // 3. Gerar perfis textuais e embeddings via embeddinggemma-300m
2567
+ const sample = leads.slice(0, 100); // max 100 por batch
2568
+ const profiles = sample.map(_buildLeadProfile);
2569
+
2570
+ const embRes = await env.AI.run('@cf/baai/bge-m3', { text: profiles });
2571
+ const vectors = embRes.data; // float32[][] shape [N, 768]
2572
+
2573
+ if (!vectors || vectors.length < nClusters) {
2574
+ throw new Error(`embeddinggemma retornou ${vectors?.length ?? 0} vetores — insuficiente para ${nClusters} clusters`);
2539
2575
  }
2540
- ],
2541
- "silhouette_score": 0.65,
2542
- "total_processed": ${sampleSize}
2543
- }`;
2544
2576
 
2545
- // 5. Executar via Workers AI
2546
- const startTime = Date.now();
2547
- const aiRes = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
2548
- messages: [{ role: 'user', content: clusteringPrompt }],
2549
- max_tokens: 2000,
2577
+ // 4. K-means vetorial real (cosine distance)
2578
+ const { assignments } = _kmeansRun(vectors, nClusters);
2579
+
2580
+ // 5. Silhouette score real
2581
+ const silhouetteScore = _silhouette(vectors, assignments, nClusters);
2582
+
2583
+ // 6. Agregar estatísticas por cluster para nomear com Granite
2584
+ const clusterStats = Array.from({ length: nClusters }, (_, c) => {
2585
+ const members = sample.filter((_, i) => assignments[i] === c);
2586
+ if (members.length === 0) return null;
2587
+ const ltvMap = { High: 1, Medium: 0.5, Low: 0 };
2588
+ const avgLtv = members.reduce((s, l) => s + (ltvMap[l.predicted_ltv_class] ?? 0), 0) / members.length;
2589
+ const avgEng = members.reduce((s, l) => s + (l.engagement_score || 0), 0) / members.length;
2590
+ const avgDays = members.reduce((s, l) => s + (l.days_since_lead || 0), 0) / members.length;
2591
+ const sources = members.map(l => l.utm_source).filter(Boolean);
2592
+ const states = members.map(l => l.state).filter(Boolean);
2593
+ const topSource = sources.length ? [...sources.reduce((m, s) => m.set(s, (m.get(s)||0)+1), new Map())].sort((a,b)=>b[1]-a[1])[0]?.[0] : 'direto';
2594
+ const topState = states.length ? [...states.reduce((m, s) => m.set(s, (m.get(s)||0)+1), new Map())].sort((a,b)=>b[1]-a[1])[0]?.[0] : 'BR';
2595
+ const intentions = members.map(l => l.intention_level).filter(Boolean);
2596
+ const topIntent = intentions.length ? [...intentions.reduce((m, s) => m.set(s,(m.get(s)||0)+1), new Map())].sort((a,b)=>b[1]-a[1])[0]?.[0] : 'desconhecida';
2597
+ return { c, size: members.length, pct: Math.round(members.length / sample.length * 100), avgLtv, avgEng, avgDays, topSource, topState, topIntent };
2598
+ }).filter(Boolean);
2599
+
2600
+ // 7. Usar Granite apenas para nomear e recomendar ação por cluster
2601
+ const namingPrompt =
2602
+ `Você é especialista em segmentação de clientes. Dê um nome descritivo em português e uma recomendação de campanha para cada segmento abaixo. Retorne SOMENTE JSON válido:
2603
+ {"segments":[{"cluster_id":0,"name":"...","action":"..."},...]}
2604
+
2605
+ Segmentos:
2606
+ ${clusterStats.map(s => `Cluster ${s.c}: LTV médio=${s.avgLtv.toFixed(2)}, engajamento=${s.avgEng.toFixed(0)}, intenção dominante="${s.topIntent}", origem="${s.topSource}", estado="${s.topState}", recência=${s.avgDays.toFixed(0)} dias, tamanho=${s.size} leads`).join('\n')}`;
2607
+
2608
+ const nameRes = await env.AI.run('@cf/ibm-granite/granite-4.0-h-micro', {
2609
+ messages: [{ role: 'user', content: namingPrompt }],
2610
+ max_tokens: 800,
2550
2611
  });
2551
- const duration = Date.now() - startTime;
2552
2612
 
2553
- if (!aiRes?.response) throw new Error('Workers AI não retornou resposta');
2613
+ let clusterNames = {};
2614
+ try {
2615
+ const m = (nameRes?.response || '').match(/\{[\s\S]*\}/);
2616
+ if (m) {
2617
+ const parsed = JSON.parse(m[0]);
2618
+ (parsed.segments || []).forEach(s => { clusterNames[s.cluster_id] = { name: s.name, action: s.action }; });
2619
+ }
2620
+ } catch { /* usa nomes fallback */ }
2554
2621
 
2555
- // 6. Parse do resultado
2556
- const jsonMatch = aiRes.response.trim().match(/\{[\s\S]*\}/);
2557
- if (!jsonMatch) throw new Error('Resposta do Workers AI não contém JSON válido');
2558
- const mlResult = JSON.parse(jsonMatch[0]);
2622
+ const duration = Date.now() - startTime;
2559
2623
 
2560
- if (!Array.isArray(mlResult.clusters) || mlResult.clusters.length === 0) {
2561
- throw new Error('Workers AI não retornou clusters válidos');
2562
- }
2624
+ // 8. Montar resultado final
2625
+ const clusters = clusterStats.map(s => ({
2626
+ cluster_id: s.c,
2627
+ name: clusterNames[s.c]?.name || `Segmento ${s.c + 1}`,
2628
+ size: s.size,
2629
+ percentage: s.pct,
2630
+ action_recommendation: clusterNames[s.c]?.action || '',
2631
+ characteristics: {
2632
+ avg_ltv_class: s.avgLtv,
2633
+ avg_engagement_score: s.avgEng,
2634
+ avg_intention_level: s.avgLtv,
2635
+ avg_days_since_lead: s.avgDays,
2636
+ dominant_countries: ['BR'],
2637
+ dominant_states: [s.topState],
2638
+ dominant_utm_sources: [s.topSource],
2639
+ top_features: ['ltv', 'engagement', 'intention'],
2640
+ },
2641
+ }));
2563
2642
 
2564
- // 7. Inativar clusters anteriores do mesmo algoritmo/vertical
2643
+ // 9. Inativar clusters anteriores do mesmo algoritmo/vertical
2565
2644
  await env.DB.prepare(
2566
2645
  `UPDATE ml_segments SET is_active = 0 WHERE clustering_algorithm = ? AND client_vertical = ? AND is_active = 1`
2567
2646
  ).bind(algorithm, clientVertical).run();
2568
2647
 
2569
- // 8. Persistir novos clusters no D1
2648
+ // 10. Persistir novos clusters no D1
2570
2649
  const now = new Date().toISOString();
2571
- for (const cluster of mlResult.clusters) {
2572
- const ch = cluster.characteristics || {};
2650
+ for (const cluster of clusters) {
2651
+ const ch = cluster.characteristics;
2573
2652
  await env.DB.prepare(`
2574
2653
  INSERT INTO ml_segments (
2575
2654
  cluster_id, cluster_name, clustering_algorithm, client_vertical,
@@ -2581,23 +2660,23 @@ Return ONLY valid JSON, zero explanation:
2581
2660
  is_active, created_at, updated_at
2582
2661
  ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?)
2583
2662
  `).bind(
2584
- cluster.cluster_id || 0,
2585
- cluster.name || `Segmento ${cluster.cluster_id}`,
2663
+ cluster.cluster_id,
2664
+ cluster.name,
2586
2665
  algorithm,
2587
2666
  clientVertical,
2588
- cluster.size || 0,
2589
- cluster.percentage || 0,
2590
- ch.avg_ltv_class || 0,
2591
- ch.avg_behavior_score || 0,
2592
- ch.avg_engagement_score || 0,
2593
- ch.avg_intention_level || 0,
2594
- ch.avg_days_since_lead || 0,
2595
- JSON.stringify(ch.dominant_countries || ['BR']),
2596
- JSON.stringify(ch.dominant_states || []),
2597
- JSON.stringify(ch.dominant_utm_sources || []),
2598
- JSON.stringify(ch.top_features || []),
2599
- mlResult.silhouette_score || 0,
2600
- JSON.stringify([cluster.action_recommendation || '']),
2667
+ cluster.size,
2668
+ cluster.percentage,
2669
+ ch.avg_ltv_class,
2670
+ ch.avg_engagement_score,
2671
+ ch.avg_engagement_score,
2672
+ ch.avg_intention_level,
2673
+ ch.avg_days_since_lead,
2674
+ JSON.stringify(ch.dominant_countries),
2675
+ JSON.stringify(ch.dominant_states),
2676
+ JSON.stringify(ch.dominant_utm_sources),
2677
+ JSON.stringify(ch.top_features),
2678
+ silhouetteScore,
2679
+ JSON.stringify([cluster.action_recommendation]),
2601
2680
  JSON.stringify([]),
2602
2681
  JSON.stringify([]),
2603
2682
  now,
@@ -2605,7 +2684,7 @@ Return ONLY valid JSON, zero explanation:
2605
2684
  ).run();
2606
2685
  }
2607
2686
 
2608
- // 9. Log no histórico de clustering
2687
+ // 11. Log no histórico de clustering
2609
2688
  try {
2610
2689
  await env.DB.prepare(`
2611
2690
  INSERT INTO ml_clustering_history (
@@ -2617,23 +2696,25 @@ Return ONLY valid JSON, zero explanation:
2617
2696
  new Date(startTime).toISOString(),
2618
2697
  algorithm,
2619
2698
  leads.length,
2620
- mlResult.clusters.length,
2699
+ clusters.length,
2621
2700
  duration,
2622
2701
  Math.ceil(duration * 0.01),
2623
- JSON.stringify({ algorithm, n_clusters: nClusters, vertical: clientVertical }),
2624
- JSON.stringify({ clusters: mlResult.clusters.length, silhouette: mlResult.silhouette_score }),
2702
+ JSON.stringify({ algorithm, n_clusters: nClusters, vertical: clientVertical, engine: 'embeddinggemma-300m+kmeans' }),
2703
+ JSON.stringify({ clusters: clusters.length, silhouette: silhouetteScore }),
2625
2704
  ).run();
2626
2705
  } catch (e) { console.error('[Segmentation] history log error:', e.message); }
2627
2706
 
2628
2707
  return new Response(JSON.stringify({
2629
2708
  success: true,
2630
2709
  algorithm,
2631
- n_clusters: mlResult.clusters.length,
2710
+ engine: 'embeddinggemma-300m + kmeans vetorial',
2711
+ n_clusters: clusters.length,
2632
2712
  client_vertical: clientVertical,
2633
2713
  leads_analyzed: leads.length,
2714
+ sample_embedded: sample.length,
2634
2715
  duration_ms: duration,
2635
- silhouette_score: mlResult.silhouette_score || null,
2636
- clusters: mlResult.clusters,
2716
+ silhouette_score: silhouetteScore,
2717
+ clusters,
2637
2718
  generated_at: now,
2638
2719
  }), { status: 200, headers });
2639
2720
 
@@ -2794,14 +2875,6 @@ async function handleSegmentationUpdate(env, request, headers) {
2794
2875
  // Heurístico puro (sem AI) — latência zero no /track
2795
2876
  // ─────────────────────────────────────────────────────────────────────────────
2796
2877
 
2797
- // Domínios de email descartáveis
2798
- const DISPOSABLE_EMAIL_DOMAINS = new Set([
2799
- 'mailinator.com','guerrillamail.com','tempmail.com','throwaway.email',
2800
- 'yopmail.com','sharklasers.com','guerrillamailblock.com','spam4.me',
2801
- '10minutemail.com','trashmail.com','maildrop.cc','fakeinbox.com',
2802
- 'dispostable.com','mailnull.com','tempr.email','getnada.com',
2803
- ]);
2804
-
2805
2878
  // ASNs conhecidos de datacenters (evitar falsos negativos em ASNs legítimos)
2806
2879
  const DATACENTER_PATTERNS = /amazon|google|microsoft|digitalocean|linode|ovh|vultr|hetzner|contabo|cloudflare|packet|rackspace|leaseweb/i;
2807
2880
 
@@ -2854,15 +2927,7 @@ async function checkFraudGate(env, request, payload) {
2854
2927
  result.score += 20; result.reasons.push('no_accept_language');
2855
2928
  }
2856
2929
 
2857
- // 6. Email descartável
2858
- if (email) {
2859
- const domain = email.split('@')[1]?.toLowerCase();
2860
- if (domain && DISPOSABLE_EMAIL_DOMAINS.has(domain)) {
2861
- result.score += 25; result.reasons.push('disposable_email');
2862
- }
2863
- }
2864
-
2865
- // 7. Velocity check via KV
2930
+ // 6. Velocity check via KV
2866
2931
  if (env.GEO_CACHE && ip) {
2867
2932
  const velKey1h = `fraud_velocity:${ip}:h`;
2868
2933
  const velStr = await env.GEO_CACHE.get(velKey1h);
@@ -3839,7 +3904,7 @@ export default {
3839
3904
 
3840
3905
  // Workers AI — ping
3841
3906
  try {
3842
- await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
3907
+ await env.AI.run('@cf/ibm-granite/granite-4.0-h-micro', {
3843
3908
  messages: [{ role: 'user', content: 'ping' }],
3844
3909
  max_tokens: 1,
3845
3910
  });
@@ -3910,7 +3975,9 @@ export default {
3910
3975
  'PageView','ViewContent','Lead','Purchase','InitiateCheckout',
3911
3976
  'AddToCart','CompleteRegistration','Contact','Schedule',
3912
3977
  'StartTrial','Subscribe','SubmitApplication','Search',
3913
- 'video_start','video_25','video_50','video_75','video_complete'
3978
+ 'video_start','video_25','video_50','video_75','video_complete',
3979
+ // Imóveis — intenção de visita física, financiamento e favoritar
3980
+ 'FindLocation','CustomizeProduct','AddToWishlist',
3914
3981
  ]);
3915
3982
  const STR_FIELDS = ['email','phone','firstName','lastName','city','state','zip','userId',
3916
3983
  'utmSource','utmMedium','utmCampaign','utmContent','utmTerm',
@@ -4017,8 +4084,9 @@ export default {
4017
4084
  // ── LTV Prediction (+ A/B Testing de Prompts) ────────────────────────────
4018
4085
  // Lead, Contact, Schedule: sem valor monetário real → injetar LTV preditivo
4019
4086
  // Isso treina os algoritmos de Meta/TikTok a priorizar leads de alto valor
4020
- const LTV_EVENTS = ['Lead', 'Contact', 'Schedule', 'CompleteRegistration'];
4087
+ const LTV_EVENTS = ['Lead', 'Contact', 'Schedule', 'CompleteRegistration', 'FindLocation', 'CustomizeProduct', 'AddToWishlist'];
4021
4088
  if (LTV_EVENTS.includes(eventName) && !payload.value) {
4089
+ payload.eventType = eventName; // expõe ao predictLtv para scoring por tipo de evento
4022
4090
  // A/B Testing: busca variação ativa (usa KV cache — ~0ms de latência extra)
4023
4091
  const abVariation = await getLtvAbVariation(env);
4024
4092
  const ltv = await predictLtv(env, payload, request, abVariation?.system_prompt || null);
@@ -4,6 +4,7 @@ name = "server-edge-tracker"
4
4
  main = "index.js"
5
5
  compatibility_date = "2025-01-01"
6
6
  compatibility_flags = ["nodejs_compat"]
7
+ workers_dev = true
7
8
 
8
9
  # ── Worker Routes — same-domain tracking (imune a bloqueios) ─────────────────
9
10
  # Substituir SEU_DOMINIO pelo domínio do cliente antes do deploy
@@ -25,10 +26,10 @@ zone_name = "lancamentosabc.com.br"
25
26
 
26
27
  # ── Variáveis públicas (não são segredos) ─────────────────────────────────────
27
28
  [vars]
28
- META_PIXEL_ID = "SEU_META_PIXEL_ID"
29
- GA4_MEASUREMENT_ID = "G-XXXXXXXXXX"
30
- TIKTOK_PIXEL_ID = "CXXXXXXXXXXXXXXX"
31
- SITE_DOMAIN = "SEU_DOMINIO"
29
+ META_PIXEL_ID = "1583939052660159"
30
+ GA4_MEASUREMENT_ID = "G-G7VEN1MNH1"
31
+ TIKTOK_PIXEL_ID = "D71D6T3C77U56RM5VF0G"
32
+ SITE_DOMAIN = "lancamentosabc.com.br"
32
33
 
33
34
  # ── Banco D1 ──────────────────────────────────────────────────────────────────
34
35
  # Após criar o banco com "wrangler d1 create cdp-edge-db",
@@ -95,6 +96,22 @@ namespace_id = "1001"
95
96
  limit = 60
96
97
  period = 60
97
98
 
99
+ # ── Observabilidade — Logs + Traces persistidos no painel Cloudflare ─────────
100
+ [observability]
101
+ enabled = false
102
+ head_sampling_rate = 1
103
+
104
+ [observability.logs]
105
+ enabled = true
106
+ head_sampling_rate = 1
107
+ persist = true
108
+ invocation_logs = true
109
+
110
+ [observability.traces]
111
+ enabled = false
112
+ persist = true
113
+ head_sampling_rate = 1
114
+
98
115
  # ── Secrets (NÃO ficam aqui — configurar via CLI) ─────────────────────────────
99
116
  # wrangler secret put META_ACCESS_TOKEN ← token Meta CAPI (obrigatório)
100
117
  # wrangler secret put GA4_API_SECRET ← secret GA4 Measurement Protocol (obrigatório)
@@ -107,6 +124,7 @@ period = 60
107
124
  # wrangler secret put RESEND_API_KEY ← API Key do Resend (resend.com)
108
125
  # wrangler secret put RESEND_FROM_EMAIL ← Remetente verificado ex: "CDP Edge <noreply@seudominio.com.br>"
109
126
  # wrangler secret put WA_WEBHOOK_VERIFY_TOKEN ← Token de verificação do webhook WhatsApp (você define — qualquer string segura)
127
+ # wrangler secret put WEBHOOK_SECRET_TICTO ← HMAC-SHA256 Ticto
110
128
  # wrangler secret put PINTEREST_ACCESS_TOKEN ← Bearer token Pinterest Conversions API
111
129
  # wrangler secret put PINTEREST_AD_ACCOUNT_ID ← ID da conta de anúncios Pinterest (ex: 549755813XXX)
112
130
  # wrangler secret put REDDIT_ACCESS_TOKEN ← Bearer token Reddit Conversions API