cdp-edge 1.2.2 → 1.3.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 +153 -306
- package/bin/cdp-edge.js +71 -61
- package/contracts/agent-versions.json +682 -0
- package/contracts/api-versions.json +372 -368
- package/contracts/types.ts +81 -0
- package/dist/commands/analyze.js +52 -52
- package/dist/commands/infra.js +54 -54
- package/dist/commands/install.js +26 -3
- package/dist/commands/server.js +174 -174
- package/dist/commands/setup.js +332 -100
- package/dist/commands/validate.js +248 -84
- package/dist/index.js +12 -12
- package/dist/sdk/cdpTrack.js +2095 -0
- package/dist/sdk/cdpTrack.min.js +64 -0
- package/dist/sdk/install-snippet.html +10 -0
- package/docs/whatsapp-ctwa.md +5 -4
- package/extracted-skill/tracking-events-generator/INTEGRACAO-COMPLETA.md +89 -0
- package/extracted-skill/tracking-events-generator/MELHORIAS-IMPLEMENTADAS.md +101 -0
- package/extracted-skill/tracking-events-generator/advanced-matching.js +364 -364
- package/extracted-skill/tracking-events-generator/agents/ab-ltv-agent.md +196 -0
- package/extracted-skill/tracking-events-generator/agents/ab-testing-agent.md +1 -1
- package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +41 -41
- package/extracted-skill/tracking-events-generator/agents/bidding-agent.md +347 -0
- package/extracted-skill/tracking-events-generator/agents/bing-agent.md +40 -50
- package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +174 -74
- package/extracted-skill/tracking-events-generator/agents/code-guardian-agent.md +1 -1
- package/extracted-skill/tracking-events-generator/agents/compliance-agent.md +25 -5
- package/extracted-skill/tracking-events-generator/agents/dashboard-agent.md +10 -10
- package/extracted-skill/tracking-events-generator/agents/database-agent.md +43 -42
- package/extracted-skill/tracking-events-generator/agents/debug-agent.md +22 -22
- package/extracted-skill/tracking-events-generator/agents/devops-agent.md +232 -0
- package/extracted-skill/tracking-events-generator/agents/domain-setup-agent.md +23 -9
- package/extracted-skill/tracking-events-generator/agents/email-agent.md +28 -1
- package/extracted-skill/tracking-events-generator/agents/evo-crm-agent.md +244 -0
- package/extracted-skill/tracking-events-generator/agents/fingerprint-agent.md +206 -1
- package/extracted-skill/tracking-events-generator/agents/fraud-detection-agent.md +143 -0
- package/extracted-skill/tracking-events-generator/agents/google-agent.md +128 -2
- package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +191 -31
- package/extracted-skill/tracking-events-generator/agents/lead-scoring-agent.md +282 -0
- package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +145 -34
- package/extracted-skill/tracking-events-generator/agents/localization-agent.md +1 -1
- package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +5 -5
- package/extracted-skill/tracking-events-generator/agents/master-feedback-loop.md +81 -21
- package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +313 -93
- package/extracted-skill/tracking-events-generator/agents/match-quality-agent.md +304 -0
- package/extracted-skill/tracking-events-generator/agents/memory-agent.md +190 -15
- package/extracted-skill/tracking-events-generator/agents/meta-agent.md +10 -2
- package/extracted-skill/tracking-events-generator/agents/ml-clustering-agent.md +749 -0
- package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +21 -4
- package/extracted-skill/tracking-events-generator/agents/performance-agent.md +41 -31
- package/extracted-skill/tracking-events-generator/agents/performance-optimization-agent.md +18 -8
- package/extracted-skill/tracking-events-generator/agents/pinterest-agent.md +14 -6
- package/extracted-skill/tracking-events-generator/agents/premium-tracking-intelligence-agent.md +7 -7
- package/extracted-skill/tracking-events-generator/agents/r2-setup-agent.md +16 -8
- package/extracted-skill/tracking-events-generator/agents/reddit-agent.md +15 -7
- package/extracted-skill/tracking-events-generator/agents/security-enterprise-agent.md +157 -48
- package/extracted-skill/tracking-events-generator/agents/server-tracking.md +35 -35
- package/extracted-skill/tracking-events-generator/agents/spotify-agent.md +15 -7
- package/extracted-skill/tracking-events-generator/agents/tiktok-agent.md +73 -2
- package/extracted-skill/tracking-events-generator/agents/tracking-plan-agent.md +104 -9
- package/extracted-skill/tracking-events-generator/agents/utm-agent.md +322 -0
- package/extracted-skill/tracking-events-generator/agents/validator-agent.md +13 -9
- package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +112 -4
- package/extracted-skill/tracking-events-generator/agents/whatsapp-agent.md +58 -5
- package/extracted-skill/tracking-events-generator/agents/whatsapp-ctwa-setup-agent.md +26 -18
- package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +152 -37
- package/extracted-skill/tracking-events-generator/anti-blocking.js +285 -285
- package/extracted-skill/tracking-events-generator/cdpTrack.js +642 -641
- package/extracted-skill/tracking-events-generator/contracts/api-versions.json +14 -10
- package/extracted-skill/tracking-events-generator/engagement-scoring.js +226 -226
- package/extracted-skill/tracking-events-generator/evals/evals.json +235 -235
- package/extracted-skill/tracking-events-generator/integration-test.js +497 -497
- package/extracted-skill/tracking-events-generator/knowledge-base.md +172 -0
- package/extracted-skill/tracking-events-generator/micro-events.js +992 -992
- package/extracted-skill/tracking-events-generator/models/lancamento-imobiliario.md +344 -0
- package/extracted-skill/tracking-events-generator/models/pinterest/conversions-api-template.js +144 -144
- package/extracted-skill/tracking-events-generator/models/pinterest/event-mappings.json +48 -48
- package/extracted-skill/tracking-events-generator/models/pinterest/tag-template.js +28 -28
- package/extracted-skill/tracking-events-generator/models/quiz-funnel.md +83 -19
- package/extracted-skill/tracking-events-generator/models/reddit/conversions-api-template.js +205 -205
- package/extracted-skill/tracking-events-generator/models/reddit/event-mappings.json +56 -56
- package/extracted-skill/tracking-events-generator/models/reddit/pixel-template.js +19 -19
- package/extracted-skill/tracking-events-generator/models/scenarios/behavior-engine.js +425 -425
- package/extracted-skill/tracking-events-generator/route-intent-capture.js +222 -0
- package/extracted-skill/tracking-events-generator/tracking.config.js +3 -3
- package/package.json +89 -75
- package/scripts/build-sdk.js +106 -0
- package/server-edge-tracker/.client.env.example +14 -0
- package/server-edge-tracker/INSTALAR.md +222 -23
- package/server-edge-tracker/SEGMENTATION-DOCS.md +513 -0
- package/server-edge-tracker/config/utm-mapping.json +64 -0
- package/server-edge-tracker/deploy-client.cjs +76 -0
- package/server-edge-tracker/index.ts +1230 -0
- package/server-edge-tracker/migrate-v7.sql +64 -0
- package/server-edge-tracker/modules/db.ts +710 -0
- package/server-edge-tracker/modules/dispatch/crm.ts +382 -0
- package/server-edge-tracker/modules/dispatch/ga4.ts +72 -0
- package/server-edge-tracker/modules/dispatch/meta.ts +143 -0
- package/server-edge-tracker/modules/dispatch/platforms.ts +255 -0
- package/server-edge-tracker/modules/dispatch/tiktok.ts +107 -0
- package/server-edge-tracker/modules/dispatch/whatsapp.ts +296 -0
- package/server-edge-tracker/modules/intelligence.ts +589 -0
- package/server-edge-tracker/modules/ml/bidding.ts +247 -0
- package/server-edge-tracker/modules/ml/fraud.ts +302 -0
- package/server-edge-tracker/modules/ml/logistic.ts +226 -0
- package/server-edge-tracker/modules/ml/ltv.ts +531 -0
- package/server-edge-tracker/modules/ml/matchquality.ts +232 -0
- package/server-edge-tracker/modules/ml/quiz.ts +343 -0
- package/server-edge-tracker/modules/ml/roas.ts +255 -0
- package/server-edge-tracker/modules/ml/segmentation.ts +407 -0
- package/server-edge-tracker/modules/nurture.ts +257 -0
- package/server-edge-tracker/modules/utils.ts +311 -0
- package/server-edge-tracker/modules/utm/utm-enricher.ts +231 -0
- package/server-edge-tracker/schema-ab-ltv.sql +97 -0
- package/server-edge-tracker/schema-bidding.sql +86 -0
- package/server-edge-tracker/schema-fraud.sql +90 -0
- package/server-edge-tracker/schema-indexes.sql +67 -0
- package/server-edge-tracker/schema-ltv-feedback.sql +11 -0
- package/server-edge-tracker/schema-quiz.sql +52 -0
- package/server-edge-tracker/schema-sales-engine.sql +113 -0
- package/server-edge-tracker/schema-segmentation.sql +219 -0
- package/server-edge-tracker/schema-utm.sql +82 -0
- package/server-edge-tracker/schema.sql +281 -265
- package/server-edge-tracker/types.ts +275 -0
- package/server-edge-tracker/wrangler.toml +140 -85
- package/templates/lancamento-imobiliario.md +344 -0
- package/templates/multi-step-checkout.md +3 -4
- package/templates/pinterest/conversions-api-template.js +144 -144
- package/templates/pinterest/event-mappings.json +48 -48
- package/templates/pinterest/tag-template.js +28 -28
- package/templates/quiz-funnel.md +83 -19
- package/templates/reddit/conversions-api-template.js +205 -205
- package/templates/reddit/event-mappings.json +56 -56
- package/templates/reddit/pixel-template.js +12 -39
- package/templates/scenarios/behavior-engine.js +45 -22
- package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
- package/docs/installation.md +0 -155
- package/docs/quick-start.md +0 -185
- package/extracted-skill/tracking-events-generator/agents/crm-integration-agent.md +0 -1419
- package/extracted-skill/tracking-events-generator/agents/intelligence-scheduling.md +0 -643
- package/server-edge-tracker/worker.js +0 -2574
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP Edge — EVO CRM Premium Integration
|
|
3
|
+
* Fluxo: Meta CAPI → Worker → Contato + Conversa + Nota interna no EVO CRM
|
|
4
|
+
*
|
|
5
|
+
* Secrets necessários (wrangler secret put):
|
|
6
|
+
* EVO_CRM_BASE_URL → URL base do EVO CRM (ex: https://api-evocrm.suaempresa.com)
|
|
7
|
+
* EVO_CRM_CLIENT_ID → OAuth client_id (Doorkeeper app no CRM)
|
|
8
|
+
* EVO_CRM_CLIENT_SECRET → OAuth client_secret
|
|
9
|
+
* EVO_CRM_INBOX_ID → ID do inbox onde as conversas serão criadas
|
|
10
|
+
*
|
|
11
|
+
* Secrets opcionais (defaults BR):
|
|
12
|
+
* EVO_CRM_DEFAULT_COUNTRY → Dial code para números locais sem +cc (default "55")
|
|
13
|
+
* EVO_CRM_LOCALE → "pt-BR" | "en-US" | "es-ES" (default "pt-BR")
|
|
14
|
+
*
|
|
15
|
+
* Uso (qualquer origem de lead):
|
|
16
|
+
* await pushLeadToCrm(env, {
|
|
17
|
+
* phone: '5511999990000',
|
|
18
|
+
* name: 'João Silva',
|
|
19
|
+
* email: 'joao@exemplo.com',
|
|
20
|
+
* utmSource: 'facebook',
|
|
21
|
+
* utmCampaign: 'campanha-x',
|
|
22
|
+
* fbclid: 'AbC123...',
|
|
23
|
+
* intentScore: 85,
|
|
24
|
+
* ltvClass: 'high',
|
|
25
|
+
* pageUrl: 'https://exemplo.com/lp',
|
|
26
|
+
* });
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { Env } from '../../types.js';
|
|
30
|
+
|
|
31
|
+
// ── Tipos públicos ────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export interface CrmLeadData {
|
|
34
|
+
// Identificação
|
|
35
|
+
phone: string;
|
|
36
|
+
name?: string | null;
|
|
37
|
+
email?: string | null;
|
|
38
|
+
|
|
39
|
+
// Meta CAPI
|
|
40
|
+
fbclid?: string | null;
|
|
41
|
+
fbc?: string | null;
|
|
42
|
+
fbp?: string | null;
|
|
43
|
+
ctwaClid?: string | null;
|
|
44
|
+
adId?: string | null;
|
|
45
|
+
messageBody?: string | null;
|
|
46
|
+
headline?: string | null;
|
|
47
|
+
|
|
48
|
+
// UTMs
|
|
49
|
+
utmSource?: string | null;
|
|
50
|
+
utmMedium?: string | null;
|
|
51
|
+
utmCampaign?: string | null;
|
|
52
|
+
utmContent?: string | null;
|
|
53
|
+
utmTerm?: string | null;
|
|
54
|
+
|
|
55
|
+
// Contexto
|
|
56
|
+
eventName?: string | null;
|
|
57
|
+
pageUrl?: string | null;
|
|
58
|
+
formName?: string | null;
|
|
59
|
+
|
|
60
|
+
// Scoring (Quantum Tracking)
|
|
61
|
+
intentScore?: number | null;
|
|
62
|
+
ltvClass?: string | null;
|
|
63
|
+
funnelStage?: string | null;
|
|
64
|
+
botScore?: number | null;
|
|
65
|
+
|
|
66
|
+
// Monetário
|
|
67
|
+
value?: number | null;
|
|
68
|
+
currency?: string | null;
|
|
69
|
+
|
|
70
|
+
// Atributos livres adicionais
|
|
71
|
+
attributes?: Record<string, string>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @deprecated Use CrmLeadData + pushLeadToCrm */
|
|
75
|
+
export interface CTWALeadData {
|
|
76
|
+
phone: string;
|
|
77
|
+
name?: string | null;
|
|
78
|
+
messageBody?: string | null;
|
|
79
|
+
ctwaClid?: string | null;
|
|
80
|
+
adId?: string | null;
|
|
81
|
+
sourceUrl?: string | null;
|
|
82
|
+
headline?: string | null;
|
|
83
|
+
wamid?: string | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** @deprecated Use CrmLeadData + pushLeadToCrm */
|
|
87
|
+
export interface FormLeadData {
|
|
88
|
+
phone: string;
|
|
89
|
+
name?: string | null;
|
|
90
|
+
email?: string | null;
|
|
91
|
+
formName?: string | null;
|
|
92
|
+
utmSource?: string | null;
|
|
93
|
+
utmCampaign?: string | null;
|
|
94
|
+
pageUrl?: string | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Helpers internos ──────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
function isCrmConfigured(env: Env): boolean {
|
|
100
|
+
return !!(env.EVO_CRM_BASE_URL && env.EVO_CRM_CLIENT_ID && env.EVO_CRM_CLIENT_SECRET);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizePhone(phone: string, defaultCountryCode = '55'): string {
|
|
104
|
+
const trimmed = phone.trim();
|
|
105
|
+
const cc = (defaultCountryCode || '55').replace(/\D/g, '') || '55';
|
|
106
|
+
|
|
107
|
+
// Already in E.164 (starts with +): só limpa e re-prefixa
|
|
108
|
+
if (trimmed.startsWith('+')) return '+' + trimmed.replace(/\D/g, '');
|
|
109
|
+
|
|
110
|
+
const digits = trimmed.replace(/\D/g, '');
|
|
111
|
+
|
|
112
|
+
// Comportamento BR canônico (preservado): 55 + 10 ou 11 dígitos = válido
|
|
113
|
+
if (cc === '55' && digits.startsWith('55') && (digits.length === 12 || digits.length === 13)) {
|
|
114
|
+
return `+${digits}`;
|
|
115
|
+
}
|
|
116
|
+
if (cc === '55' && (digits.length === 10 || digits.length === 11)) {
|
|
117
|
+
return `+55${digits}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Genérico: já tem o country code
|
|
121
|
+
if (digits.startsWith(cc) && digits.length > cc.length + 6) return `+${digits}`;
|
|
122
|
+
|
|
123
|
+
// Genérico: número local → prepend country code
|
|
124
|
+
if (digits.length >= 7 && digits.length <= 11) return `+${cc}${digits}`;
|
|
125
|
+
|
|
126
|
+
// Fallback: assume que já tem algum cc
|
|
127
|
+
return `+${digits}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Localização da nota interna ───────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
type Locale = 'pt-BR' | 'en-US' | 'es-ES';
|
|
133
|
+
|
|
134
|
+
const NOTE_LABELS: Record<Locale, Record<string, string>> = {
|
|
135
|
+
'pt-BR': {
|
|
136
|
+
title: '📊 *Novo Lead*',
|
|
137
|
+
name: 'Nome',
|
|
138
|
+
source: 'Origem',
|
|
139
|
+
campaign: 'Campanha',
|
|
140
|
+
form: 'Formulário',
|
|
141
|
+
ad: 'Anúncio',
|
|
142
|
+
message: 'Mensagem',
|
|
143
|
+
score: 'Score',
|
|
144
|
+
ltv: 'LTV',
|
|
145
|
+
},
|
|
146
|
+
'en-US': {
|
|
147
|
+
title: '📊 *New Lead*',
|
|
148
|
+
name: 'Name',
|
|
149
|
+
source: 'Source',
|
|
150
|
+
campaign: 'Campaign',
|
|
151
|
+
form: 'Form',
|
|
152
|
+
ad: 'Ad',
|
|
153
|
+
message: 'Message',
|
|
154
|
+
score: 'Score',
|
|
155
|
+
ltv: 'LTV',
|
|
156
|
+
},
|
|
157
|
+
'es-ES': {
|
|
158
|
+
title: '📊 *Nuevo Lead*',
|
|
159
|
+
name: 'Nombre',
|
|
160
|
+
source: 'Origen',
|
|
161
|
+
campaign: 'Campaña',
|
|
162
|
+
form: 'Formulario',
|
|
163
|
+
ad: 'Anuncio',
|
|
164
|
+
message: 'Mensaje',
|
|
165
|
+
score: 'Score',
|
|
166
|
+
ltv: 'LTV',
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
function resolveLocale(input?: string | null): Locale {
|
|
171
|
+
if (input && input in NOTE_LABELS) return input as Locale;
|
|
172
|
+
return 'pt-BR';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function getAccessToken(env: Env): Promise<string | null> {
|
|
176
|
+
try {
|
|
177
|
+
const res = await fetch(`${env.EVO_CRM_BASE_URL}/oauth/token`, {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
180
|
+
body: new URLSearchParams({
|
|
181
|
+
grant_type: 'client_credentials',
|
|
182
|
+
client_id: env.EVO_CRM_CLIENT_ID!,
|
|
183
|
+
client_secret: env.EVO_CRM_CLIENT_SECRET!,
|
|
184
|
+
}),
|
|
185
|
+
});
|
|
186
|
+
if (!res.ok) return null;
|
|
187
|
+
const data: any = await res.json();
|
|
188
|
+
return data.access_token ?? null;
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Nota interna para o agente: apenas o essencial para atender o lead. */
|
|
195
|
+
function buildAgentNote(data: CrmLeadData, locale: Locale = 'pt-BR'): string {
|
|
196
|
+
const L = NOTE_LABELS[locale];
|
|
197
|
+
const lines: string[] = [L.title];
|
|
198
|
+
|
|
199
|
+
const name = data.name || data.phone;
|
|
200
|
+
lines.push(`• ${L.name}: ${name}`);
|
|
201
|
+
|
|
202
|
+
const source = [data.utmSource, data.utmMedium].filter(Boolean).join(' / ');
|
|
203
|
+
if (source) lines.push(`• ${L.source}: ${source}`);
|
|
204
|
+
|
|
205
|
+
if (data.utmCampaign) lines.push(`• ${L.campaign}: ${data.utmCampaign}`);
|
|
206
|
+
if (data.formName && data.formName !== data.eventName) lines.push(`• ${L.form}: ${data.formName}`);
|
|
207
|
+
if (data.headline) lines.push(`• ${L.ad}: ${data.headline}`);
|
|
208
|
+
if (data.messageBody) lines.push(`• ${L.message}: "${data.messageBody}"`);
|
|
209
|
+
|
|
210
|
+
if (data.intentScore != null) {
|
|
211
|
+
const score = data.intentScore > 1 ? data.intentScore : Math.round(data.intentScore * 100);
|
|
212
|
+
const stage = data.funnelStage ? ` · ${data.funnelStage}` : '';
|
|
213
|
+
lines.push(`• ${L.score}: ${score}${stage}`);
|
|
214
|
+
}
|
|
215
|
+
if (data.ltvClass) lines.push(`• ${L.ltv}: ${data.ltvClass}`);
|
|
216
|
+
|
|
217
|
+
return lines.join('\n');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Monta additional_attributes para o contato no CRM. */
|
|
221
|
+
function buildContactAttributes(data: CrmLeadData): Record<string, string> {
|
|
222
|
+
const attrs: Record<string, string> = {};
|
|
223
|
+
|
|
224
|
+
if (data.utmSource) attrs['utm_source'] = data.utmSource;
|
|
225
|
+
if (data.utmMedium) attrs['utm_medium'] = data.utmMedium;
|
|
226
|
+
if (data.utmCampaign) attrs['utm_campaign'] = data.utmCampaign;
|
|
227
|
+
if (data.utmContent) attrs['utm_content'] = data.utmContent;
|
|
228
|
+
if (data.utmTerm) attrs['utm_term'] = data.utmTerm;
|
|
229
|
+
if (data.pageUrl) attrs['pagina'] = data.pageUrl;
|
|
230
|
+
if (data.formName) attrs['formulario'] = data.formName;
|
|
231
|
+
if (data.fbclid) attrs['fbclid'] = data.fbclid;
|
|
232
|
+
if (data.fbc) attrs['fbc'] = data.fbc;
|
|
233
|
+
if (data.fbp) attrs['fbp'] = data.fbp;
|
|
234
|
+
if (data.ctwaClid) attrs['ctwa_clid'] = data.ctwaClid;
|
|
235
|
+
if (data.adId) attrs['ad_id'] = data.adId;
|
|
236
|
+
if (data.messageBody) attrs['mensagem'] = data.messageBody;
|
|
237
|
+
if (data.headline) attrs['anuncio'] = data.headline;
|
|
238
|
+
if (data.ltvClass) attrs['ltv_class'] = data.ltvClass;
|
|
239
|
+
if (data.funnelStage) attrs['funil'] = data.funnelStage;
|
|
240
|
+
if (data.intentScore != null) attrs['intencao'] = String(data.intentScore);
|
|
241
|
+
if (data.eventName) attrs['evento'] = data.eventName;
|
|
242
|
+
|
|
243
|
+
// Atributos livres
|
|
244
|
+
if (data.attributes) Object.assign(attrs, data.attributes);
|
|
245
|
+
|
|
246
|
+
return attrs;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── API principal (recomendada) ───────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Cria contato + conversa aberta + nota interna no EVO CRM.
|
|
253
|
+
* Silencioso se os secrets não estiverem configurados.
|
|
254
|
+
* Retorna o ID do contato criado ou null em caso de erro/duplicata.
|
|
255
|
+
*/
|
|
256
|
+
export async function pushLeadToCrm(env: Env, data: CrmLeadData): Promise<string | null> {
|
|
257
|
+
if (!isCrmConfigured(env)) return null;
|
|
258
|
+
|
|
259
|
+
const token = await getAccessToken(env);
|
|
260
|
+
if (!token) {
|
|
261
|
+
console.warn('[EVO CRM] Failed to get access token');
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const locale = resolveLocale(env.EVO_CRM_LOCALE);
|
|
266
|
+
const defaultCountry = env.EVO_CRM_DEFAULT_COUNTRY || '55';
|
|
267
|
+
|
|
268
|
+
const attrs = buildContactAttributes(data);
|
|
269
|
+
const contactPayload: Record<string, any> = {
|
|
270
|
+
name: data.name || data.phone,
|
|
271
|
+
phone_number: normalizePhone(data.phone, defaultCountry),
|
|
272
|
+
};
|
|
273
|
+
if (data.email) contactPayload.email = data.email;
|
|
274
|
+
if (Object.keys(attrs).length) contactPayload.additional_attributes = attrs;
|
|
275
|
+
|
|
276
|
+
// 1. Criar contato
|
|
277
|
+
let contactId: string | null = null;
|
|
278
|
+
try {
|
|
279
|
+
const res = await fetch(`${env.EVO_CRM_BASE_URL}/api/v1/contacts`, {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
|
282
|
+
body: JSON.stringify(contactPayload),
|
|
283
|
+
});
|
|
284
|
+
if (!res.ok) {
|
|
285
|
+
const body = await res.text();
|
|
286
|
+
console.warn('[EVO CRM] createContact non-ok:', res.status, body.slice(0, 200));
|
|
287
|
+
// 422 = contato já existe — tentar extrair o ID do payload de erro
|
|
288
|
+
if (res.status === 422) {
|
|
289
|
+
try {
|
|
290
|
+
const errJson: any = JSON.parse(body);
|
|
291
|
+
const rawId = errJson?.id ?? errJson?.data?.id ?? errJson?.contact?.id ?? errJson?.data?.contact?.id;
|
|
292
|
+
if (rawId != null) contactId = String(rawId);
|
|
293
|
+
} catch { /* body não é JSON */ }
|
|
294
|
+
}
|
|
295
|
+
if (!contactId) return null;
|
|
296
|
+
} else {
|
|
297
|
+
const result: any = await res.json();
|
|
298
|
+
// EVO CRM pode retornar { id } direto ou { data: { id } } ou { data: { contact: { id } } }
|
|
299
|
+
const rawId = result?.id ?? result?.data?.id ?? result?.data?.contact?.id;
|
|
300
|
+
contactId = rawId != null ? String(rawId) : null;
|
|
301
|
+
}
|
|
302
|
+
} catch (err: any) {
|
|
303
|
+
console.warn('[EVO CRM] createContact failed:', err?.message || String(err));
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!contactId || !env.EVO_CRM_INBOX_ID) return contactId;
|
|
308
|
+
|
|
309
|
+
// 2. Criar conversa
|
|
310
|
+
let conversationId: string | null = null;
|
|
311
|
+
try {
|
|
312
|
+
const res = await fetch(`${env.EVO_CRM_BASE_URL}/api/v1/conversations`, {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
|
315
|
+
body: JSON.stringify({
|
|
316
|
+
inbox_id: env.EVO_CRM_INBOX_ID,
|
|
317
|
+
contact_id: contactId,
|
|
318
|
+
additional_attributes: {},
|
|
319
|
+
}),
|
|
320
|
+
});
|
|
321
|
+
if (!res.ok) {
|
|
322
|
+
const err = await res.text();
|
|
323
|
+
console.warn('[EVO CRM] createConversation non-ok:', res.status, err.slice(0, 200));
|
|
324
|
+
} else {
|
|
325
|
+
const result: any = await res.json();
|
|
326
|
+
const rawConvId = result?.id ?? result?.data?.id;
|
|
327
|
+
conversationId = rawConvId != null ? String(rawConvId) : null;
|
|
328
|
+
}
|
|
329
|
+
} catch (err: any) {
|
|
330
|
+
console.warn('[EVO CRM] createConversation failed:', err?.message || String(err));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 3. Criar nota interna com contexto do lead
|
|
334
|
+
if (conversationId) {
|
|
335
|
+
try {
|
|
336
|
+
await fetch(`${env.EVO_CRM_BASE_URL}/api/v1/conversations/${conversationId}/messages`, {
|
|
337
|
+
method: 'POST',
|
|
338
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
|
339
|
+
body: JSON.stringify({
|
|
340
|
+
content: buildAgentNote(data, locale),
|
|
341
|
+
message_type: 'activity',
|
|
342
|
+
private: true,
|
|
343
|
+
}),
|
|
344
|
+
});
|
|
345
|
+
} catch {
|
|
346
|
+
// nota interna é best-effort
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return contactId;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ── Compat: CTWA (WhatsApp click-to-chat) ────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
export async function notifyEvolutionCTWA(env: Env, data: CTWALeadData): Promise<void> {
|
|
356
|
+
await pushLeadToCrm(env, {
|
|
357
|
+
phone: data.phone,
|
|
358
|
+
name: data.name,
|
|
359
|
+
messageBody: data.messageBody,
|
|
360
|
+
ctwaClid: data.ctwaClid,
|
|
361
|
+
adId: data.adId,
|
|
362
|
+
pageUrl: data.sourceUrl,
|
|
363
|
+
headline: data.headline,
|
|
364
|
+
utmSource: 'whatsapp',
|
|
365
|
+
eventName: 'CTWA',
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ── Compat: Formulário ────────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
export async function notifyEvolutionForm(env: Env, data: FormLeadData): Promise<void> {
|
|
372
|
+
await pushLeadToCrm(env, {
|
|
373
|
+
phone: data.phone,
|
|
374
|
+
name: data.name,
|
|
375
|
+
email: data.email,
|
|
376
|
+
formName: data.formName,
|
|
377
|
+
utmSource: data.utmSource,
|
|
378
|
+
utmCampaign: data.utmCampaign,
|
|
379
|
+
pageUrl: data.pageUrl,
|
|
380
|
+
eventName: 'Lead',
|
|
381
|
+
});
|
|
382
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP Edge — GA4 Measurement Protocol
|
|
3
|
+
* Envia eventos server-side para o GA4 via Measurement Protocol.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { normalizePhone } from '../utils.js';
|
|
7
|
+
import { logApiFailure } from '../db.js';
|
|
8
|
+
import { Env, TrackPayload } from '../../types.js';
|
|
9
|
+
import { ExecutionContext } from '@cloudflare/workers-types';
|
|
10
|
+
|
|
11
|
+
export async function sendGA4Mp(env: Env, ga4EventName: string, payload: TrackPayload, ctx: ExecutionContext | null): Promise<{ ok?: boolean; status?: number; skipped?: string; error?: string }> {
|
|
12
|
+
if (!env.GA4_API_SECRET) return { skipped: 'GA4_API_SECRET not set' };
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
gaClientId: clientId, sessionId,
|
|
16
|
+
value, currency, contentName,
|
|
17
|
+
email, phone, firstName,
|
|
18
|
+
orderId,
|
|
19
|
+
} = payload;
|
|
20
|
+
|
|
21
|
+
if (!clientId) return { skipped: 'no clientId' };
|
|
22
|
+
|
|
23
|
+
const eventParams: Record<string, string | number> = {
|
|
24
|
+
...(value !== undefined && { value: parseFloat(String(value)) }),
|
|
25
|
+
...(currency && { currency: String(currency).toUpperCase() }),
|
|
26
|
+
...(contentName && { content_name: contentName }),
|
|
27
|
+
...(orderId && { transaction_id: orderId }),
|
|
28
|
+
...(email && { user_data_email_address: email.toLowerCase().trim() }),
|
|
29
|
+
...(phone && { user_data_phone_number: normalizePhone(phone) || '' }),
|
|
30
|
+
...(firstName && { user_data_first_name: firstName.toLowerCase().trim() }),
|
|
31
|
+
...(sessionId && { session_id: sessionId }),
|
|
32
|
+
engagement_time_msec: 100,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const body = {
|
|
36
|
+
client_id: clientId,
|
|
37
|
+
events: [{ name: ga4EventName, params: eventParams }],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const url = `https://www.google-analytics.com/mp/collect`
|
|
41
|
+
+ `?measurement_id=${env.GA4_MEASUREMENT_ID}`
|
|
42
|
+
+ `&api_secret=${env.GA4_API_SECRET}`;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch(url, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
body: JSON.stringify(body),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (res.status !== 204) {
|
|
52
|
+
if (env.DB && ctx) {
|
|
53
|
+
ctx.waitUntil(logApiFailure(env.DB, 'ga4', ga4EventName, String(res.status), 'GA4 returned non-204 status', '', JSON.stringify(body)));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return res.status === 204 ? { ok: true } : { status: res.status };
|
|
58
|
+
} catch (err: any) {
|
|
59
|
+
console.error('GA4 MP fetch failed:', err?.message || String(err));
|
|
60
|
+
|
|
61
|
+
if (env.DB && ctx) {
|
|
62
|
+
ctx.waitUntil(logApiFailure(env.DB, 'ga4', ga4EventName, 'FETCH_ERROR', err?.message || String(err), '', JSON.stringify(body)));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (env.RETRY_QUEUE) {
|
|
66
|
+
const send = env.RETRY_QUEUE.send({ eventType: ga4EventName, payload, platform: 'ga4' });
|
|
67
|
+
if (ctx) ctx.waitUntil(send); else send.catch(() => {});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { error: err?.message || String(err) };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP Edge — Meta Conversions API v22.0
|
|
3
|
+
* Envia eventos server-side para a Meta CAPI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { sha256, normalizePhone, normalizeCity } from '../utils.js';
|
|
7
|
+
import { logApiFailure } from '../db.js';
|
|
8
|
+
import { logMatchQuality, autoEnrichPayload } from '../ml/matchquality.js';
|
|
9
|
+
import { Env, TrackPayload } from '../../types.js';
|
|
10
|
+
import { ExecutionContext } from '@cloudflare/workers-types';
|
|
11
|
+
|
|
12
|
+
interface EnrichedPayload {
|
|
13
|
+
payload: TrackPayload;
|
|
14
|
+
recovered: { email: boolean; utm: boolean };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function sendMetaCapi(env: Env, eventName: string, payload: TrackPayload, request: Request | null, ctx: ExecutionContext | null): Promise<any> {
|
|
18
|
+
// Auto-enriquecer payload com dados do Identity Graph antes do envio
|
|
19
|
+
let recovered = { email: false, utm: false };
|
|
20
|
+
if (env.DB && payload) {
|
|
21
|
+
const enriched = await autoEnrichPayload(env, payload) as EnrichedPayload;
|
|
22
|
+
payload = enriched.payload;
|
|
23
|
+
recovered = enriched.recovered;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
email, phone, firstName, lastName,
|
|
28
|
+
city, state, country,
|
|
29
|
+
zip, dob,
|
|
30
|
+
fbp, fbc, userId,
|
|
31
|
+
eventId, pageUrl,
|
|
32
|
+
value, currency,
|
|
33
|
+
contentIds, contentName, contentType, numItems,
|
|
34
|
+
// Dual-layer context — funil avançado + imóveis
|
|
35
|
+
funnel_stage, distanceBucket: distance_bucket, intentScoreNum: intent_score, intent_bucket,
|
|
36
|
+
ltvScore, ltvClass, metaSignal, metaSignalBucket: metaSignalBucketVal,
|
|
37
|
+
} = payload;
|
|
38
|
+
|
|
39
|
+
const phoneNorm = normalizePhone(phone);
|
|
40
|
+
const countryCode = (country || (request as any)?.cf?.country || 'br').toLowerCase();
|
|
41
|
+
const stateCode = state ? String(state).toLowerCase() : undefined;
|
|
42
|
+
const cityNorm = normalizeCity(city);
|
|
43
|
+
|
|
44
|
+
const userData: Record<string, string> = {
|
|
45
|
+
...(email && { em: await sha256(email) || '' }),
|
|
46
|
+
...(phoneNorm && { ph: await sha256(phoneNorm) || '' }),
|
|
47
|
+
...(firstName && { fn: await sha256(firstName) || '' }),
|
|
48
|
+
...(lastName && { ln: await sha256(lastName) || '' }),
|
|
49
|
+
...(cityNorm && { ct: await sha256(cityNorm) || '' }),
|
|
50
|
+
...(stateCode && { st: await sha256(stateCode) || '' }),
|
|
51
|
+
...(countryCode && { country: await sha256(countryCode) || '' }),
|
|
52
|
+
...(userId && { external_id: await sha256(String(userId)) || '' }),
|
|
53
|
+
...(zip && { zp: await sha256(zip) || '' }),
|
|
54
|
+
...(dob && { db: await sha256(dob) || '' }),
|
|
55
|
+
...(fbp && { fbp }),
|
|
56
|
+
...(fbc && { fbc }),
|
|
57
|
+
client_ip_address: request?.headers.get('CF-Connecting-IP')
|
|
58
|
+
|| request?.headers.get('X-Forwarded-For')
|
|
59
|
+
|| '',
|
|
60
|
+
client_user_agent: request?.headers.get('User-Agent') || '',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const customData: Record<string, string | number | string[]> = {
|
|
64
|
+
...(value !== undefined && { value: parseFloat(String(value)) }),
|
|
65
|
+
...(currency && { currency: String(currency).toUpperCase() }),
|
|
66
|
+
...(contentIds && contentIds.length > 0 && { content_ids: contentIds }),
|
|
67
|
+
...(contentName && { content_name: contentName }),
|
|
68
|
+
...(contentType && { content_type: contentType }),
|
|
69
|
+
...(numItems && { num_items: parseInt(String(numItems)) }),
|
|
70
|
+
// Contexto de funil e proximidade — enriquece matching e otimização Meta
|
|
71
|
+
...(funnel_stage && { funnel_stage }),
|
|
72
|
+
...(distance_bucket && { distance_bucket }),
|
|
73
|
+
...(intent_score && { intent_score }),
|
|
74
|
+
...(ltvScore !== undefined && ltvScore !== null && { ltv_score: ltvScore }),
|
|
75
|
+
...(ltvClass && { ltv_class: ltvClass }),
|
|
76
|
+
...(metaSignal !== undefined && metaSignal !== null && { meta_signal: metaSignal }),
|
|
77
|
+
...(metaSignalBucketVal && { meta_signal_bucket: metaSignalBucketVal }),
|
|
78
|
+
...(intent_bucket && { intent_bucket }),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const eventPayload = {
|
|
82
|
+
event_name: eventName,
|
|
83
|
+
event_time: Math.floor(Date.now() / 1000),
|
|
84
|
+
event_id: eventId || `cdp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
85
|
+
event_source_url: pageUrl || `https://${env.SITE_DOMAIN}`,
|
|
86
|
+
action_source: 'website',
|
|
87
|
+
user_data: userData,
|
|
88
|
+
...(Object.keys(customData).length > 0 && { custom_data: customData }),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const requestBody: Record<string, any> = {
|
|
92
|
+
data: [eventPayload],
|
|
93
|
+
access_token: env.META_ACCESS_TOKEN,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (env.META_TEST_CODE) {
|
|
97
|
+
requestBody.test_event_code = env.META_TEST_CODE;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Logar match quality em background (não bloqueia dispatch)
|
|
101
|
+
if (env.DB && ctx) {
|
|
102
|
+
ctx.waitUntil(logMatchQuality(env.DB, eventName, payload, recovered));
|
|
103
|
+
} else if (env.DB) {
|
|
104
|
+
logMatchQuality(env.DB, eventName, payload, recovered).catch(() => {});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const endpoint = `https://graph.facebook.com/v22.0/${env.META_PIXEL_ID}/events`;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch(endpoint, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body: JSON.stringify(requestBody),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const data = await res.json();
|
|
117
|
+
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
const errorCode = (data as any).error?.code || String(res.status);
|
|
120
|
+
const errorMessage = (data as any).error?.message || (data as any).error?.error_user_msg || 'Unknown error';
|
|
121
|
+
console.error('Meta CAPI error:', res.status, errorMessage);
|
|
122
|
+
|
|
123
|
+
if (env.DB && ctx) {
|
|
124
|
+
ctx.waitUntil(logApiFailure(env.DB, 'meta', eventName, errorCode, errorMessage, eventPayload.event_id, JSON.stringify(requestBody)));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return data;
|
|
129
|
+
} catch (err: any) {
|
|
130
|
+
console.error('Meta CAPI fetch failed:', err?.message || String(err));
|
|
131
|
+
|
|
132
|
+
if (env.DB && ctx) {
|
|
133
|
+
ctx.waitUntil(logApiFailure(env.DB, 'meta', eventName, 'FETCH_ERROR', err?.message || String(err), eventPayload.event_id, JSON.stringify(requestBody)));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (env.RETRY_QUEUE) {
|
|
137
|
+
const send = env.RETRY_QUEUE.send({ eventType: eventName, payload, platform: 'meta' });
|
|
138
|
+
if (ctx) ctx.waitUntil(send); else send.catch(() => {});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { error: err?.message || String(err) };
|
|
142
|
+
}
|
|
143
|
+
}
|