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 +1 -1
- package/contracts/agent-versions.json +2 -2
- package/extracted-skill/tracking-events-generator/cdpTrack.js +4 -2
- package/package.json +1 -1
- package/server-edge-tracker/index.ts +37 -7
- package/server-edge-tracker/modules/db.ts +9 -1
- package/server-edge-tracker/modules/dispatch/crm.ts +116 -0
- package/server-edge-tracker/modules/dispatch/whatsapp.ts +17 -0
- package/server-edge-tracker/types.ts +5 -0
- package/server-edge-tracker/wrangler.toml +10 -7
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.
|
|
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.
|
|
4
|
-
"worker_hash_date": "2026-04-
|
|
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
|
|
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:
|
|
221
|
+
rclid: _urlParams.get('rclid') || undefined,
|
|
222
|
+
msclkid: _msclkid || undefined,
|
|
221
223
|
};
|
|
222
224
|
};
|
|
223
225
|
|
package/package.json
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
771
|
+
// Deduplicação server-side — INSERT OR IGNORE retorna changes=0 para duplicatas
|
|
765
772
|
if (env.DB && payload.eventId) {
|
|
766
|
-
|
|
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
|
-
|
|
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 (
|
|
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 = "
|
|
21
|
-
zone_name = "
|
|
22
|
-
|
|
23
|
-
[[routes]]
|
|
24
|
-
pattern = "*.
|
|
25
|
-
zone_name = "
|
|
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
|