cdp-edge 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Padrão Quantum Tracking: 100% Cloudflare Edge.** Sem GTM. Sem Stape. Sem cookies de terceiros.
4
4
 
5
- > **v2.5.3** — Auditoria de Dependências + Sync de Agentes + Fix D1 (25 de Abril de 2026) 🔧
5
+ > **v2.5.4** — Auditoria de Dependências + Sync de Agentes + Fix D1 (25 de Abril de 2026) 🔧
6
6
 
7
7
  ---
8
8
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_comment": "Fonte de verdade para versões dos agent files. Atualizar quando modules/ ou index.ts mudarem. Use scripts/validate-agents.js para detectar drifts.",
3
- "worker_version": "2.5.3",
4
- "worker_hash_date": "2026-04-15",
3
+ "worker_version": "2.5.4",
4
+ "worker_hash_date": "2026-04-25",
5
5
  "agents": {
6
6
  "master-orchestrator": {
7
7
  "version": "2.0.7",
@@ -140,7 +140,8 @@ const _wbraid = _urlParams.get('wbraid') || ''; // Google Ads (iOS, web-to-app,
140
140
  const _gbraid = _urlParams.get('gbraid') || ''; // Google Ads (app campaigns, privacy preserving)
141
141
 
142
142
  // TikTok
143
- const _ttclid = _urlParams.get('ttclid') || ''; // TikTok Ads click ID → complementa cookie _ttp
143
+ const _ttclid = _urlParams.get('ttclid') || ''; // TikTok Ads click ID → complementa cookie _ttp
144
+ const _msclkid = _urlParams.get('msclkid') || ''; // Microsoft Ads click ID
144
145
 
145
146
  // UTMs — rastreamento interno de origem de tráfego (independente da atribuição das plataformas)
146
147
  const _utms = {
@@ -217,7 +218,8 @@ const _getClickIDs = () => {
217
218
  wbraid: _wbraid || undefined,
218
219
  ttclid: _ttclid || undefined,
219
220
  ttp: document.cookie.match(/_ttp=([^;]+)/)?.[1] || undefined, // TikTok Pixel cookie — EMQ TikTok
220
- rclid: _urlParams.get('rclid') || undefined,
221
+ rclid: _urlParams.get('rclid') || undefined,
222
+ msclkid: _msclkid || undefined,
221
223
  };
222
224
  };
223
225
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdp-edge",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "CDP Edge - Quantum Tracking - Sistema multi-agente para tracking digital Cloudflare Native (Workers + D1)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -63,6 +63,9 @@ import {
63
63
  processWhatsAppWebhook,
64
64
  verifyHmac,
65
65
  } from './modules/dispatch/whatsapp';
66
+ import {
67
+ notifyEvolutionForm,
68
+ } from './modules/dispatch/crm';
66
69
 
67
70
  // ── ML — LTV + A/B Testing ────────────────────────────────────────────────────
68
71
  import {
@@ -239,6 +242,9 @@ export default {
239
242
  WA_NOTIFY_NUMBER: env.WA_NOTIFY_NUMBER ? 'set' : 'not set (optional - only for auto-reply)',
240
243
  TIKTOK_ACCESS_TOKEN: env.TIKTOK_ACCESS_TOKEN ? 'set' : 'not set (optional)',
241
244
  CALLMEBOT_PHONE: env.CALLMEBOT_PHONE ? 'set' : 'not set (optional)',
245
+ EVOLUTION_BASE_URL: env.EVOLUTION_BASE_URL ? 'set' : 'not set (optional - CRM Evolution)',
246
+ EVOLUTION_INSTANCE: env.EVOLUTION_INSTANCE ? 'set' : 'not set (optional - CRM Evolution)',
247
+ EVOLUTION_API_KEY: env.EVOLUTION_API_KEY ? 'set' : 'not set (optional - CRM Evolution)',
242
248
  };
243
249
 
244
250
  const hasMissing =
@@ -389,7 +395,8 @@ export default {
389
395
  if (!trackPayload.wbraid && c.wbraid) trackPayload.wbraid = c.wbraid;
390
396
  if (!trackPayload.gbraid && c.gbraid) trackPayload.gbraid = c.gbraid;
391
397
  if (!trackPayload.ttclid && c.ttclid) trackPayload.ttclid = c.ttclid;
392
- if (!trackPayload.ttp && c.ttp) trackPayload.ttp = c.ttp;
398
+ if (!trackPayload.ttp && c.ttp) trackPayload.ttp = c.ttp;
399
+ if (!trackPayload.msclkid && c.msclkid) trackPayload.msclkid = c.msclkid;
393
400
  }
394
401
  if (payload.utms && typeof payload.utms === 'object') {
395
402
  const u = payload.utms as Record<string, string>;
@@ -761,15 +768,19 @@ export default {
761
768
  ctx.waitUntil(resolveDeviceGraph(env.DB, payload.userId, payload.email, payload.phone));
762
769
  }
763
770
 
764
- // Registrar em events deduplicação + label ML (INSERT OR IGNORE descarta duplicatas)
771
+ // Deduplicação server-side — INSERT OR IGNORE retorna changes=0 para duplicatas
765
772
  if (env.DB && payload.eventId) {
766
- ctx.waitUntil(
767
- env.DB.prepare(
773
+ try {
774
+ const dedup = await env.DB.prepare(
768
775
  `INSERT OR IGNORE INTO events (event_id, event_name, user_id, created_at)
769
776
  VALUES (?, ?, ?, datetime('now'))`
770
- ).bind(payload.eventId, eventName, payload.userId || null).run()
771
- .catch(() => { /* silencioso — não bloqueia o pipeline */ })
772
- );
777
+ ).bind(payload.eventId, eventName, payload.userId || null).run();
778
+ if (dedup.meta.changes === 0) {
779
+ return new Response(JSON.stringify({ ok: true, skipped: 'duplicate' }), { status: 200, headers });
780
+ }
781
+ } catch {
782
+ // Tabela ausente ou erro de DB — não bloqueia o pipeline
783
+ }
773
784
  }
774
785
 
775
786
  // R2 Audit Log — background
@@ -788,6 +799,25 @@ export default {
788
799
  : []),
789
800
  ]);
790
801
 
802
+ // ── Notifica o Evolution CRM (formulários de cadastro) ────────────────
803
+ // Ativo para Lead e Contact vindos de formulários (quando tem telefone).
804
+ // Silencioso se EVOLUTION_BASE_URL / EVOLUTION_INSTANCE / EVOLUTION_API_KEY
805
+ // não estiverem configurados — zero impacto no pipeline principal.
806
+ const EVOLUTION_FORM_EVENTS = ['Lead', 'Contact', 'CompleteRegistration'];
807
+ if (EVOLUTION_FORM_EVENTS.includes(eventName) && trackPayload.phone) {
808
+ ctx.waitUntil(
809
+ notifyEvolutionForm(env, {
810
+ phone: trackPayload.phone,
811
+ name: [trackPayload.firstName, trackPayload.lastName].filter(Boolean).join(' ') || null,
812
+ email: trackPayload.email || null,
813
+ formName: trackPayload.contentName || trackPayload.productName || eventName,
814
+ utmSource: trackPayload.utmSource || null,
815
+ utmCampaign: trackPayload.utmCampaign || null,
816
+ pageUrl: trackPayload.pageUrl || null,
817
+ })
818
+ );
819
+ }
820
+
791
821
  // Automação de mensagens
792
822
  const AUTOMATION_EVENTS = ['Lead', 'Purchase', 'InitiateCheckout'];
793
823
  if (AUTOMATION_EVENTS.includes(eventName) && env.DB) {
@@ -115,6 +115,7 @@ export async function upsertProfile(env: Env, eventName: string, payload: TrackP
115
115
  const {
116
116
  userId, email, phone,
117
117
  fbp, fbc, ttp, gclid, ttclid, gaClientId,
118
+ wbraid, gbraid, msclkid,
118
119
  city, state, country,
119
120
  engagementScore, userScore,
120
121
  } = payload;
@@ -131,8 +132,9 @@ export async function upsertProfile(env: Env, eventName: string, payload: TrackP
131
132
  await env.DB.prepare(`
132
133
  INSERT INTO user_profiles
133
134
  (user_id, email, phone, fbp, fbc, ttp, gclid, ttclid, ga_client_id,
135
+ wbraid, gbraid, msclkid,
134
136
  city, state, country, score, cohort_label, created_at, updated_at)
135
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'),datetime('now'))
137
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'),datetime('now'))
136
138
  ON CONFLICT(user_id) DO UPDATE SET
137
139
  email = COALESCE(excluded.email, user_profiles.email),
138
140
  phone = COALESCE(excluded.phone, user_profiles.phone),
@@ -142,6 +144,9 @@ export async function upsertProfile(env: Env, eventName: string, payload: TrackP
142
144
  gclid = COALESCE(excluded.gclid, user_profiles.gclid),
143
145
  ttclid = COALESCE(excluded.ttclid, user_profiles.ttclid),
144
146
  ga_client_id = COALESCE(excluded.ga_client_id, user_profiles.ga_client_id),
147
+ wbraid = COALESCE(excluded.wbraid, user_profiles.wbraid),
148
+ gbraid = COALESCE(excluded.gbraid, user_profiles.gbraid),
149
+ msclkid = COALESCE(excluded.msclkid, user_profiles.msclkid),
145
150
  city = COALESCE(excluded.city, user_profiles.city),
146
151
  state = COALESCE(excluded.state, user_profiles.state),
147
152
  country = COALESCE(excluded.country, user_profiles.country),
@@ -158,6 +163,9 @@ export async function upsertProfile(env: Env, eventName: string, payload: TrackP
158
163
  gclid || null,
159
164
  ttclid || null,
160
165
  gaClientId || null,
166
+ wbraid || null,
167
+ gbraid || null,
168
+ msclkid || null,
161
169
  city || null,
162
170
  state || null,
163
171
  (country || (request as any).cf?.country || null),
@@ -0,0 +1,116 @@
1
+ /**
2
+ * CDP Edge — Evolution GO Integration
3
+ * Encaminha leads para o Evolution GO, que dispara o webhook para o EVO CRM.
4
+ *
5
+ * Secrets necessários (wrangler secret put):
6
+ * EVOLUTION_BASE_URL → URL base (ex: https://go.arkitekt.space)
7
+ * EVOLUTION_INSTANCE → Nome da instância (ex: Ramon)
8
+ * EVOLUTION_API_KEY → API Key do Evolution GO
9
+ */
10
+
11
+ import { Env } from '../../types.js';
12
+
13
+ export interface CTWALeadData {
14
+ phone: string;
15
+ messageBody?: string;
16
+ ctwaClid?: string | null;
17
+ adId?: string | null;
18
+ sourceUrl?: string | null;
19
+ headline?: string | null;
20
+ wamid?: string | null;
21
+ }
22
+
23
+ export interface FormLeadData {
24
+ phone: string;
25
+ name?: string | null;
26
+ email?: string | null;
27
+ formName?: string | null;
28
+ utmSource?: string | null;
29
+ utmCampaign?: string | null;
30
+ pageUrl?: string | null;
31
+ }
32
+
33
+ // ── Helpers ──────────────────────────────────────────────────────────────────
34
+
35
+ function isEvolutionConfigured(env: Env): boolean {
36
+ return !!(env.EVOLUTION_BASE_URL && env.EVOLUTION_INSTANCE && env.EVOLUTION_API_KEY);
37
+ }
38
+
39
+ function evolutionHeaders(env: Env): Record<string, string> {
40
+ return {
41
+ 'Content-Type': 'application/json',
42
+ 'apikey': env.EVOLUTION_API_KEY!,
43
+ };
44
+ }
45
+
46
+ function normalizePhone(phone: string): string {
47
+ const digits = phone.replace(/\D/g, '');
48
+ if (digits.startsWith('55') && (digits.length === 12 || digits.length === 13)) return digits;
49
+ if (digits.length === 10 || digits.length === 11) return `55${digits}`;
50
+ return digits;
51
+ }
52
+
53
+ // ── Envio via Evolution GO /send/text ────────────────────────────────────────
54
+ // POST {EVOLUTION_BASE_URL}/send/text
55
+ // Body: { id: instanceName, number, text, delay }
56
+
57
+ async function sendEvolutionText(env: Env, phone: string, text: string): Promise<void> {
58
+ const number = normalizePhone(phone);
59
+ try {
60
+ const res = await fetch(`${env.EVOLUTION_BASE_URL}/send/text`, {
61
+ method: 'POST',
62
+ headers: evolutionHeaders(env),
63
+ body: JSON.stringify({
64
+ id: env.EVOLUTION_INSTANCE,
65
+ number,
66
+ text,
67
+ delay: 0,
68
+ }),
69
+ });
70
+ if (!res.ok) {
71
+ const err = await res.text();
72
+ console.warn('[Evolution] sendText non-ok:', res.status, err.slice(0, 200));
73
+ }
74
+ } catch (err: any) {
75
+ console.warn('[Evolution] sendText failed (non-critical):', err?.message || String(err));
76
+ }
77
+ }
78
+
79
+ // ── API Pública — CTWA ────────────────────────────────────────────────────────
80
+
81
+ export async function notifyEvolutionCTWA(env: Env, data: CTWALeadData): Promise<void> {
82
+ if (!isEvolutionConfigured(env)) return;
83
+
84
+ const { phone, messageBody, ctwaClid, adId, sourceUrl, headline } = data;
85
+
86
+ const linhas: string[] = [`🔔 *Novo Lead via WhatsApp (CTWA)*`, ``];
87
+ linhas.push(`📱 *Número:* ${phone}`);
88
+ if (messageBody) linhas.push(`💬 *Mensagem:* ${messageBody}`);
89
+ if (headline) linhas.push(`📢 *Anúncio:* ${headline}`);
90
+ if (sourceUrl) linhas.push(`🔗 *URL:* ${sourceUrl}`);
91
+ if (adId) linhas.push(`🆔 *Ad ID:* ${adId}`);
92
+ if (ctwaClid) linhas.push(`🧩 *CTWA ID:* ${ctwaClid}`);
93
+ linhas.push(``, `🕐 ${new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' })}`);
94
+
95
+ await sendEvolutionText(env, phone, linhas.join('\n'));
96
+ }
97
+
98
+ // ── API Pública — Formulário ──────────────────────────────────────────────────
99
+
100
+ export async function notifyEvolutionForm(env: Env, data: FormLeadData): Promise<void> {
101
+ if (!isEvolutionConfigured(env)) return;
102
+
103
+ const { phone, name, email, formName, utmSource, utmCampaign, pageUrl } = data;
104
+
105
+ const linhas: string[] = [`📋 *Novo Lead via Formulário*`, ``];
106
+ if (name) linhas.push(`👤 *Nome:* ${name}`);
107
+ if (email) linhas.push(`📧 *E-mail:* ${email}`);
108
+ linhas.push( `📱 *Telefone:* ${phone}`);
109
+ if (formName) linhas.push(`📝 *Formulário:* ${formName}`);
110
+ if (utmSource) linhas.push(`🔗 *Fonte:* ${utmSource}`);
111
+ if (utmCampaign) linhas.push(`📣 *Campanha:* ${utmCampaign}`);
112
+ if (pageUrl) linhas.push(`🌐 *Página:* ${pageUrl}`);
113
+ linhas.push(``, `🕐 ${new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' })}`);
114
+
115
+ await sendEvolutionText(env, phone, linhas.join('\n'));
116
+ }
@@ -7,6 +7,7 @@ import { sha256, normalizePhone } from '../utils.js';
7
7
  import { saveLead, logApiFailure } from '../db.js';
8
8
  import { Env, TrackPayload } from '../../types.js';
9
9
  import { ExecutionContext } from '@cloudflare/workers-types';
10
+ import { notifyEvolutionCTWA } from './crm.js';
10
11
 
11
12
  // ── Tipos ───────────────────────────────────────────────────────────────────────
12
13
  interface WhatsAppOptions {
@@ -248,6 +249,22 @@ export async function processWhatsAppWebhook(env: Env, body: any, request: Reque
248
249
  }, request, 'whatsapp')
249
250
  );
250
251
 
252
+ // ── Notifica o Evolution CRM sobre o novo lead CTWA ──────────────────────
253
+ // Cria/atualiza o contato no Evolution e abre a conversa para o vendedor.
254
+ // Silencioso se EVOLUTION_BASE_URL / EVOLUTION_INSTANCE / EVOLUTION_API_KEY
255
+ // não estiverem configurados.
256
+ ctx.waitUntil(
257
+ notifyEvolutionCTWA(env, {
258
+ phone: phoneNorm,
259
+ messageBody: messageBody || undefined,
260
+ ctwaClid,
261
+ adId,
262
+ sourceUrl,
263
+ headline,
264
+ wamid,
265
+ })
266
+ );
267
+
251
268
  results.push({ ok: true, phone: phoneNorm.slice(0, 4) + '****', ctwa_clid: ctwaClid ? 'present' : 'absent', event_id: eventId });
252
269
  }
253
270
 
@@ -64,6 +64,11 @@ export interface Env {
64
64
  RESEND_API_KEY?: string;
65
65
  RESEND_FROM_EMAIL?: string;
66
66
  CALLMEBOT_APIKEY?: string;
67
+
68
+ // Evolution API CRM
69
+ EVOLUTION_BASE_URL?: string; // URL base do servidor Evolution (ex: https://evolution.suaempresa.com)
70
+ EVOLUTION_INSTANCE?: string; // Nome da instância WhatsApp no Evolution
71
+ EVOLUTION_API_KEY?: string; // Chave de autenticação da API Evolution
67
72
  }
68
73
 
69
74
  // ── Event Payload Types ───────────────────────────────────────────────────────
@@ -16,13 +16,13 @@ workers_dev = true
16
16
  # pattern = "*.SEU_DOMINIO/track*"
17
17
  # zone_name = "SEU_DOMINIO"
18
18
 
19
- [[routes]]
20
- pattern = "SEU_DOMINIO/track*"
21
- zone_name = "SEU_DOMINIO"
22
-
23
- [[routes]]
24
- pattern = "*.SEU_DOMINIO/track*"
25
- zone_name = "SEU_DOMINIO"
19
+ # [[routes]]
20
+ # pattern = "lancamentosabc.com.br/track*"
21
+ # zone_name = "lancamentosabc.com.br"
22
+ #
23
+ # [[routes]]
24
+ # pattern = "*.lancamentosabc.com.br/track*"
25
+ # zone_name = "lancamentosabc.com.br"
26
26
 
27
27
  # ── Variáveis públicas (não são segredos) ─────────────────────────────────────
28
28
  [vars]
@@ -134,3 +134,6 @@ head_sampling_rate = 1
134
134
  # wrangler secret put LINKEDIN_AD_ACCOUNT_ID ← ID da conta de anúncios LinkedIn
135
135
  # wrangler secret put SPOTIFY_ACCESS_TOKEN ← Bearer token Spotify Advertising API
136
136
  # wrangler secret put SPOTIFY_AD_ACCOUNT_ID ← ID da conta de anúncios Spotify Ads
137
+ # wrangler secret put EVOLUTION_BASE_URL ← URL base do Evolution API (ex: https://evolution.suaempresa.com)
138
+ # wrangler secret put EVOLUTION_INSTANCE ← Nome da instância WhatsApp no Evolution
139
+ # wrangler secret put EVOLUTION_API_KEY ← Chave de autenticação do Evolution API