cdp-edge 2.1.0 → 2.2.1

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.
@@ -1903,7 +1903,7 @@ async function predictLtv(env, payload, request, customSystemPrompt = null) {
1903
1903
  has_phone: !!payload.phone,
1904
1904
  })},
1905
1905
  ];
1906
- const aiRes = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { messages: prompt, max_tokens: 32 });
1906
+ const aiRes = await env.AI.run('@cf/ibm-granite/granite-4.0-h-micro', { messages: prompt, max_tokens: 32 });
1907
1907
  const parsed = JSON.parse(aiRes.response.trim());
1908
1908
  if (typeof parsed.adjustment === 'number') {
1909
1909
  aiAdjustment = Math.max(-10, Math.min(10, parsed.adjustment));
@@ -2415,8 +2415,82 @@ function tryParseJson(str, fallback) {
2415
2415
  try { return JSON.parse(str); } catch { return fallback !== undefined ? fallback : null; }
2416
2416
  }
2417
2417
 
2418
+ // ── Helpers K-means vetorial (usado pelo clustering com embeddings) ───────────
2419
+
2420
+ function _cosDist(a, b) {
2421
+ let dot = 0, na = 0, nb = 0;
2422
+ for (let i = 0; i < a.length; i++) { dot += a[i]*b[i]; na += a[i]*a[i]; nb += b[i]*b[i]; }
2423
+ return 1 - dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-10);
2424
+ }
2425
+
2426
+ function _kmeansRun(vectors, k, maxIter = 25) {
2427
+ const n = vectors.length;
2428
+ const dim = vectors[0].length;
2429
+ // K-means++ init
2430
+ const centroids = [vectors[Math.floor(Math.random() * n)]];
2431
+ while (centroids.length < k) {
2432
+ const dists = vectors.map(v => Math.min(...centroids.map(c => _cosDist(v, c))));
2433
+ const sum = dists.reduce((a, b) => a + b, 0);
2434
+ let r = Math.random() * sum, cumul = 0;
2435
+ for (let i = 0; i < n; i++) { cumul += dists[i]; if (cumul >= r) { centroids.push(vectors[i]); break; } }
2436
+ if (centroids.length < k) centroids.push(vectors[Math.floor(Math.random() * n)]);
2437
+ }
2438
+
2439
+ let assignments = new Array(n).fill(0);
2440
+ for (let iter = 0; iter < maxIter; iter++) {
2441
+ let changed = false;
2442
+ for (let i = 0; i < n; i++) {
2443
+ let best = 0, bestD = Infinity;
2444
+ for (let c = 0; c < k; c++) { const d = _cosDist(vectors[i], centroids[c]); if (d < bestD) { bestD = d; best = c; } }
2445
+ if (assignments[i] !== best) { assignments[i] = best; changed = true; }
2446
+ }
2447
+ if (!changed) break;
2448
+ // Recompute centroids
2449
+ for (let c = 0; c < k; c++) {
2450
+ const members = vectors.filter((_, i) => assignments[i] === c);
2451
+ if (members.length === 0) continue;
2452
+ for (let d = 0; d < dim; d++) centroids[c][d] = members.reduce((s, v) => s + v[d], 0) / members.length;
2453
+ }
2454
+ }
2455
+ return { assignments, centroids };
2456
+ }
2457
+
2458
+ function _silhouette(vectors, assignments, k) {
2459
+ const n = vectors.length;
2460
+ let total = 0;
2461
+ for (let i = 0; i < n; i++) {
2462
+ const ci = assignments[i];
2463
+ const sameCluster = vectors.filter((_, j) => j !== i && assignments[j] === ci);
2464
+ const a = sameCluster.length ? sameCluster.reduce((s, v) => s + _cosDist(vectors[i], v), 0) / sameCluster.length : 0;
2465
+ let b = Infinity;
2466
+ for (let c = 0; c < k; c++) {
2467
+ if (c === ci) continue;
2468
+ const other = vectors.filter((_, j) => assignments[j] === c);
2469
+ if (other.length) b = Math.min(b, other.reduce((s, v) => s + _cosDist(vectors[i], v), 0) / other.length);
2470
+ }
2471
+ total += b === Infinity ? 0 : (b - a) / Math.max(a, b);
2472
+ }
2473
+ return Math.round((total / n) * 1000) / 1000;
2474
+ }
2475
+
2476
+ function _buildLeadProfile(l) {
2477
+ return [
2478
+ `LTV: ${l.predicted_ltv_class || 'desconhecido'}`,
2479
+ `engajamento: ${Math.round(l.engagement_score || 0)}`,
2480
+ `intenção: ${l.intention_level || 'desconhecida'}`,
2481
+ `origem: ${l.utm_source || 'direto'}`,
2482
+ `canal: ${l.utm_medium || 'desconhecido'}`,
2483
+ `país: ${l.country || 'BR'}`,
2484
+ `estado: ${l.state || ''}`,
2485
+ `hora: ${l.hour_of_day || 12}h`,
2486
+ (l.is_weekend ? 'fim-de-semana' : 'dia-útil'),
2487
+ `recência: ${l.days_since_lead || 0} dias`,
2488
+ ].filter(Boolean).join(', ');
2489
+ }
2490
+
2418
2491
  // ── POST /api/segmentation/cluster ───────────────────────────────────────────
2419
- // Executa clustering K-means/DBSCAN/Hierarchical via Workers AI
2492
+ // Clustering real com embeddings (embeddinggemma-300m) + K-means vetorial
2493
+ // Granite usado apenas para nomear segmentos
2420
2494
  // Requer bindings: DB + AI
2421
2495
  async function handleSegmentationCluster(env, request, headers) {
2422
2496
  if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
@@ -2424,7 +2498,7 @@ async function handleSegmentationCluster(env, request, headers) {
2424
2498
 
2425
2499
  const url = new URL(request.url);
2426
2500
  const algorithm = url.searchParams.get('algorithm') || 'kmeans';
2427
- const nClusters = Math.min(10, Math.max(3, parseInt(url.searchParams.get('n_clusters') || '5')));
2501
+ const nClusters = Math.min(10, Math.max(2, parseInt(url.searchParams.get('n_clusters') || '5')));
2428
2502
  const clientVertical = url.searchParams.get('vertical') || 'general';
2429
2503
  const forceRecluster = url.searchParams.get('force') === 'true';
2430
2504
 
@@ -2480,96 +2554,94 @@ async function handleSegmentationCluster(env, request, headers) {
2480
2554
  }), { status: 400, headers });
2481
2555
  }
2482
2556
 
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
- }));
2557
+ const startTime = Date.now();
2498
2558
 
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]"
2559
+ // 3. Gerar perfis textuais e embeddings via embeddinggemma-300m
2560
+ const sample = leads.slice(0, 100); // max 100 por batch
2561
+ const profiles = sample.map(_buildLeadProfile);
2562
+
2563
+ const embRes = await env.AI.run('@cf/baai/bge-m3', { text: profiles });
2564
+ const vectors = embRes.data; // float32[][] shape [N, 768]
2565
+
2566
+ if (!vectors || vectors.length < nClusters) {
2567
+ throw new Error(`embeddinggemma retornou ${vectors?.length ?? 0} vetores — insuficiente para ${nClusters} clusters`);
2539
2568
  }
2540
- ],
2541
- "silhouette_score": 0.65,
2542
- "total_processed": ${sampleSize}
2543
- }`;
2544
2569
 
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,
2570
+ // 4. K-means vetorial real (cosine distance)
2571
+ const { assignments } = _kmeansRun(vectors, nClusters);
2572
+
2573
+ // 5. Silhouette score real
2574
+ const silhouetteScore = _silhouette(vectors, assignments, nClusters);
2575
+
2576
+ // 6. Agregar estatísticas por cluster para nomear com Granite
2577
+ const clusterStats = Array.from({ length: nClusters }, (_, c) => {
2578
+ const members = sample.filter((_, i) => assignments[i] === c);
2579
+ if (members.length === 0) return null;
2580
+ const ltvMap = { High: 1, Medium: 0.5, Low: 0 };
2581
+ const avgLtv = members.reduce((s, l) => s + (ltvMap[l.predicted_ltv_class] ?? 0), 0) / members.length;
2582
+ const avgEng = members.reduce((s, l) => s + (l.engagement_score || 0), 0) / members.length;
2583
+ const avgDays = members.reduce((s, l) => s + (l.days_since_lead || 0), 0) / members.length;
2584
+ const sources = members.map(l => l.utm_source).filter(Boolean);
2585
+ const states = members.map(l => l.state).filter(Boolean);
2586
+ 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';
2587
+ 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';
2588
+ const intentions = members.map(l => l.intention_level).filter(Boolean);
2589
+ 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';
2590
+ return { c, size: members.length, pct: Math.round(members.length / sample.length * 100), avgLtv, avgEng, avgDays, topSource, topState, topIntent };
2591
+ }).filter(Boolean);
2592
+
2593
+ // 7. Usar Granite apenas para nomear e recomendar ação por cluster
2594
+ const namingPrompt =
2595
+ `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:
2596
+ {"segments":[{"cluster_id":0,"name":"...","action":"..."},...]}
2597
+
2598
+ Segmentos:
2599
+ ${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')}`;
2600
+
2601
+ const nameRes = await env.AI.run('@cf/ibm-granite/granite-4.0-h-micro', {
2602
+ messages: [{ role: 'user', content: namingPrompt }],
2603
+ max_tokens: 800,
2550
2604
  });
2551
- const duration = Date.now() - startTime;
2552
2605
 
2553
- if (!aiRes?.response) throw new Error('Workers AI não retornou resposta');
2606
+ let clusterNames = {};
2607
+ try {
2608
+ const m = (nameRes?.response || '').match(/\{[\s\S]*\}/);
2609
+ if (m) {
2610
+ const parsed = JSON.parse(m[0]);
2611
+ (parsed.segments || []).forEach(s => { clusterNames[s.cluster_id] = { name: s.name, action: s.action }; });
2612
+ }
2613
+ } catch { /* usa nomes fallback */ }
2554
2614
 
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]);
2615
+ const duration = Date.now() - startTime;
2559
2616
 
2560
- if (!Array.isArray(mlResult.clusters) || mlResult.clusters.length === 0) {
2561
- throw new Error('Workers AI não retornou clusters válidos');
2562
- }
2617
+ // 8. Montar resultado final
2618
+ const clusters = clusterStats.map(s => ({
2619
+ cluster_id: s.c,
2620
+ name: clusterNames[s.c]?.name || `Segmento ${s.c + 1}`,
2621
+ size: s.size,
2622
+ percentage: s.pct,
2623
+ action_recommendation: clusterNames[s.c]?.action || '',
2624
+ characteristics: {
2625
+ avg_ltv_class: s.avgLtv,
2626
+ avg_engagement_score: s.avgEng,
2627
+ avg_intention_level: s.avgLtv,
2628
+ avg_days_since_lead: s.avgDays,
2629
+ dominant_countries: ['BR'],
2630
+ dominant_states: [s.topState],
2631
+ dominant_utm_sources: [s.topSource],
2632
+ top_features: ['ltv', 'engagement', 'intention'],
2633
+ },
2634
+ }));
2563
2635
 
2564
- // 7. Inativar clusters anteriores do mesmo algoritmo/vertical
2636
+ // 9. Inativar clusters anteriores do mesmo algoritmo/vertical
2565
2637
  await env.DB.prepare(
2566
2638
  `UPDATE ml_segments SET is_active = 0 WHERE clustering_algorithm = ? AND client_vertical = ? AND is_active = 1`
2567
2639
  ).bind(algorithm, clientVertical).run();
2568
2640
 
2569
- // 8. Persistir novos clusters no D1
2641
+ // 10. Persistir novos clusters no D1
2570
2642
  const now = new Date().toISOString();
2571
- for (const cluster of mlResult.clusters) {
2572
- const ch = cluster.characteristics || {};
2643
+ for (const cluster of clusters) {
2644
+ const ch = cluster.characteristics;
2573
2645
  await env.DB.prepare(`
2574
2646
  INSERT INTO ml_segments (
2575
2647
  cluster_id, cluster_name, clustering_algorithm, client_vertical,
@@ -2581,23 +2653,23 @@ Return ONLY valid JSON, zero explanation:
2581
2653
  is_active, created_at, updated_at
2582
2654
  ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?)
2583
2655
  `).bind(
2584
- cluster.cluster_id || 0,
2585
- cluster.name || `Segmento ${cluster.cluster_id}`,
2656
+ cluster.cluster_id,
2657
+ cluster.name,
2586
2658
  algorithm,
2587
2659
  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 || '']),
2660
+ cluster.size,
2661
+ cluster.percentage,
2662
+ ch.avg_ltv_class,
2663
+ ch.avg_engagement_score,
2664
+ ch.avg_engagement_score,
2665
+ ch.avg_intention_level,
2666
+ ch.avg_days_since_lead,
2667
+ JSON.stringify(ch.dominant_countries),
2668
+ JSON.stringify(ch.dominant_states),
2669
+ JSON.stringify(ch.dominant_utm_sources),
2670
+ JSON.stringify(ch.top_features),
2671
+ silhouetteScore,
2672
+ JSON.stringify([cluster.action_recommendation]),
2601
2673
  JSON.stringify([]),
2602
2674
  JSON.stringify([]),
2603
2675
  now,
@@ -2605,7 +2677,7 @@ Return ONLY valid JSON, zero explanation:
2605
2677
  ).run();
2606
2678
  }
2607
2679
 
2608
- // 9. Log no histórico de clustering
2680
+ // 11. Log no histórico de clustering
2609
2681
  try {
2610
2682
  await env.DB.prepare(`
2611
2683
  INSERT INTO ml_clustering_history (
@@ -2617,23 +2689,25 @@ Return ONLY valid JSON, zero explanation:
2617
2689
  new Date(startTime).toISOString(),
2618
2690
  algorithm,
2619
2691
  leads.length,
2620
- mlResult.clusters.length,
2692
+ clusters.length,
2621
2693
  duration,
2622
2694
  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 }),
2695
+ JSON.stringify({ algorithm, n_clusters: nClusters, vertical: clientVertical, engine: 'embeddinggemma-300m+kmeans' }),
2696
+ JSON.stringify({ clusters: clusters.length, silhouette: silhouetteScore }),
2625
2697
  ).run();
2626
2698
  } catch (e) { console.error('[Segmentation] history log error:', e.message); }
2627
2699
 
2628
2700
  return new Response(JSON.stringify({
2629
2701
  success: true,
2630
2702
  algorithm,
2631
- n_clusters: mlResult.clusters.length,
2703
+ engine: 'embeddinggemma-300m + kmeans vetorial',
2704
+ n_clusters: clusters.length,
2632
2705
  client_vertical: clientVertical,
2633
2706
  leads_analyzed: leads.length,
2707
+ sample_embedded: sample.length,
2634
2708
  duration_ms: duration,
2635
- silhouette_score: mlResult.silhouette_score || null,
2636
- clusters: mlResult.clusters,
2709
+ silhouette_score: silhouetteScore,
2710
+ clusters,
2637
2711
  generated_at: now,
2638
2712
  }), { status: 200, headers });
2639
2713
 
@@ -2794,14 +2868,6 @@ async function handleSegmentationUpdate(env, request, headers) {
2794
2868
  // Heurístico puro (sem AI) — latência zero no /track
2795
2869
  // ─────────────────────────────────────────────────────────────────────────────
2796
2870
 
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
2871
  // ASNs conhecidos de datacenters (evitar falsos negativos em ASNs legítimos)
2806
2872
  const DATACENTER_PATTERNS = /amazon|google|microsoft|digitalocean|linode|ovh|vultr|hetzner|contabo|cloudflare|packet|rackspace|leaseweb/i;
2807
2873
 
@@ -2854,15 +2920,7 @@ async function checkFraudGate(env, request, payload) {
2854
2920
  result.score += 20; result.reasons.push('no_accept_language');
2855
2921
  }
2856
2922
 
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
2923
+ // 6. Velocity check via KV
2866
2924
  if (env.GEO_CACHE && ip) {
2867
2925
  const velKey1h = `fraud_velocity:${ip}:h`;
2868
2926
  const velStr = await env.GEO_CACHE.get(velKey1h);
@@ -3839,7 +3897,7 @@ export default {
3839
3897
 
3840
3898
  // Workers AI — ping
3841
3899
  try {
3842
- await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
3900
+ await env.AI.run('@cf/ibm-granite/granite-4.0-h-micro', {
3843
3901
  messages: [{ role: 'user', content: 'ping' }],
3844
3902
  max_tokens: 1,
3845
3903
  });
@@ -25,10 +25,10 @@ zone_name = "lancamentosabc.com.br"
25
25
 
26
26
  # ── Variáveis públicas (não são segredos) ─────────────────────────────────────
27
27
  [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"
28
+ META_PIXEL_ID = "1583939052660159"
29
+ GA4_MEASUREMENT_ID = "G-G7VEN1MNH1"
30
+ TIKTOK_PIXEL_ID = "D71D6T3C77U56RM5VF0G"
31
+ SITE_DOMAIN = "lancamentosabc.com.br"
32
32
 
33
33
  # ── Banco D1 ──────────────────────────────────────────────────────────────────
34
34
  # Após criar o banco com "wrangler d1 create cdp-edge-db",
@@ -95,6 +95,22 @@ namespace_id = "1001"
95
95
  limit = 60
96
96
  period = 60
97
97
 
98
+ # ── Observabilidade — Logs + Traces persistidos no painel Cloudflare ─────────
99
+ [observability]
100
+ enabled = false
101
+ head_sampling_rate = 1
102
+
103
+ [observability.logs]
104
+ enabled = true
105
+ head_sampling_rate = 1
106
+ persist = true
107
+ invocation_logs = true
108
+
109
+ [observability.traces]
110
+ enabled = false
111
+ persist = true
112
+ head_sampling_rate = 1
113
+
98
114
  # ── Secrets (NÃO ficam aqui — configurar via CLI) ─────────────────────────────
99
115
  # wrangler secret put META_ACCESS_TOKEN ← token Meta CAPI (obrigatório)
100
116
  # wrangler secret put GA4_API_SECRET ← secret GA4 Measurement Protocol (obrigatório)
@@ -107,6 +123,7 @@ period = 60
107
123
  # wrangler secret put RESEND_API_KEY ← API Key do Resend (resend.com)
108
124
  # wrangler secret put RESEND_FROM_EMAIL ← Remetente verificado ex: "CDP Edge <noreply@seudominio.com.br>"
109
125
  # wrangler secret put WA_WEBHOOK_VERIFY_TOKEN ← Token de verificação do webhook WhatsApp (você define — qualquer string segura)
126
+ # wrangler secret put WEBHOOK_SECRET_TICTO ← HMAC-SHA256 Ticto
110
127
  # wrangler secret put PINTEREST_ACCESS_TOKEN ← Bearer token Pinterest Conversions API
111
128
  # wrangler secret put PINTEREST_AD_ACCOUNT_ID ← ID da conta de anúncios Pinterest (ex: 549755813XXX)
112
129
  # wrangler secret put REDDIT_ACCESS_TOKEN ← Bearer token Reddit Conversions API