cdp-edge 1.2.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 +367 -0
- package/bin/cdp-edge.js +61 -0
- package/contracts/api-versions.json +368 -0
- package/dist/commands/analyze.js +52 -0
- package/dist/commands/infra.js +54 -0
- package/dist/commands/install.js +168 -0
- package/dist/commands/server.js +174 -0
- package/dist/commands/setup.js +123 -0
- package/dist/commands/validate.js +84 -0
- package/dist/index.js +12 -0
- package/docs/CI-CD-SETUP.md +217 -0
- package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
- package/docs/events-reference.md +359 -0
- package/docs/installation.md +155 -0
- package/docs/quick-start.md +185 -0
- package/docs/sdk-reference.md +371 -0
- package/docs/whatsapp-ctwa.md +209 -0
- package/extracted-skill/tracking-events-generator/INDEX.md +94 -0
- package/extracted-skill/tracking-events-generator/INSTALACAO-CDPEDGE.md +58 -0
- package/extracted-skill/tracking-events-generator/INTEGRACAO-COMPLETA.md +594 -0
- package/extracted-skill/tracking-events-generator/MELHORIAS-IMPLEMENTADAS.md +412 -0
- package/extracted-skill/tracking-events-generator/Premium-Tracking-Intelligence-Resumo.md +333 -0
- package/extracted-skill/tracking-events-generator/SKILL.md +257 -0
- package/extracted-skill/tracking-events-generator/advanced-matching.js +364 -0
- package/extracted-skill/tracking-events-generator/agents/ab-testing-agent.md +54 -0
- package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +1304 -0
- package/extracted-skill/tracking-events-generator/agents/bing-agent.md +76 -0
- package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +264 -0
- package/extracted-skill/tracking-events-generator/agents/code-guardian-agent.md +149 -0
- package/extracted-skill/tracking-events-generator/agents/compliance-agent.md +2077 -0
- package/extracted-skill/tracking-events-generator/agents/crm-integration-agent.md +1419 -0
- package/extracted-skill/tracking-events-generator/agents/dashboard-agent.md +456 -0
- package/extracted-skill/tracking-events-generator/agents/database-agent.md +667 -0
- package/extracted-skill/tracking-events-generator/agents/debug-agent.md +1455 -0
- package/extracted-skill/tracking-events-generator/agents/domain-setup-agent.md +224 -0
- package/extracted-skill/tracking-events-generator/agents/email-agent.md +61 -0
- package/extracted-skill/tracking-events-generator/agents/fingerprint-agent.md +52 -0
- package/extracted-skill/tracking-events-generator/agents/google-agent.md +109 -0
- package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +365 -0
- package/extracted-skill/tracking-events-generator/agents/intelligence-scheduling.md +643 -0
- package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +62 -0
- package/extracted-skill/tracking-events-generator/agents/localization-agent.md +55 -0
- package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +59 -0
- package/extracted-skill/tracking-events-generator/agents/master-feedback-loop.md +900 -0
- package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +1922 -0
- package/extracted-skill/tracking-events-generator/agents/memory-agent.json +109 -0
- package/extracted-skill/tracking-events-generator/agents/memory-agent.md +703 -0
- package/extracted-skill/tracking-events-generator/agents/meta-agent.md +110 -0
- package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +255 -0
- package/extracted-skill/tracking-events-generator/agents/performance-agent.md +1157 -0
- package/extracted-skill/tracking-events-generator/agents/performance-optimization-agent.md +1432 -0
- package/extracted-skill/tracking-events-generator/agents/pinterest-agent.md +310 -0
- package/extracted-skill/tracking-events-generator/agents/premium-tracking-intelligence-agent.md +849 -0
- package/extracted-skill/tracking-events-generator/agents/r2-setup-agent.md +250 -0
- package/extracted-skill/tracking-events-generator/agents/reddit-agent.md +313 -0
- package/extracted-skill/tracking-events-generator/agents/security-enterprise-agent.md +1752 -0
- package/extracted-skill/tracking-events-generator/agents/server-tracking.md +1188 -0
- package/extracted-skill/tracking-events-generator/agents/spotify-agent.md +383 -0
- package/extracted-skill/tracking-events-generator/agents/tiktok-agent.md +111 -0
- package/extracted-skill/tracking-events-generator/agents/tracking-plan-agent.md +364 -0
- package/extracted-skill/tracking-events-generator/agents/validator-agent.md +267 -0
- package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +69 -0
- package/extracted-skill/tracking-events-generator/agents/whatsapp-agent.md +76 -0
- package/extracted-skill/tracking-events-generator/agents/whatsapp-ctwa-setup-agent.md +699 -0
- package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +422 -0
- package/extracted-skill/tracking-events-generator/anti-blocking.js +285 -0
- package/extracted-skill/tracking-events-generator/cdpTrack.js +641 -0
- package/extracted-skill/tracking-events-generator/contracts/api-versions.json +368 -0
- package/extracted-skill/tracking-events-generator/docs/guia-cloudflare-iniciante.md +107 -0
- package/extracted-skill/tracking-events-generator/engagement-scoring.js +226 -0
- package/extracted-skill/tracking-events-generator/evals/evals.json +235 -0
- package/extracted-skill/tracking-events-generator/integration-test.js +497 -0
- package/extracted-skill/tracking-events-generator/knowledge-base.md +2894 -0
- package/extracted-skill/tracking-events-generator/micro-events.js +992 -0
- package/extracted-skill/tracking-events-generator/models/captura-de-lead.md +78 -0
- package/extracted-skill/tracking-events-generator/models/captura-lead-evento-externo.md +99 -0
- package/extracted-skill/tracking-events-generator/models/checkout-proprio.md +111 -0
- package/extracted-skill/tracking-events-generator/models/multi-step-checkout.md +672 -0
- package/extracted-skill/tracking-events-generator/models/pagina-obrigado.md +55 -0
- package/extracted-skill/tracking-events-generator/models/pinterest/conversions-api-template.js +144 -0
- package/extracted-skill/tracking-events-generator/models/pinterest/event-mappings.json +48 -0
- package/extracted-skill/tracking-events-generator/models/pinterest/tag-template.js +28 -0
- package/extracted-skill/tracking-events-generator/models/quiz-funnel.md +68 -0
- package/extracted-skill/tracking-events-generator/models/reddit/conversions-api-template.js +205 -0
- package/extracted-skill/tracking-events-generator/models/reddit/event-mappings.json +56 -0
- package/extracted-skill/tracking-events-generator/models/reddit/pixel-template.js +19 -0
- package/extracted-skill/tracking-events-generator/models/scenarios/behavior-engine.js +425 -0
- package/extracted-skill/tracking-events-generator/models/scenarios/real-estate-logic.md +50 -0
- package/extracted-skill/tracking-events-generator/models/scenarios/sales-page-logic.md +50 -0
- package/extracted-skill/tracking-events-generator/models/trafego-direto.md +582 -0
- package/extracted-skill/tracking-events-generator/models/webinar-registration.md +63 -0
- package/extracted-skill/tracking-events-generator/tracking.config.js +46 -0
- package/extracted-skill/tracking-events-generator/walkthrough.md +26 -0
- package/package.json +75 -0
- package/server-edge-tracker/INSTALAR.md +328 -0
- package/server-edge-tracker/migrate-new-db.sql +137 -0
- package/server-edge-tracker/migrate-v2.sql +16 -0
- package/server-edge-tracker/migrate-v3.sql +6 -0
- package/server-edge-tracker/migrate-v4.sql +18 -0
- package/server-edge-tracker/migrate-v5.sql +17 -0
- package/server-edge-tracker/migrate-v6.sql +24 -0
- package/server-edge-tracker/migrate.sql +111 -0
- package/server-edge-tracker/schema.sql +265 -0
- package/server-edge-tracker/worker.js +2574 -0
- package/server-edge-tracker/wrangler.toml +85 -0
- package/templates/afiliado-sem-landing.md +312 -0
- package/templates/captura-de-lead.md +78 -0
- package/templates/captura-lead-evento-externo.md +99 -0
- package/templates/checkout-proprio.md +111 -0
- package/templates/install/.claude/commands/cdp.md +1 -0
- package/templates/install/CLAUDE.md +65 -0
- package/templates/linkedin/tag-template.js +46 -0
- package/templates/multi-step-checkout.md +673 -0
- package/templates/pagina-obrigado.md +55 -0
- package/templates/pinterest/conversions-api-template.js +144 -0
- package/templates/pinterest/event-mappings.json +48 -0
- package/templates/pinterest/tag-template.js +28 -0
- package/templates/quiz-funnel.md +68 -0
- package/templates/reddit/conversions-api-template.js +205 -0
- package/templates/reddit/event-mappings.json +56 -0
- package/templates/reddit/pixel-template.js +46 -0
- package/templates/scenarios/behavior-engine.js +402 -0
- package/templates/scenarios/real-estate-logic.md +50 -0
- package/templates/scenarios/sales-page-logic.md +50 -0
- package/templates/spotify/pixel-template.js +46 -0
- package/templates/trafego-direto.md +582 -0
- package/templates/vsl-page.md +292 -0
- package/templates/webinar-registration.md +63 -0
|
@@ -0,0 +1,2894 @@
|
|
|
1
|
+
# Knowledge Base: CDP Edge (Quantum Tier)
|
|
2
|
+
|
|
3
|
+
Esta é a fonte técnica oficial para o CDP Edge. Toda implementação deve seguir os padrões de infraestrutura Cloudflare Native (Workers + D1) para garantir máxima atribuição e performance.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🏗️ 1. ARQUITETURA DE DADOS (CLOUDFLARE NATIVE)
|
|
8
|
+
|
|
9
|
+
### Camadas do Sistema:
|
|
10
|
+
1. **Browser (Coleta)**: O SDK `cdpTrack.js` captura eventos de interação, dados de identidade e **micro-comportamentos**.
|
|
11
|
+
2. **Worker (Processamento)**: O Cloudflare Worker recebe os dados, normaliza, executa o hashing WebCrypto e persiste no banco D1.
|
|
12
|
+
3. **D1 Database (Persistência)**: Fonte de verdade para o Identity Graph e log de eventos completo (Low-level).
|
|
13
|
+
4. **APIs (Despacho)**: Envio seletivo e assíncrono para Meta, TikTok e GA4 via `ctx.waitUntil`.
|
|
14
|
+
|
|
15
|
+
### Estratégia: D1 vs. Plataformas (EMQ Optimization)
|
|
16
|
+
Para manter o Meta Pixel limpo e focado em conversão, aplicamos a regra de **Filtragem de Intent**:
|
|
17
|
+
|
|
18
|
+
* **D1 (Tudo)**: Salva 100% dos eventos (cliques x/y, rage clicks, scrolls, heartbeats). É sua ferramenta de auditoria e BI.
|
|
19
|
+
* **Plataformas (Meta/Google)**: Recebe apenas eventos de **Alta Intenção** (Milestones):
|
|
20
|
+
* `Retention_Pulse` (Ex: usuário ativo por +60s).
|
|
21
|
+
* `Rage_Click` (Exclusão técnica).
|
|
22
|
+
* `VSL_25/50/75/100` (Funis de vídeo).
|
|
23
|
+
* Eventos Standard (`Lead`, `Purchase`, `AddToCart`).
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 🛠️ 2. PADRÕES TÉCNICOS
|
|
28
|
+
|
|
29
|
+
### 2.1 Identity Graph (Lead Lock)
|
|
30
|
+
A identidade do usuário é o pilar da atribuição. Ela vincula identificadores anônimos a dados reais (PII) de forma segura.
|
|
31
|
+
- **Campos**: `email_hash`, `phone_hash`, `fbp`, `fbc`, `ttp`, `ttclid`, `ip`, `ua`.
|
|
32
|
+
- **Lógica**: Quando um Webhook de venda chega, o sistema busca no D1 pelo e-mail/telefone para recuperar os cookies de browser originais, garantindo Match Quality máximo.
|
|
33
|
+
|
|
34
|
+
### 2.2 Deduplicação (Event ID)
|
|
35
|
+
- Todo evento disparado no browser gera um `event_id` único.
|
|
36
|
+
- Esse mesmo ID deve ser enviado na chamada de servidor (CAPI) para que as plataformas ignorem duplicatas.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 🏗️ 3. HUMAN-BEHAVIOR ENGINE (MICRO-EVENTOS)
|
|
41
|
+
|
|
42
|
+
Protocolos de captura para análise de CRO e saúde técnica.
|
|
43
|
+
|
|
44
|
+
### 3.1 Rage Click Detector
|
|
45
|
+
Detecta quando o usuário clica repetidamente (>3 cliques em 500ms) em uma área não interativa.
|
|
46
|
+
* **Ação**: Disparar `rage_click`.
|
|
47
|
+
* **Uso**: Identificar bugs de UI ou frustração do usuário.
|
|
48
|
+
|
|
49
|
+
### 3.2 Visibility Heartbeat (Tab Focus)
|
|
50
|
+
Detecta se o usuário mudou de aba (`document.visibilityState === 'hidden'`).
|
|
51
|
+
* **Ação**: Disparar `tab_visibility_change` com status `hidden` ou `visible`.
|
|
52
|
+
* **Uso**: Medir retenção real em VSLs onde o áudio continua mas o vídeo não é visto.
|
|
53
|
+
|
|
54
|
+
### 3.3 Click Heatmap (D1 Only)
|
|
55
|
+
Captura as coordenadas relativas do clique no documento.
|
|
56
|
+
* **Schema D1**: `x_pos` (float), `y_pos` (float), `element_id` (string), `element_class` (string).
|
|
57
|
+
* **Uso**: Reconstrução de mapas de calor sem ferramentas externas.
|
|
58
|
+
|
|
59
|
+
### 3.4 Retention Pulse
|
|
60
|
+
Disparado em intervalos fixos (30s, 60s, 120s) para medir o tempo de atenção ativa.
|
|
61
|
+
* **Ação**: Disparar `pulse_heartbeat` com o atributo `duration`.
|
|
62
|
+
* **Uso**: Treinar a IA da Meta com usuários de "Alto Tempo de Atenção".
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 🛠️ 4. GERADOR DE TRACKING (SDK UNIFICADO)
|
|
67
|
+
|
|
68
|
+
**Gerado uma vez. Nunca editar.** Despacha cada evento para todas as plataformas ativas automaticamente.
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
// src/tracking/tracking.js
|
|
72
|
+
// ============================================================
|
|
73
|
+
// CDPEDGE — Utilitário multi-plataforma unificado
|
|
74
|
+
// Suporta: Meta · GA4 · Google Ads · TikTok · Pinterest · Bing · Reddit
|
|
75
|
+
// NÃO edite — edite apenas tracking.config.js
|
|
76
|
+
// ============================================================
|
|
77
|
+
import CONFIG from './tracking.config.js';
|
|
78
|
+
|
|
79
|
+
// ── Guards — segurança em SSR e SDK não carregado ──
|
|
80
|
+
const isBrowser = typeof window !== 'undefined';
|
|
81
|
+
const has = (fn) => isBrowser && typeof window[fn] === 'function';
|
|
82
|
+
|
|
83
|
+
// ── Captura de parâmetros de URL no carregamento da página ──
|
|
84
|
+
// Cada plataforma injeta seu próprio Click ID na URL quando o usuário clica em um anúncio.
|
|
85
|
+
// Guardamos todos em memória para enviar ao servidor e garantir atribuição correta.
|
|
86
|
+
const _urlParams = isBrowser ? new URLSearchParams(window.location.search) : new URLSearchParams();
|
|
87
|
+
|
|
88
|
+
// Meta
|
|
89
|
+
const _ctwaClid = _urlParams.get('ctwa_clid') || ''; // Click-to-WhatsApp Click ID
|
|
90
|
+
const _fbclid = _urlParams.get('fbclid') || ''; // Meta Ads click ID → gera cookie _fbc
|
|
91
|
+
|
|
92
|
+
// Google Ads (três variantes por tipo de match/privacy)
|
|
93
|
+
const _gclid = _urlParams.get('gclid') || ''; // Google Ads standard click ID → gera cookie _gcl_aw
|
|
94
|
+
const _wbraid = _urlParams.get('wbraid') || ''; // Google Ads (iOS, web-to-app, privacy preserving)
|
|
95
|
+
const _gbraid = _urlParams.get('gbraid') || ''; // Google Ads (app campaigns, privacy preserving)
|
|
96
|
+
|
|
97
|
+
// TikTok
|
|
98
|
+
const _ttclid = _urlParams.get('ttclid') || ''; // TikTok Ads click ID → complementa cookie _ttp
|
|
99
|
+
|
|
100
|
+
// UTMs — rastreamento interno de origem de tráfego (independente da atribuição das plataformas)
|
|
101
|
+
const _utms = {
|
|
102
|
+
utm_source: _urlParams.get('utm_source') || '',
|
|
103
|
+
utm_medium: _urlParams.get('utm_medium') || '',
|
|
104
|
+
utm_campaign: _urlParams.get('utm_campaign') || '',
|
|
105
|
+
utm_content: _urlParams.get('utm_content') || '',
|
|
106
|
+
utm_term: _urlParams.get('utm_term') || '',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// ── User ID persistente (first-party cookie) ──────────────────────────────────
|
|
110
|
+
// Identifica o mesmo usuário entre sessões diferentes, mesmo sem login.
|
|
111
|
+
// Resolve o problema de cookies de terceiros bloqueados por ad-blockers.
|
|
112
|
+
const _getUserId = () => {
|
|
113
|
+
if (!isBrowser) return '';
|
|
114
|
+
const KEY = '_cdp_uid';
|
|
115
|
+
let uid = document.cookie.match(new RegExp(`${KEY}=([^;]+)`))?.[1];
|
|
116
|
+
if (!uid) {
|
|
117
|
+
uid = `${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
118
|
+
// Cookie first-party de 365 dias — não bloqueado por navegadores
|
|
119
|
+
document.cookie = `${KEY}=${uid}; max-age=${60*60*24*365}; path=/; SameSite=Lax`;
|
|
120
|
+
}
|
|
121
|
+
return uid;
|
|
122
|
+
};
|
|
123
|
+
const _userId = isBrowser ? _getUserId() : '';
|
|
124
|
+
|
|
125
|
+
/** getUTMs() — retorna os UTMs capturados na chegada do usuário
|
|
126
|
+
* Útil para: salvar no CRM, incluir no payload do servidor, rastreamento interno.
|
|
127
|
+
* Atribuição própria independente da janela das plataformas (último clique registrado). */
|
|
128
|
+
export const getUTMs = () => ({ ..._utms });
|
|
129
|
+
|
|
130
|
+
/** getUserId() — retorna o User ID persistente gerado como cookie first-party.
|
|
131
|
+
* Identifica o mesmo usuário entre sessões. Não é bloqueado por ad-blockers.
|
|
132
|
+
* Identificador de usuário persistente nativo. */
|
|
133
|
+
export const getUserId = () => _userId;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* passCheckoutParams(options?) — adiciona UTMs + User ID nos links de checkout externo.
|
|
137
|
+
*
|
|
138
|
+
* Problema: quando o usuário clica em um link de Hotmart, Kiwify, Eduzz etc., os UTMs e
|
|
139
|
+
* o user ID são perdidos — a plataforma não sabe de onde veio o comprador.
|
|
140
|
+
*
|
|
141
|
+
* Solução: interceptar os links de checkout e adicionar os parâmetros de rastreamento
|
|
142
|
+
* na URL antes do clique, para que a plataforma receba e registre a origem.
|
|
143
|
+
*
|
|
144
|
+
* Plataformas suportadas e seus parâmetros:
|
|
145
|
+
* hotmart → xcod (user ID) + sck (UTMs pipe-separados: src|med|camp|con|term)
|
|
146
|
+
* kiwify → src (utm_source)
|
|
147
|
+
* eduzz → src (utm_source)
|
|
148
|
+
* monetizze → src (utm_source)
|
|
149
|
+
* cartpanda → utm_* passthrough direto
|
|
150
|
+
* ticto → utm_* + user_id (_cdp_uid) — capturado em wh.tracking e wh.url_params
|
|
151
|
+
* custom → qualquer domínio via opção `domains`
|
|
152
|
+
*
|
|
153
|
+
* Uso:
|
|
154
|
+
* passCheckoutParams() // detecta automaticamente
|
|
155
|
+
* passCheckoutParams({ platforms: ['hotmart'] }) // só Hotmart
|
|
156
|
+
* passCheckoutParams({ domains: ['meusite.com/checkout'] }) // domínio customizado
|
|
157
|
+
*/
|
|
158
|
+
export const passCheckoutParams = (options = {}) => {
|
|
159
|
+
if (!isBrowser) return;
|
|
160
|
+
|
|
161
|
+
const {
|
|
162
|
+
platforms = ['hotmart', 'kiwify', 'eduzz', 'monetizze', 'cartpanda', 'ticto'],
|
|
163
|
+
domains = [], // domínios extras além das plataformas padrão
|
|
164
|
+
extra = {}, // parâmetros extras a adicionar em todos os links
|
|
165
|
+
} = options;
|
|
166
|
+
|
|
167
|
+
const utms = getUTMs();
|
|
168
|
+
const userId = _userId;
|
|
169
|
+
|
|
170
|
+
// Mapa de domínios por plataforma
|
|
171
|
+
const PLATFORM_DOMAINS = {
|
|
172
|
+
hotmart: ['hotmart.com', 'pay.hotmart.com', 'payment.hotmart.com'],
|
|
173
|
+
kiwify: ['kiwify.com.br', 'checkout.kiwify.com.br'],
|
|
174
|
+
eduzz: ['eduzz.com', 'sun.eduzz.com'],
|
|
175
|
+
monetizze: ['monetizze.com.br'],
|
|
176
|
+
cartpanda: ['cartpanda.com', 'pay.cartpanda.com'],
|
|
177
|
+
ticto: ['ticto.app', 'pay.ticto.app', 'checkout.ticto.app'],
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Constrói o `sck` do Hotmart: utm_source|utm_medium|utm_campaign|utm_content|utm_term
|
|
181
|
+
const buildSck = () =>
|
|
182
|
+
[utms.utm_source, utms.utm_medium, utms.utm_campaign, utms.utm_content, utms.utm_term]
|
|
183
|
+
.map(v => v || 'direto')
|
|
184
|
+
.join('|');
|
|
185
|
+
|
|
186
|
+
// Parâmetros por plataforma
|
|
187
|
+
const getParamsForUrl = (href) => {
|
|
188
|
+
const params = { ...extra };
|
|
189
|
+
const url = href.toLowerCase();
|
|
190
|
+
|
|
191
|
+
const isMatch = (list) => list.some(d => url.includes(d));
|
|
192
|
+
|
|
193
|
+
if (platforms.includes('hotmart') && isMatch(PLATFORM_DOMAINS.hotmart)) {
|
|
194
|
+
if (userId) params.xcod = userId;
|
|
195
|
+
if (utms.utm_source) params.sck = buildSck();
|
|
196
|
+
} else if (platforms.includes('kiwify') && isMatch(PLATFORM_DOMAINS.kiwify)) {
|
|
197
|
+
if (utms.utm_source) params.src = utms.utm_source;
|
|
198
|
+
if (utms.utm_medium) params.utm_medium = utms.utm_medium;
|
|
199
|
+
if (utms.utm_campaign) params.utm_campaign = utms.utm_campaign;
|
|
200
|
+
} else if (
|
|
201
|
+
(platforms.includes('eduzz') && isMatch(PLATFORM_DOMAINS.eduzz)) ||
|
|
202
|
+
(platforms.includes('monetizze') && isMatch(PLATFORM_DOMAINS.monetizze))
|
|
203
|
+
) {
|
|
204
|
+
if (utms.utm_source) params.src = utms.utm_source;
|
|
205
|
+
} else if (platforms.includes('cartpanda') && isMatch(PLATFORM_DOMAINS.cartpanda)) {
|
|
206
|
+
// CartPanda recebe utm_* diretamente
|
|
207
|
+
Object.entries(utms).forEach(([k, v]) => { if (v) params[k] = v; });
|
|
208
|
+
} else if (platforms.includes('ticto') && isMatch(PLATFORM_DOMAINS.ticto)) {
|
|
209
|
+
Object.entries(utms).forEach(([k, v]) => { if (v) params[k] = v; });
|
|
210
|
+
if (userId) params.user_id = userId;
|
|
211
|
+
} else {
|
|
212
|
+
// Domínios customizados: repassa utm_* e userId
|
|
213
|
+
const allDomains = Object.values(PLATFORM_DOMAINS).flat().concat(domains);
|
|
214
|
+
if (!isMatch(allDomains)) return null; // não é checkout conhecido
|
|
215
|
+
Object.entries(utms).forEach(([k, v]) => { if (v) params[k] = v; });
|
|
216
|
+
if (userId) params.user_id = userId;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return Object.keys(params).length ? params : null;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const applyParams = (link) => {
|
|
223
|
+
if (!link.href || link.href.startsWith('javascript')) return;
|
|
224
|
+
const p = getParamsForUrl(link.href);
|
|
225
|
+
if (!p) return;
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const url = new URL(link.href);
|
|
229
|
+
Object.entries(p).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
230
|
+
link.href = url.toString();
|
|
231
|
+
} catch { /* URL inválida — ignora */ }
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Aplica em todos os links existentes
|
|
235
|
+
document.querySelectorAll('a[href]').forEach(applyParams);
|
|
236
|
+
|
|
237
|
+
// Aplica em links adicionados dinamicamente (ex: botões de checkout lazy loaded)
|
|
238
|
+
new MutationObserver((mutations) => {
|
|
239
|
+
mutations.forEach(m => m.addedNodes.forEach(node => {
|
|
240
|
+
if (node.nodeType !== 1) return;
|
|
241
|
+
if (node.tagName === 'A') applyParams(node);
|
|
242
|
+
node.querySelectorAll?.('a[href]').forEach(applyParams);
|
|
243
|
+
}));
|
|
244
|
+
}).observe(document.body, { childList: true, subtree: true });
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// ── Utilitários de formulário ─────────────────────────────────────────────────
|
|
248
|
+
// Normalizam dados ANTES de enviar às plataformas, alinhados com as variáveis
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
/** normalizePhone(tel) — normaliza telefone para o formato exigido pelas plataformas.
|
|
252
|
+
* Remove tudo que não é número. Adiciona DDI Brasil 55 se necessário.
|
|
253
|
+
* Meta exige somente dígitos com DDI — ex: '5511999998888'
|
|
254
|
+
* Retorna undefined se vazio (evita enviar campo em branco às APIs). */
|
|
255
|
+
export const normalizePhone = (tel = '') => {
|
|
256
|
+
let t = String(tel).replace(/\D/g, '');
|
|
257
|
+
if (!t) return undefined;
|
|
258
|
+
if (!t.startsWith('55') || t.length <= 11) t = '55' + t;
|
|
259
|
+
return t;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/** splitName(fullName) — divide nome completo em firstName e lastName.
|
|
263
|
+
* firstName = todos os nomes menos o último (alinha com campo Meta fn).
|
|
264
|
+
* lastName = somente o último nome (alinha com campo Meta ln).
|
|
265
|
+
* Ex: 'João da Silva' → { firstName: 'João da', lastName: 'Silva' }
|
|
266
|
+
* Ex: 'João' → { firstName: 'João', lastName: undefined } */
|
|
267
|
+
export const splitName = (fullName = '') => {
|
|
268
|
+
const parts = String(fullName).trim().split(/\s+/).filter(Boolean);
|
|
269
|
+
if (!parts.length) return { firstName: undefined, lastName: undefined };
|
|
270
|
+
if (parts.length === 1) return { firstName: parts[0], lastName: undefined };
|
|
271
|
+
return { firstName: parts.slice(0, -1).join(' '), lastName: parts.at(-1) };
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/** isValidEmail(email) — valida formato de email antes de enviar.
|
|
275
|
+
* Evita poluir os dados das plataformas com emails inválidos.
|
|
276
|
+
* Validação de Email nativa do CDP Edge.
|
|
277
|
+
* Retorna true se válido, false caso contrário. */
|
|
278
|
+
export const isValidEmail = (email = '') =>
|
|
279
|
+
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email).toLowerCase().trim());
|
|
280
|
+
|
|
281
|
+
/** readField(selector) — lê valor de campo de formulário por id, name ou seletor CSS.
|
|
282
|
+
* ex: readField('email') → busca id="email" ou name="email"
|
|
283
|
+
* ex: readField('#meu-email') ou readField('input[name="telefone"]') */
|
|
284
|
+
export const readField = (selector = '') => {
|
|
285
|
+
if (!isBrowser) return '';
|
|
286
|
+
const el = /^[#.[]/.test(selector)
|
|
287
|
+
? document.querySelector(selector)
|
|
288
|
+
: document.getElementById(selector) || document.querySelector(`[name="${selector}"]`);
|
|
289
|
+
return el?.value?.trim() || '';
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/** normalizeCity(city) — normaliza cidade para o campo `ct` do Meta Advanced Matching.
|
|
293
|
+
* Regra Meta: lowercase, sem acentos, sem espaços, sem caracteres especiais.
|
|
294
|
+
* ex: 'São Paulo' → 'saopaulo' | 'Belo Horizonte' → 'belohorizonte'
|
|
295
|
+
* DEVE ser aplicada ANTES do SHA256 no lado servidor (CAPI). */
|
|
296
|
+
export const normalizeCity = (city = '') => {
|
|
297
|
+
if (!city) return undefined;
|
|
298
|
+
return String(city)
|
|
299
|
+
.normalize('NFD') // decompõe acentos (e.g. ã → a + ~)
|
|
300
|
+
.replace(/[\u0300-\u036f]/g, '') // remove diacríticos
|
|
301
|
+
.toLowerCase()
|
|
302
|
+
.replace(/[^a-z0-9]/g, ''); // remove tudo que não é letra ou número
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// ── Utilitários de DataLayer / Items ─────────────────────────────────────────
|
|
306
|
+
// Transformam qualquer estrutura de produto para o formato GA4 items (a base de tudo).
|
|
307
|
+
// O GA4 items é a "língua universal" — de lá convertemos para Meta, TikTok, etc.
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* fixItems(malFormatted) — converte items MAL FORMATADOS para o padrão GA4.
|
|
311
|
+
* Recebe objeto ou array com qualquer estrutura de campo e mapeia para GA4.
|
|
312
|
+
* Uso: quando o dataLayer do site/plataforma chega com nomes diferentes dos esperados.
|
|
313
|
+
*
|
|
314
|
+
* Parâmetro fieldMap: { ga4Campo: 'nomeCampoOrigem' }
|
|
315
|
+
* Ex: fixItems(dlItems, { item_id:'id', item_name:'nome', price:'valor', quantity:'qtd' })
|
|
316
|
+
*
|
|
317
|
+
* Se não passar fieldMap, tenta detecção automática de campos comuns.
|
|
318
|
+
*/
|
|
319
|
+
export const fixItems = (malFormatted, fieldMap = {}) => {
|
|
320
|
+
if (!malFormatted) return [];
|
|
321
|
+
const arr = Array.isArray(malFormatted) ? malFormatted : [malFormatted];
|
|
322
|
+
const fm = {
|
|
323
|
+
item_id: fieldMap.item_id || 'id',
|
|
324
|
+
item_name: fieldMap.item_name || 'name' ,
|
|
325
|
+
price: fieldMap.price || 'price',
|
|
326
|
+
quantity: fieldMap.quantity || 'quantity',
|
|
327
|
+
currency: fieldMap.currency || 'currency',
|
|
328
|
+
item_brand: fieldMap.item_brand || 'brand',
|
|
329
|
+
item_category: fieldMap.item_category || 'category',
|
|
330
|
+
item_variant: fieldMap.item_variant || 'variant',
|
|
331
|
+
};
|
|
332
|
+
return arr.map((item, index) => ({
|
|
333
|
+
item_id: item[fm.item_id] || item.sku || item.product_id || '',
|
|
334
|
+
item_name: item[fm.item_name] || item.title || item.nome || item.produto || '',
|
|
335
|
+
price: parseFloat(String(item[fm.price] || 0).replace(',', '.')),
|
|
336
|
+
quantity: parseInt(item[fm.quantity] || 1, 10),
|
|
337
|
+
index,
|
|
338
|
+
...(item[fm.currency] && { currency: item[fm.currency] }),
|
|
339
|
+
...(item[fm.item_brand] && { item_brand: item[fm.item_brand] }),
|
|
340
|
+
...(item[fm.item_category]&& { item_category: item[fm.item_category] }),
|
|
341
|
+
...(item[fm.item_variant] && { item_variant: item[fm.item_variant] }),
|
|
342
|
+
// campos opcionais GA4 — passados direto se presentes
|
|
343
|
+
...(item.item_category2 && { item_category2: item.item_category2 }),
|
|
344
|
+
...(item.item_category3 && { item_category3: item.item_category3 }),
|
|
345
|
+
...(item.affiliation && { affiliation: item.affiliation }),
|
|
346
|
+
...(item.coupon && { coupon: item.coupon }),
|
|
347
|
+
...(item.discount && { discount: parseFloat(item.discount) }),
|
|
348
|
+
...(item.item_list_id && { item_list_id: item.item_list_id }),
|
|
349
|
+
...(item.item_list_name && { item_list_name: item.item_list_name }),
|
|
350
|
+
...(item.item_variant && { item_variant: item.item_variant }),
|
|
351
|
+
...(item.location_id && { location_id: item.location_id }),
|
|
352
|
+
...(item.promotion_id && { promotion_id: item.promotion_id }),
|
|
353
|
+
...(item.promotion_name && { promotion_name: item.promotion_name }),
|
|
354
|
+
}));
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* buildItems(params) — monta array GA4 a partir de parâmetros SOLTOS.
|
|
359
|
+
* Uso: quando o produto chega em campos separados (product_id, product_name, etc.)
|
|
360
|
+
* em vez de um array items.
|
|
361
|
+
*
|
|
362
|
+
* Ex: buildItems({ item_id: 'SKU123', item_name: 'Produto', price: 99.9 })
|
|
363
|
+
* → [{ item_id: 'SKU123', item_name: 'Produto', price: 99.9, quantity: 1 }]
|
|
364
|
+
*/
|
|
365
|
+
export const buildItems = (params = {}) => [{
|
|
366
|
+
item_id: params.item_id || params.product_id || params.id || '',
|
|
367
|
+
item_name: params.item_name || params.product_name || params.name || params.nome || '',
|
|
368
|
+
price: parseFloat(params.price || params.valor || 0),
|
|
369
|
+
quantity: parseInt(params.quantity || params.quantidade || 1, 10),
|
|
370
|
+
currency: params.currency || 'BRL',
|
|
371
|
+
...(params.item_brand && { item_brand: params.item_brand }),
|
|
372
|
+
...(params.item_category && { item_category: params.item_category }),
|
|
373
|
+
...(params.item_variant && { item_variant: params.item_variant }),
|
|
374
|
+
...(params.item_category2&& { item_category2:params.item_category2 }),
|
|
375
|
+
...(params.discount && { discount: parseFloat(params.discount) }),
|
|
376
|
+
...(params.coupon && { coupon: params.coupon }),
|
|
377
|
+
...(params.affiliation && { affiliation: params.affiliation }),
|
|
378
|
+
}];
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* ga4ItemsToMeta(items) — converte GA4 items para formato de content_ids + contents do Meta.
|
|
382
|
+
* Usado internamente pelo trackPurchase / trackAddToCart quando recebem _ga4Items.
|
|
383
|
+
* Converte dados para o formato Schema Standard da Meta.
|
|
384
|
+
*/
|
|
385
|
+
export const ga4ItemsToMeta = (items = []) => ({
|
|
386
|
+
content_ids: items.map(i => String(i.item_id)),
|
|
387
|
+
contents: items.map(i => ({
|
|
388
|
+
id: String(i.item_id),
|
|
389
|
+
quantity: i.quantity || 1,
|
|
390
|
+
item_price: i.price || 0,
|
|
391
|
+
title: i.item_name || undefined,
|
|
392
|
+
category: i.item_category || undefined,
|
|
393
|
+
brand: i.item_brand || undefined,
|
|
394
|
+
})),
|
|
395
|
+
num_items: items.reduce((s, i) => s + (i.quantity || 1), 0),
|
|
396
|
+
value: items.reduce((s, i) => s + (i.price || 0) * (i.quantity || 1), 0),
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* ga4ItemsToTikTok(items) — converte GA4 items para formato de contents do TikTok Pixel.
|
|
401
|
+
* Converte dados para o formato Schema Standard do TikTok.
|
|
402
|
+
*/
|
|
403
|
+
export const ga4ItemsToTikTok = (items = []) => ({
|
|
404
|
+
contents: items.map(i => ({
|
|
405
|
+
content_id: String(i.item_id),
|
|
406
|
+
content_name: i.item_name || undefined,
|
|
407
|
+
content_category: i.item_category || undefined,
|
|
408
|
+
brand: i.item_brand || undefined,
|
|
409
|
+
price: i.price || 0,
|
|
410
|
+
quantity: i.quantity || 1,
|
|
411
|
+
})),
|
|
412
|
+
value: items.reduce((s, i) => s + (i.price || 0) * (i.quantity || 1), 0),
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// ── Dispatchers por plataforma ──
|
|
416
|
+
const meta = (type, name, p = {}) => has('fbq') && window.fbq(type, name, p);
|
|
417
|
+
const tiktok = (name, p = {}) => has('ttq') && window.ttq.track(name, p);
|
|
418
|
+
const pinterest = (name, p = {}) => has('pintrk') && window.pintrk('track', name, p);
|
|
419
|
+
const bing = (name, p = {}) => isBrowser && window.uetq?.push({ ea: name, ...p });
|
|
420
|
+
const reddit = (name, p = {}) => has('rdt') && window.rdt('track', name, p);
|
|
421
|
+
const gtag = (...a) => has('gtag') && window.gtag(...a);
|
|
422
|
+
|
|
423
|
+
// ── Verificação de credencial ──
|
|
424
|
+
const active = (key) => !!CONFIG[key];
|
|
425
|
+
|
|
426
|
+
// ── Dispatcher GA4 ──
|
|
427
|
+
const ga4Event = (name, params = {}) => gtag('event', name, params);
|
|
428
|
+
const ga4Ecommerce = (name, data) => gtag('event', name, { ecommerce: data });
|
|
429
|
+
|
|
430
|
+
// ── Conversão Google Ads (com Enhanced Conversions / Conversões Otimizadas) ──
|
|
431
|
+
const gadsConversion = (label, value = 0, currency = 'BRL', txId = '', email = '', phone = '') => {
|
|
432
|
+
if (!label || !active('googleAdsId')) return;
|
|
433
|
+
gtag('event', 'conversion', {
|
|
434
|
+
send_to: `${CONFIG.googleAdsId}/${label}`,
|
|
435
|
+
value, currency,
|
|
436
|
+
...(txId && { transaction_id: txId }),
|
|
437
|
+
// Enhanced Conversions (Conversões Otimizadas) — atribuição a nível de usuário
|
|
438
|
+
...(email && { email_address: email }),
|
|
439
|
+
...(phone && { phone_number: phone }),
|
|
440
|
+
});
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// ============================================================
|
|
444
|
+
// API PÚBLICA
|
|
445
|
+
// ============================================================
|
|
446
|
+
|
|
447
|
+
/** PageView — chame em cada página via useEffect(()=>{}, []) */
|
|
448
|
+
export const trackPageView = (title = document.title, url = window.location.pathname) => {
|
|
449
|
+
if (active('metaPixelId')) meta('track', 'PageView');
|
|
450
|
+
if (active('tiktokPixelId')) tiktok('PageView'); // ttq.page() automático
|
|
451
|
+
if (active('pinterestTagId')) pinterest('pagevisit');
|
|
452
|
+
if (active('redditPixelId')) reddit('PageVisit');
|
|
453
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'pageLoad' }); // Bing
|
|
454
|
+
ga4Event('page_view', { page_title: title, page_location: url });
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/** ViewContent — ao carregar a página do produto/imóvel/serviço (NO useEffect, não no scroll) */
|
|
458
|
+
export const trackViewContent = (params = {}) => {
|
|
459
|
+
const { _ga4Items = [], _tiktok = {}, ...metaParams } = params;
|
|
460
|
+
|
|
461
|
+
if (active('metaPixelId')) meta('track', 'ViewContent', metaParams);
|
|
462
|
+
if (active('tiktokPixelId')) tiktok('ViewContent', {
|
|
463
|
+
content_id: (_tiktok.content_id || metaParams.content_ids?.[0] || ''),
|
|
464
|
+
content_name: metaParams.content_name || '',
|
|
465
|
+
content_type: metaParams.content_type || 'product',
|
|
466
|
+
value: metaParams.value || 0,
|
|
467
|
+
currency: metaParams.currency || 'BRL',
|
|
468
|
+
..._tiktok
|
|
469
|
+
});
|
|
470
|
+
if (active('pinterestTagId')) pinterest('viewcategory', {
|
|
471
|
+
product_id: metaParams.content_ids?.[0] || '',
|
|
472
|
+
product_name: metaParams.content_name || '',
|
|
473
|
+
value: metaParams.value || 0,
|
|
474
|
+
currency: metaParams.currency || 'BRL',
|
|
475
|
+
});
|
|
476
|
+
if (active('redditPixelId')) reddit('ViewContent', { value: metaParams.value, currency: metaParams.currency });
|
|
477
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'view_content' });
|
|
478
|
+
|
|
479
|
+
ga4Ecommerce('view_item', {
|
|
480
|
+
currency: metaParams.currency || 'BRL',
|
|
481
|
+
value: metaParams.value || 0,
|
|
482
|
+
items: _ga4Items
|
|
483
|
+
});
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// ── Geolocalização nativa (cidade do lead via OpenStreetMap — gratuito) ──
|
|
487
|
+
const getCidade = () => new Promise((resolve) => {
|
|
488
|
+
if (!isBrowser || !navigator.geolocation) return resolve(null);
|
|
489
|
+
navigator.geolocation.getCurrentPosition(
|
|
490
|
+
async ({ coords: { latitude, longitude } }) => {
|
|
491
|
+
try {
|
|
492
|
+
const r = await fetch(
|
|
493
|
+
`https://nominatim.openstreetmap.org/reverse?lat=${latitude}&lon=${longitude}&format=json`,
|
|
494
|
+
{ headers: { 'Accept-Language': 'pt-BR' } }
|
|
495
|
+
);
|
|
496
|
+
const d = await r.json();
|
|
497
|
+
resolve(d.address?.city || d.address?.town || d.address?.municipality || null);
|
|
498
|
+
} catch { resolve(null); }
|
|
499
|
+
},
|
|
500
|
+
() => resolve(null), // usuário negou permissão → ignora silenciosamente
|
|
501
|
+
{ timeout: 5000 }
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
/** Lead — envio de formulário (CONVERSÃO PRINCIPAL) — captura cidade automaticamente */
|
|
506
|
+
export const trackLead = async (params = {}) => {
|
|
507
|
+
const eventId = genId(); // ID único — deduplicação browser ↔ servidor
|
|
508
|
+
const cidade = await getCidade(); // tenta capturar cidade
|
|
509
|
+
const base = { value: 0, currency: 'BRL', ...params, ...(cidade && { city: cidade }) };
|
|
510
|
+
|
|
511
|
+
// Meta: eventID no 3º argumento → deduplicação com CAPI
|
|
512
|
+
if (active('metaPixelId')) meta('track', 'Lead', base, { eventID: eventId });
|
|
513
|
+
if (active('tiktokPixelId')) {
|
|
514
|
+
// ttq.identify(): Advanced Matching — vincula eventos ao usuário (cross-session)
|
|
515
|
+
// Chamar ANTES do track para que o evento já carregue o match key
|
|
516
|
+
if ((base.email || base.phone) && has('ttq')) {
|
|
517
|
+
window.ttq.identify({
|
|
518
|
+
email: base.email?.toLowerCase().trim() || '',
|
|
519
|
+
phone_number: base.phone ? `+${String(base.phone).replace(/\D/g, '')}` : '',
|
|
520
|
+
external_id: _userId || '',
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
tiktok('Lead', { content_name: base.content_name || '', value: base.value, currency: base.currency });
|
|
524
|
+
}
|
|
525
|
+
if (active('pinterestTagId')) pinterest('lead', { lead_type: base.content_category || 'formulario', value: base.value, currency: base.currency });
|
|
526
|
+
if (active('redditPixelId')) reddit('Lead', { value: base.value, currency: base.currency });
|
|
527
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'submit_form', ev: base.value || 0, ec: base.currency || 'BRL' });
|
|
528
|
+
|
|
529
|
+
// event_id incluído para deduplicação browser ↔ servidor
|
|
530
|
+
ga4Event('generate_lead', { method: 'formulario', currency: base.currency, value: base.value, event_id: eventId, ...(cidade && { city: cidade }) });
|
|
531
|
+
gadsConversion(CONFIG.googleAdsConversions?.lead, base.value, base.currency, '', base.email, base.phone);
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
/** Contact — clique em WhatsApp / telefone — captura cidade e ctwa_clid automaticamente */
|
|
535
|
+
export const trackContact = async (method = 'whatsapp') => {
|
|
536
|
+
const eventId = genId();
|
|
537
|
+
const cidade = await getCidade();
|
|
538
|
+
const extra = cidade ? { city: cidade } : {};
|
|
539
|
+
|
|
540
|
+
// Meta: passa eventID para deduplicação com CAPI
|
|
541
|
+
if (active('metaPixelId')) has('fbq') && window.fbq('track', 'Contact', extra, { eventID: eventId });
|
|
542
|
+
if (active('tiktokPixelId')) tiktok('Contact', extra);
|
|
543
|
+
if (active('pinterestTagId')) pinterest('lead', { lead_type: method });
|
|
544
|
+
if (active('redditPixelId')) reddit('Lead', extra);
|
|
545
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'contact', el: method });
|
|
546
|
+
|
|
547
|
+
// event_id incluído para deduplicação Meta
|
|
548
|
+
ga4Event('generate_lead', { method, currency: 'BRL', value: 0, event_id: eventId, ...extra });
|
|
549
|
+
gadsConversion(CONFIG.googleAdsConversions?.whatsapp);
|
|
550
|
+
|
|
551
|
+
// Server-side: envia ctwa_clid se veio de anúncio Click-to-WhatsApp
|
|
552
|
+
sendServerEvent('Contact', eventId, {
|
|
553
|
+
city: cidade || '',
|
|
554
|
+
method,
|
|
555
|
+
// ctwaClid é incluído automaticamente via getCookies() dentro de sendServerEvent
|
|
556
|
+
});
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
/** Schedule — agendamento */
|
|
560
|
+
export const trackSchedule = () => {
|
|
561
|
+
if (active('metaPixelId')) meta('track', 'Schedule');
|
|
562
|
+
if (active('tiktokPixelId')) tiktok('Schedule');
|
|
563
|
+
if (active('pinterestTagId')) pinterest('lead', { lead_type: 'agendamento' });
|
|
564
|
+
if (active('redditPixelId')) reddit('Lead');
|
|
565
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'schedule' });
|
|
566
|
+
|
|
567
|
+
ga4Event('generate_lead', { method: 'agendamento', currency: 'BRL', value: 0 });
|
|
568
|
+
gadsConversion(CONFIG.googleAdsConversions?.schedule);
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
/** InitiateCheckout — clique em "Comprar" */
|
|
572
|
+
export const trackInitiateCheckout = (params = {}) => {
|
|
573
|
+
const { _ga4Items = [], ...p } = params;
|
|
574
|
+
const base = { currency: 'BRL', value: 0, ...p };
|
|
575
|
+
|
|
576
|
+
if (active('metaPixelId')) meta('track', 'InitiateCheckout', base);
|
|
577
|
+
if (active('tiktokPixelId')) tiktok('InitiateCheckout', { value: base.value, currency: base.currency, content_id: base.content_ids?.[0] });
|
|
578
|
+
if (active('pinterestTagId')) pinterest('checkout', { value: base.value, currency: base.currency, order_quantity: 1 });
|
|
579
|
+
if (active('redditPixelId')) reddit('AddToCart', { value: base.value, currency: base.currency });
|
|
580
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'initiate_checkout', ev: base.value, ec: base.currency });
|
|
581
|
+
|
|
582
|
+
ga4Ecommerce('begin_checkout', { currency: base.currency, value: base.value, items: _ga4Items });
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
/** AddToCart — adicionar ao carrinho */
|
|
586
|
+
export const trackAddToCart = (params = {}) => {
|
|
587
|
+
const { _ga4Items = [], ...p } = params;
|
|
588
|
+
const base = { currency: 'BRL', value: 0, ...p };
|
|
589
|
+
// Converte GA4 items → formato Meta/TikTok automaticamente (Universal Conversion)
|
|
590
|
+
const metaP = _ga4Items.length ? { ...base, ...ga4ItemsToMeta(_ga4Items) } : base;
|
|
591
|
+
const ttkP = _ga4Items.length ? ga4ItemsToTikTok(_ga4Items) : { value: base.value, currency: base.currency };
|
|
592
|
+
|
|
593
|
+
if (active('metaPixelId')) meta('track', 'AddToCart', metaP);
|
|
594
|
+
if (active('tiktokPixelId')) tiktok('AddToCart', { ...ttkP, currency: base.currency, content_id: metaP.content_ids?.[0] });
|
|
595
|
+
if (active('pinterestTagId')) pinterest('addtocart', { value: metaP.value, currency: base.currency, product_id: metaP.content_ids?.[0] });
|
|
596
|
+
if (active('redditPixelId')) reddit('AddToCart', { value: metaP.value, currency: base.currency });
|
|
597
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'add_to_cart', ev: metaP.value, ec: base.currency });
|
|
598
|
+
|
|
599
|
+
ga4Ecommerce('add_to_cart', { currency: base.currency, value: metaP.value, items: _ga4Items });
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
/** Purchase — compra concluída (value + currency obrigatórios) */
|
|
603
|
+
export const trackPurchase = (params = {}) => {
|
|
604
|
+
const { _ga4Items = [], ...p } = params;
|
|
605
|
+
if (!p.value || !p.currency) console.warn('[CDP Edge] trackPurchase requer value e currency');
|
|
606
|
+
const txId = p.order_id || `T_${Date.now()}`;
|
|
607
|
+
// Converte GA4 items → formato Meta/TikTok automaticamente (Universal Conversion)
|
|
608
|
+
const metaP = _ga4Items.length ? { ...p, ...ga4ItemsToMeta(_ga4Items) } : p;
|
|
609
|
+
const ttkP = _ga4Items.length ? ga4ItemsToTikTok(_ga4Items) : { value: p.value };
|
|
610
|
+
|
|
611
|
+
if (active('metaPixelId')) meta('track', 'Purchase', { currency: p.currency || 'BRL', ...metaP });
|
|
612
|
+
if (active('tiktokPixelId')) {
|
|
613
|
+
// identify() antes do track: Advanced Matching vincula compra ao usuário
|
|
614
|
+
if ((p.email || p.phone) && has('ttq')) {
|
|
615
|
+
window.ttq.identify({
|
|
616
|
+
email: p.email?.toLowerCase().trim() || '',
|
|
617
|
+
phone_number: p.phone ? `+${String(p.phone).replace(/\D/g, '')}` : '',
|
|
618
|
+
external_id: _userId || '',
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
tiktok('CompletePayment', { ...ttkP, currency: p.currency || 'BRL', order_id: txId });
|
|
622
|
+
}
|
|
623
|
+
if (active('pinterestTagId')) pinterest('checkout', { value: metaP.value, currency: p.currency || 'BRL', order_id: txId, order_quantity: metaP.num_items || 1 });
|
|
624
|
+
if (active('redditPixelId')) reddit('Purchase', { value: metaP.value, currency: p.currency || 'BRL' });
|
|
625
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'purchase', ev: p.value, ec: p.currency || 'BRL', gc: txId });
|
|
626
|
+
|
|
627
|
+
// event_id = txId incluído para deduplicação Meta
|
|
628
|
+
ga4Ecommerce('purchase', { transaction_id: txId, event_id: txId, currency: p.currency || 'BRL', value: p.value, items: _ga4Items });
|
|
629
|
+
gadsConversion(CONFIG.googleAdsConversions?.purchase, p.value, p.currency || 'BRL', txId, p.email, p.phone);
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
/** CompleteRegistration — cadastro concluído */
|
|
633
|
+
export const trackSignUp = (method = 'formulario') => {
|
|
634
|
+
if (active('metaPixelId')) meta('track', 'CompleteRegistration', { status: 'completed' });
|
|
635
|
+
if (active('tiktokPixelId')) tiktok('CompleteRegistration');
|
|
636
|
+
if (active('pinterestTagId')) pinterest('signup', { lead_type: method });
|
|
637
|
+
if (active('redditPixelId')) reddit('SignUp');
|
|
638
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'sign_up' });
|
|
639
|
+
|
|
640
|
+
ga4Event('sign_up', { method });
|
|
641
|
+
gadsConversion(CONFIG.googleAdsConversions?.signup);
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
// ============================================================
|
|
645
|
+
// EVENTOS COMPLETOS — E-COMMERCE E FUNIL AVANÇADO
|
|
646
|
+
// ============================================================
|
|
647
|
+
|
|
648
|
+
/** Search — busca no site */
|
|
649
|
+
export const trackSearch = (query = '') => {
|
|
650
|
+
if (active('metaPixelId')) meta('track', 'Search', { search_string: query });
|
|
651
|
+
if (active('tiktokPixelId')) tiktok('Search', { query });
|
|
652
|
+
if (active('redditPixelId')) reddit('Search');
|
|
653
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'search', el: query });
|
|
654
|
+
ga4Event('search', { search_term: query });
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
/** AddToWishlist — adicionar à lista de desejos / favoritos */
|
|
658
|
+
export const trackAddToWishlist = (params = {}) => {
|
|
659
|
+
const { _ga4Items = [], ...p } = params;
|
|
660
|
+
const base = { currency: 'BRL', value: 0, ...p };
|
|
661
|
+
|
|
662
|
+
if (active('metaPixelId')) meta('track', 'AddToWishlist', base);
|
|
663
|
+
if (active('tiktokPixelId')) tiktok('AddToWishlist', { content_id: base.content_ids?.[0], value: base.value, currency: base.currency });
|
|
664
|
+
if (active('redditPixelId')) reddit('AddToWishlist');
|
|
665
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'add_to_wishlist', ev: base.value, ec: base.currency });
|
|
666
|
+
ga4Ecommerce('add_to_wishlist', { currency: base.currency, value: base.value, items: _ga4Items });
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
/** RemoveFromCart — remover item do carrinho */
|
|
670
|
+
export const trackRemoveFromCart = (params = {}) => {
|
|
671
|
+
const { _ga4Items = [], ...p } = params;
|
|
672
|
+
const base = { currency: 'BRL', value: 0, ...p };
|
|
673
|
+
|
|
674
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'remove_from_cart', ev: base.value, ec: base.currency });
|
|
675
|
+
ga4Ecommerce('remove_from_cart', { currency: base.currency, value: base.value, items: _ga4Items });
|
|
676
|
+
// Meta e TikTok não têm RemoveFromCart padrão — usar trackCustom se necessário
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
/** ViewCart — visualizar o carrinho */
|
|
680
|
+
export const trackViewCart = (params = {}) => {
|
|
681
|
+
const { _ga4Items = [], ...p } = params;
|
|
682
|
+
const base = { currency: 'BRL', value: 0, ...p };
|
|
683
|
+
|
|
684
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'view_cart' });
|
|
685
|
+
ga4Ecommerce('view_cart', { currency: base.currency, value: base.value, items: _ga4Items });
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
/** ViewItemList — listagem de produtos (categoria, busca, vitrine) */
|
|
689
|
+
export const trackViewItemList = (params = {}) => {
|
|
690
|
+
// aceita item_list_id/item_list_name (GA4 spec) ou list_id/list_name (compat)
|
|
691
|
+
const {
|
|
692
|
+
_ga4Items = [], items = _ga4Items,
|
|
693
|
+
item_list_id = params.list_id || '',
|
|
694
|
+
item_list_name = params.list_name || '',
|
|
695
|
+
} = params;
|
|
696
|
+
|
|
697
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'view_item_list' });
|
|
698
|
+
ga4Ecommerce('view_item_list', { item_list_name, item_list_id, items });
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
/** SelectItem — clicou em produto na lista */
|
|
702
|
+
export const trackSelectItem = (params = {}) => {
|
|
703
|
+
const {
|
|
704
|
+
_ga4Items = [], items = _ga4Items,
|
|
705
|
+
item_list_id = params.list_id || '',
|
|
706
|
+
item_list_name = params.list_name || '',
|
|
707
|
+
} = params;
|
|
708
|
+
|
|
709
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'select_item' });
|
|
710
|
+
ga4Ecommerce('select_item', { item_list_name, item_list_id, items });
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
/** AddPaymentInfo — informou dados de pagamento */
|
|
714
|
+
export const trackAddPaymentInfo = (params = {}) => {
|
|
715
|
+
const { _ga4Items = [], payment_type = '', ...p } = params;
|
|
716
|
+
const base = { currency: 'BRL', value: 0, ...p };
|
|
717
|
+
|
|
718
|
+
if (active('metaPixelId')) meta('track', 'AddPaymentInfo', base);
|
|
719
|
+
if (active('tiktokPixelId')) tiktok('AddPaymentInfo', { value: base.value, currency: base.currency });
|
|
720
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'add_payment_info', ev: base.value, ec: base.currency });
|
|
721
|
+
ga4Ecommerce('add_payment_info', { currency: base.currency, value: base.value, payment_type, items: _ga4Items });
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
/** AddShippingInfo — informou dados de entrega */
|
|
725
|
+
export const trackAddShippingInfo = (params = {}) => {
|
|
726
|
+
const { _ga4Items = [], shipping_tier = '', ...p } = params;
|
|
727
|
+
const base = { currency: 'BRL', value: 0, ...p };
|
|
728
|
+
|
|
729
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'add_shipping_info' });
|
|
730
|
+
ga4Ecommerce('add_shipping_info', { currency: base.currency, value: base.value, shipping_tier, items: _ga4Items });
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
/** ViewPromotion — visualizou banner/promoção */
|
|
734
|
+
export const trackViewPromotion = (params = {}) => {
|
|
735
|
+
const { _ga4Items = [], promotion_id = '', promotion_name = '', creative_name = '', creative_slot = '' } = params;
|
|
736
|
+
|
|
737
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'view_promotion' });
|
|
738
|
+
ga4Ecommerce('view_promotion', { promotion_id, promotion_name, creative_name, creative_slot, items: _ga4Items });
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
/** SelectPromotion — clicou em banner/promoção */
|
|
742
|
+
export const trackSelectPromotion = (params = {}) => {
|
|
743
|
+
const { _ga4Items = [], promotion_id = '', promotion_name = '', creative_name = '', creative_slot = '' } = params;
|
|
744
|
+
|
|
745
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'select_promotion' });
|
|
746
|
+
ga4Ecommerce('select_promotion', { promotion_id, promotion_name, creative_name, creative_slot, items: _ga4Items });
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
/** Subscribe — assinou newsletter ou serviço recorrente */
|
|
750
|
+
export const trackSubscribe = (params = {}) => {
|
|
751
|
+
const base = { value: 0, currency: 'BRL', predicted_ltv: 0, ...params };
|
|
752
|
+
|
|
753
|
+
if (active('metaPixelId')) meta('track', 'Subscribe', base);
|
|
754
|
+
if (active('tiktokPixelId')) tiktok('Subscribe', { value: base.value, currency: base.currency });
|
|
755
|
+
if (active('redditPixelId')) reddit('Lead');
|
|
756
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'subscribe', ev: base.value, ec: base.currency });
|
|
757
|
+
ga4Event('generate_lead', { method: 'subscribe', currency: base.currency, value: base.value });
|
|
758
|
+
gadsConversion(CONFIG.googleAdsConversions?.signup);
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
/** StartTrial — iniciou período de teste gratuito
|
|
762
|
+
* TikTok: usa evento padrão 'StartTrial' (não 'Subscribe') */
|
|
763
|
+
export const trackStartTrial = (params = {}) => {
|
|
764
|
+
const base = { value: 0, currency: 'BRL', predicted_ltv: 0, ...params };
|
|
765
|
+
|
|
766
|
+
if (active('metaPixelId')) meta('track', 'StartTrial', base);
|
|
767
|
+
if (active('tiktokPixelId')) tiktok('StartTrial', { value: base.value, currency: base.currency });
|
|
768
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'start_trial' });
|
|
769
|
+
ga4Event('generate_lead', { method: 'trial', currency: base.currency, value: base.value });
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
/** FindLocation — buscou localização física da empresa */
|
|
773
|
+
export const trackFindLocation = () => {
|
|
774
|
+
if (active('metaPixelId')) meta('track', 'FindLocation');
|
|
775
|
+
if (active('tiktokPixelId')) tiktok('FindLocation');
|
|
776
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'find_location' });
|
|
777
|
+
ga4Event('find_location');
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
/** Donate — fez uma doação */
|
|
781
|
+
export const trackDonate = (params = {}) => {
|
|
782
|
+
const base = { value: 0, currency: 'BRL', ...params };
|
|
783
|
+
|
|
784
|
+
if (active('metaPixelId')) meta('track', 'Donate', base);
|
|
785
|
+
if (active('redditPixelId')) reddit('Purchase', { value: base.value, currency: base.currency });
|
|
786
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'donate', ev: base.value, ec: base.currency });
|
|
787
|
+
ga4Event('generate_lead', { method: 'donate', currency: base.currency, value: base.value });
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
/** SubmitApplication — enviou candidatura, inscrição ou formulário longo */
|
|
791
|
+
export const trackSubmitApplication = (params = {}) => {
|
|
792
|
+
const base = { value: 0, currency: 'BRL', ...params };
|
|
793
|
+
|
|
794
|
+
if (active('metaPixelId')) meta('track', 'SubmitApplication', base);
|
|
795
|
+
if (active('tiktokPixelId')) tiktok('SubmitForm', { value: base.value, currency: base.currency });
|
|
796
|
+
if (active('redditPixelId')) reddit('Lead');
|
|
797
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'submit_application' });
|
|
798
|
+
ga4Event('generate_lead', { method: 'application', currency: base.currency, value: base.value });
|
|
799
|
+
gadsConversion(CONFIG.googleAdsConversions?.lead);
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
/** Download — download de arquivo (PDF, app, material) */
|
|
803
|
+
export const trackDownload = (params = {}) => {
|
|
804
|
+
const base = { file_name: '', file_extension: '', link_url: '', ...params };
|
|
805
|
+
|
|
806
|
+
if (active('tiktokPixelId')) tiktok('Download', { content_name: base.file_name });
|
|
807
|
+
if (isBrowser && window.uetq) window.uetq.push({ ea: 'file_download', el: base.file_name });
|
|
808
|
+
ga4Event('file_download', { file_name: base.file_name, file_extension: base.file_extension, link_url: base.link_url });
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
/** CustomizeProduct — personalizou produto (cor, tamanho, gravação, etc.) */
|
|
812
|
+
export const trackCustomizeProduct = (params = {}) => {
|
|
813
|
+
if (active('metaPixelId')) meta('track', 'CustomizeProduct', params);
|
|
814
|
+
if (active('tiktokPixelId')) tiktok('CustomizeProduct', { content_name: params.content_name || '' });
|
|
815
|
+
ga4Event('customize_product', params);
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// ============================================================
|
|
819
|
+
// EVENTO GENÉRICO CUSTOMIZADO
|
|
820
|
+
// ============================================================
|
|
821
|
+
|
|
822
|
+
/** Evento customizado — qualquer nome de evento não coberto acima */
|
|
823
|
+
export const trackCustom = (metaEventName, ga4EventName, params = {}) => {
|
|
824
|
+
if (active('metaPixelId')) window.fbq?.('trackCustom', metaEventName, params);
|
|
825
|
+
if (active('tiktokPixelId')) window.ttq?.track(metaEventName, params);
|
|
826
|
+
ga4Event(ga4EventName, params);
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
/** Scroll depth tracker — utilitário para medir profundidade de scroll */
|
|
830
|
+
export const scrollTracker = (threshold = 50, callback) => {
|
|
831
|
+
let fired = false;
|
|
832
|
+
return () => {
|
|
833
|
+
if (fired) return;
|
|
834
|
+
const pct = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
|
|
835
|
+
if (pct >= threshold) { fired = true; callback(); }
|
|
836
|
+
};
|
|
837
|
+
};
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
---
|
|
841
|
+
|
|
842
|
+
## PASSO 4 — Eventos por página e tipo de negócio
|
|
843
|
+
|
|
844
|
+
### Regra de ouro por plataforma (NUNCA violar)
|
|
845
|
+
|
|
846
|
+
| Plataforma | Regra crítica |
|
|
847
|
+
|---|---|
|
|
848
|
+
| **Meta** | `ViewContent` sempre no load da página — NUNCA em scroll. `Lead`/`Purchase` apenas quando ação é confirmada (onSubmit, não onClick) |
|
|
849
|
+
| **GA4** | Sempre `{ecommerce: null}` antes de qualquer evento de e-commerce |
|
|
850
|
+
| **TikTok** | `CompletePayment` = Purchase. `SubmitForm` = Lead. Nunca usar eventos Meta como nomes |
|
|
851
|
+
| **Google Ads** | Conversão só no evento real de conversão (Lead confirmado, Purchase confirmado). Não usar em PageView |
|
|
852
|
+
| **Geral** | `trackPageView()` em TODA rota via `useEffect(()=>{}, [])`. Em SPA nunca confiar no carregamento automático |
|
|
853
|
+
|
|
854
|
+
---
|
|
855
|
+
|
|
856
|
+
### 📄 TIPO 1 — Landing Page / Página de Captura (Infoproduto, Serviço, Imóvel)
|
|
857
|
+
|
|
858
|
+
**Funil:** Visita → Interesse → Lead (formulário ou WhatsApp)
|
|
859
|
+
|
|
860
|
+
```jsx
|
|
861
|
+
// Eventos no LOAD (automáticos)
|
|
862
|
+
// ✅ PageView → todas as plataformas
|
|
863
|
+
// ✅ ViewContent → todas as plataformas (sinaliza o que o usuário está vendo)
|
|
864
|
+
|
|
865
|
+
// Eventos na INTERAÇÃO (nunca automáticos)
|
|
866
|
+
// ✅ Lead → no onSubmit do formulário (NÃO no onClick do botão)
|
|
867
|
+
// ✅ Contact → no clique no botão de WhatsApp/telefone
|
|
868
|
+
// ✅ Schedule → no clique em "Agendar"
|
|
869
|
+
// ✅ trackCustom('FormStart') → no onFocus do primeiro campo (micro-conversão)
|
|
870
|
+
// ❌ NUNCA: ViewContent no scroll, Lead no onClick, PageView duplicado
|
|
871
|
+
|
|
872
|
+
import { useEffect } from 'react';
|
|
873
|
+
import {
|
|
874
|
+
trackPageView, trackViewContent, trackLead,
|
|
875
|
+
trackContact, trackSchedule, trackCustom, scrollTracker,
|
|
876
|
+
readField, normalizePhone, splitName, isValidEmail, normalizeCity, // utilitários de formulário
|
|
877
|
+
} from '../tracking/tracking';
|
|
878
|
+
|
|
879
|
+
// ── Dados do conteúdo da página ────────────────────────────
|
|
880
|
+
const CONTEUDO = {
|
|
881
|
+
content_ids: ['ID_PRODUTO'],
|
|
882
|
+
content_type: 'product', // 'home_listing' para imóveis
|
|
883
|
+
content_name: 'Nome do Produto / Serviço',
|
|
884
|
+
value: 0, // 0 se não tiver preço fixo; preço real se tiver
|
|
885
|
+
currency: 'BRL',
|
|
886
|
+
_ga4Items: [{ item_id: 'ID_PRODUTO', item_name: 'Nome do Produto', price: 0, quantity: 1 }],
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
export function LandingPage() {
|
|
890
|
+
|
|
891
|
+
// ── LOAD: PageView + ViewContent (obrigatórios em toda landing page) ──
|
|
892
|
+
useEffect(() => {
|
|
893
|
+
trackPageView();
|
|
894
|
+
trackViewContent(CONTEUDO);
|
|
895
|
+
}, []);
|
|
896
|
+
|
|
897
|
+
// ── SCROLL: micro-conversão de interesse (opcional, não substitui ViewContent) ──
|
|
898
|
+
useEffect(() => {
|
|
899
|
+
return scrollTracker([75], (pct) =>
|
|
900
|
+
trackCustom('ScrollEngagement', { percent_scrolled: pct }));
|
|
901
|
+
}, []);
|
|
902
|
+
|
|
903
|
+
// ── INTERAÇÕES ──────────────────────────────────────────
|
|
904
|
+
const onWhatsApp = async () => {
|
|
905
|
+
await trackContact('whatsapp'); // Meta: Contact | GA4: generate_lead | TikTok: Contact
|
|
906
|
+
window.open('https://wa.me/55SEUNUMERO', '_blank');
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
const onFormFocus = () =>
|
|
910
|
+
trackCustom('FormStart', { form_name: 'captacao_lead' }); // sinaliza interesse em preencher
|
|
911
|
+
|
|
912
|
+
const onSubmit = async (e) => {
|
|
913
|
+
e.preventDefault();
|
|
914
|
+
|
|
915
|
+
// ── Leitura e normalização dos campos ──────────────────────────────
|
|
916
|
+
// readField() busca por id="X" ou name="X" — adaptar ao HTML real do site
|
|
917
|
+
const emailRaw = readField('email'); // id="email" ou name="email"
|
|
918
|
+
const nomeRaw = readField('nome'); // id="nome" ou name="nome"
|
|
919
|
+
const telRaw = readField('telefone'); // id="telefone" ou name="telefone"
|
|
920
|
+
|
|
921
|
+
// Validar email antes de disparar
|
|
922
|
+
if (!isValidEmail(emailRaw)) return; // ← não dispara se email inválido
|
|
923
|
+
|
|
924
|
+
// Separar nome em firstName / lastName (alinha com campos fn/ln do Meta CAPI)
|
|
925
|
+
const { firstName, lastName } = splitName(nomeRaw);
|
|
926
|
+
|
|
927
|
+
// Normalizar telefone para formato Meta: somente dígitos + DDI
|
|
928
|
+
const phone = normalizePhone(telRaw); // ex: '5511999998888'
|
|
929
|
+
|
|
930
|
+
await trackLead({ // Meta: Lead | GA4: generate_lead | TikTok: SubmitForm
|
|
931
|
+
email: emailRaw, // → SHA256 no servidor (Meta Advanced Matching / Google Enhanced Conversions)
|
|
932
|
+
phone, // → já normalizado; SHA256 no servidor
|
|
933
|
+
firstName, // → SHA256 no servidor (campo fn no Meta CAPI)
|
|
934
|
+
lastName, // → SHA256 no servidor (campo ln no Meta CAPI)
|
|
935
|
+
content_name: CONTEUDO.content_name,
|
|
936
|
+
});
|
|
937
|
+
// redirecionar ou mostrar mensagem de sucesso
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
const onSchedule = () =>
|
|
941
|
+
trackSchedule(); // Meta: Schedule | GA4: generate_lead | TikTok: Schedule
|
|
942
|
+
|
|
943
|
+
return (
|
|
944
|
+
<div>
|
|
945
|
+
<button onClick={onWhatsApp}>Falar no WhatsApp</button>
|
|
946
|
+
{/* ── Campos com id E name ── */}
|
|
947
|
+
<form onSubmit={onSubmit}>
|
|
948
|
+
<input id="nome" name="nome" placeholder="Seu nome completo" required />
|
|
949
|
+
<input id="email" name="email" type="email" placeholder="Seu e-mail" required onFocus={onFormFocus} />
|
|
950
|
+
<input id="telefone" name="telefone" type="tel" placeholder="Seu telefone" />
|
|
951
|
+
<button type="submit">Quero saber mais</button>
|
|
952
|
+
</form>
|
|
953
|
+
<button onClick={onSchedule}>Agendar visita</button>
|
|
954
|
+
</div>
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
---
|
|
961
|
+
|
|
962
|
+
### 🏠 TIPO 2 — Página de Imóvel (Real Estate)
|
|
963
|
+
|
|
964
|
+
**Diferença:** `content_type: 'home_listing'` obrigatório para Meta Dynamic Ads imobiliário.
|
|
965
|
+
|
|
966
|
+
```jsx
|
|
967
|
+
const IMOVEL = {
|
|
968
|
+
content_ids: ['APT-SBC-001'],
|
|
969
|
+
content_type: 'home_listing', // ← obrigatório para catálogo Meta imobiliário
|
|
970
|
+
content_name: 'Apartamento 3 Quartos - São Bernardo',
|
|
971
|
+
city: 'São Bernardo do Campo',
|
|
972
|
+
region: 'SP',
|
|
973
|
+
country: 'BR',
|
|
974
|
+
neighborhood: 'Bairro Nobre',
|
|
975
|
+
value: 850000,
|
|
976
|
+
currency: 'BRL',
|
|
977
|
+
property_size: 260,
|
|
978
|
+
num_baths: 2,
|
|
979
|
+
num_beds: 3,
|
|
980
|
+
parking_spaces: 3,
|
|
981
|
+
_ga4Items: [{
|
|
982
|
+
item_id: 'APT-SBC-001', item_name: 'Apartamento 3 Quartos',
|
|
983
|
+
item_category: 'Residencial', item_category2: 'São Bernardo do Campo',
|
|
984
|
+
price: 850000, quantity: 1
|
|
985
|
+
}],
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
export function PaginaImovel() {
|
|
989
|
+
useEffect(() => {
|
|
990
|
+
trackPageView();
|
|
991
|
+
trackViewContent(IMOVEL); // content_type: 'home_listing' → Dynamic Ads imobiliário
|
|
992
|
+
}, []);
|
|
993
|
+
|
|
994
|
+
// Scroll 75%: sinaliza leitura profunda (interesse qualificado)
|
|
995
|
+
useEffect(() => {
|
|
996
|
+
return scrollTracker([75], (pct) =>
|
|
997
|
+
trackCustom('ImovelLido', { content_name: IMOVEL.content_name, scroll_depth: pct }));
|
|
998
|
+
}, []);
|
|
999
|
+
|
|
1000
|
+
const onWhatsApp = async () => { await trackContact('whatsapp'); window.open('https://wa.me/55...', '_blank'); };
|
|
1001
|
+
const onSubmit = async (e, d) => { e.preventDefault(); await trackLead({ ...d, content_name: IMOVEL.content_name }); };
|
|
1002
|
+
const onSchedule = () => trackSchedule();
|
|
1003
|
+
}
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
---
|
|
1007
|
+
|
|
1008
|
+
### 🛒 TIPO 3 — E-commerce: Página de Listagem de Produtos
|
|
1009
|
+
|
|
1010
|
+
**Funil:** Listagem → Produto → Carrinho → Checkout → Confirmação
|
|
1011
|
+
|
|
1012
|
+
```jsx
|
|
1013
|
+
// ── Listagem (Category / Collection page) ──────────────────
|
|
1014
|
+
// ✅ PageView no load
|
|
1015
|
+
// ✅ ViewItemList no load (GA4 e Bing — lista de produtos visíveis)
|
|
1016
|
+
// ✅ SelectItem no clique em qualquer produto
|
|
1017
|
+
// ✅ ViewPromotion se houver banner de promoção visível no load
|
|
1018
|
+
// ✅ SelectPromotion no clique no banner
|
|
1019
|
+
|
|
1020
|
+
import {
|
|
1021
|
+
trackPageView, trackViewItemList, trackSelectItem,
|
|
1022
|
+
trackViewPromotion, trackSelectPromotion, trackSearch
|
|
1023
|
+
} from '../tracking/tracking';
|
|
1024
|
+
|
|
1025
|
+
export function ListagemProdutos({ produtos, categoria }) {
|
|
1026
|
+
useEffect(() => {
|
|
1027
|
+
trackPageView();
|
|
1028
|
+
trackViewItemList({
|
|
1029
|
+
item_list_id: categoria.id,
|
|
1030
|
+
item_list_name: categoria.nome,
|
|
1031
|
+
items: produtos.map((p, i) => ({
|
|
1032
|
+
item_id: p.id, item_name: p.nome,
|
|
1033
|
+
item_category: categoria.nome, price: p.preco,
|
|
1034
|
+
index: i, quantity: 1,
|
|
1035
|
+
})),
|
|
1036
|
+
});
|
|
1037
|
+
// Se tiver banner de promoção visível na página:
|
|
1038
|
+
trackViewPromotion({ promotion_id: 'banner_topo', promotion_name: 'Oferta da Semana' });
|
|
1039
|
+
}, []);
|
|
1040
|
+
|
|
1041
|
+
const onClicarProduto = (produto, index) => {
|
|
1042
|
+
trackSelectItem({
|
|
1043
|
+
item_list_id: categoria.id,
|
|
1044
|
+
item_list_name: categoria.nome,
|
|
1045
|
+
items: [{ item_id: produto.id, item_name: produto.nome, price: produto.preco, index }],
|
|
1046
|
+
});
|
|
1047
|
+
// navegar para página do produto
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
const onClicarBanner = () =>
|
|
1051
|
+
trackSelectPromotion({ promotion_id: 'banner_topo', promotion_name: 'Oferta da Semana' });
|
|
1052
|
+
|
|
1053
|
+
const onPesquisar = (termo) =>
|
|
1054
|
+
trackSearch(termo); // Meta: Search | GA4: search | TikTok: Search
|
|
1055
|
+
|
|
1056
|
+
return (/* jsx */);
|
|
1057
|
+
}
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
---
|
|
1061
|
+
|
|
1062
|
+
### 🛍️ TIPO 4 — E-commerce: Página de Produto
|
|
1063
|
+
|
|
1064
|
+
```jsx
|
|
1065
|
+
// ✅ PageView + ViewContent no load (obrigatório)
|
|
1066
|
+
// ✅ AddToCart no clique em "Adicionar ao carrinho"
|
|
1067
|
+
// ✅ AddToWishlist no clique em "Favoritar"
|
|
1068
|
+
// ✅ CustomizeProduct se houver seleção de variante (cor, tamanho)
|
|
1069
|
+
// ❌ NUNCA: AddToCart automático no load
|
|
1070
|
+
|
|
1071
|
+
import {
|
|
1072
|
+
trackPageView, trackViewContent, trackAddToCart,
|
|
1073
|
+
trackAddToWishlist, trackCustomizeProduct
|
|
1074
|
+
} from '../tracking/tracking';
|
|
1075
|
+
|
|
1076
|
+
export function PaginaProduto({ produto }) {
|
|
1077
|
+
useEffect(() => {
|
|
1078
|
+
trackPageView();
|
|
1079
|
+
trackViewContent({
|
|
1080
|
+
content_ids: [produto.id],
|
|
1081
|
+
content_type: 'product',
|
|
1082
|
+
content_name: produto.nome,
|
|
1083
|
+
value: produto.preco,
|
|
1084
|
+
currency: 'BRL',
|
|
1085
|
+
_ga4Items: [{ item_id: produto.id, item_name: produto.nome, price: produto.preco, quantity: 1 }],
|
|
1086
|
+
});
|
|
1087
|
+
}, []);
|
|
1088
|
+
|
|
1089
|
+
const onAddToCart = (quantidade = 1) => {
|
|
1090
|
+
trackAddToCart({
|
|
1091
|
+
content_ids: [produto.id],
|
|
1092
|
+
content_name: produto.nome,
|
|
1093
|
+
value: produto.preco * quantidade,
|
|
1094
|
+
currency: 'BRL',
|
|
1095
|
+
num_items: quantidade,
|
|
1096
|
+
_ga4Items: [{ item_id: produto.id, item_name: produto.nome, price: produto.preco, quantity: quantidade }],
|
|
1097
|
+
});
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
const onFavoritar = () =>
|
|
1101
|
+
trackAddToWishlist({
|
|
1102
|
+
content_ids: [produto.id],
|
|
1103
|
+
content_name: produto.nome,
|
|
1104
|
+
value: produto.preco,
|
|
1105
|
+
currency: 'BRL',
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
const onSelecionarVariante = (variante) =>
|
|
1109
|
+
trackCustomizeProduct({ content_name: produto.nome, variante });
|
|
1110
|
+
|
|
1111
|
+
return (/* jsx */);
|
|
1112
|
+
}
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
---
|
|
1116
|
+
|
|
1117
|
+
### 🛒 TIPO 5 — E-commerce: Página do Carrinho
|
|
1118
|
+
|
|
1119
|
+
```jsx
|
|
1120
|
+
// ✅ PageView + ViewCart no load
|
|
1121
|
+
// ✅ RemoveFromCart ao remover item
|
|
1122
|
+
// ✅ InitiateCheckout no clique em "Finalizar compra"
|
|
1123
|
+
// ❌ NUNCA: Purchase aqui — apenas na confirmação
|
|
1124
|
+
|
|
1125
|
+
import {
|
|
1126
|
+
trackPageView, trackViewCart, trackRemoveFromCart, trackInitiateCheckout
|
|
1127
|
+
} from '../tracking/tracking';
|
|
1128
|
+
|
|
1129
|
+
export function Carrinho({ itens, total }) {
|
|
1130
|
+
useEffect(() => {
|
|
1131
|
+
trackPageView();
|
|
1132
|
+
trackViewCart({
|
|
1133
|
+
value: total,
|
|
1134
|
+
currency: 'BRL',
|
|
1135
|
+
_ga4Items: itens.map(i => ({ item_id: i.id, item_name: i.nome, price: i.preco, quantity: i.qtd })),
|
|
1136
|
+
});
|
|
1137
|
+
}, []);
|
|
1138
|
+
|
|
1139
|
+
const onRemoverItem = (item) =>
|
|
1140
|
+
trackRemoveFromCart({
|
|
1141
|
+
content_name: item.nome,
|
|
1142
|
+
value: item.preco * item.qtd,
|
|
1143
|
+
currency: 'BRL',
|
|
1144
|
+
_ga4Items: [{ item_id: item.id, item_name: item.nome, price: item.preco, quantity: item.qtd }],
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
const onFinalizarCompra = () => {
|
|
1148
|
+
trackInitiateCheckout({
|
|
1149
|
+
value: total,
|
|
1150
|
+
currency: 'BRL',
|
|
1151
|
+
num_items: itens.length,
|
|
1152
|
+
content_ids: itens.map(i => i.id),
|
|
1153
|
+
_ga4Items: itens.map(i => ({ item_id: i.id, item_name: i.nome, price: i.preco, quantity: i.qtd })),
|
|
1154
|
+
});
|
|
1155
|
+
// navegar para checkout
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
return (/* jsx */);
|
|
1159
|
+
}
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
---
|
|
1163
|
+
|
|
1164
|
+
### 💳 TIPO 6 — E-commerce: Página de Checkout
|
|
1165
|
+
|
|
1166
|
+
```jsx
|
|
1167
|
+
// ✅ PageView no load
|
|
1168
|
+
// ✅ AddShippingInfo ao confirmar endereço de entrega
|
|
1169
|
+
// ✅ AddPaymentInfo ao informar dados de pagamento
|
|
1170
|
+
// ❌ NUNCA: Purchase aqui — apenas na confirmação do pedido
|
|
1171
|
+
|
|
1172
|
+
import {
|
|
1173
|
+
trackPageView, trackAddShippingInfo, trackAddPaymentInfo
|
|
1174
|
+
} from '../tracking/tracking';
|
|
1175
|
+
|
|
1176
|
+
export function Checkout({ itens, total }) {
|
|
1177
|
+
useEffect(() => { trackPageView(); }, []);
|
|
1178
|
+
|
|
1179
|
+
const onConfirmarEndereco = () =>
|
|
1180
|
+
trackAddShippingInfo({
|
|
1181
|
+
value: total,
|
|
1182
|
+
currency: 'BRL',
|
|
1183
|
+
_ga4Items: itens.map(i => ({ item_id: i.id, item_name: i.nome, price: i.preco, quantity: i.qtd })),
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
const onInformarPagamento = (metodoPagamento) =>
|
|
1187
|
+
trackAddPaymentInfo({
|
|
1188
|
+
value: total,
|
|
1189
|
+
currency: 'BRL',
|
|
1190
|
+
payment_type: metodoPagamento, // 'credit_card', 'pix', 'boleto'
|
|
1191
|
+
_ga4Items: itens.map(i => ({ item_id: i.id, item_name: i.nome, price: i.preco, quantity: i.qtd })),
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
return (/* jsx */);
|
|
1195
|
+
}
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
---
|
|
1199
|
+
|
|
1200
|
+
### ✅ TIPO 7 — E-commerce: Página de Confirmação / Obrigado (Compra)
|
|
1201
|
+
|
|
1202
|
+
```jsx
|
|
1203
|
+
// ✅ PageView + Purchase no load — UMA VEZ (usar order_id único para deduplicação)
|
|
1204
|
+
// ✅ Purchase com value, currency e order_id obrigatórios
|
|
1205
|
+
// ❌ NUNCA: Purchase em qualquer outra página além desta
|
|
1206
|
+
// ❌ NUNCA: sem order_id (causa duplicação nos relatórios)
|
|
1207
|
+
|
|
1208
|
+
import { useEffect, useRef } from 'react';
|
|
1209
|
+
import { trackPageView, trackPurchase } from '../tracking/tracking';
|
|
1210
|
+
|
|
1211
|
+
export function PaginaObrigado({ pedido }) {
|
|
1212
|
+
const fired = useRef(false); // proteção contra double-fire no StrictMode
|
|
1213
|
+
|
|
1214
|
+
useEffect(() => {
|
|
1215
|
+
if (fired.current) return;
|
|
1216
|
+
fired.current = true;
|
|
1217
|
+
|
|
1218
|
+
trackPageView();
|
|
1219
|
+
trackPurchase({
|
|
1220
|
+
order_id: pedido.id, // OBRIGATÓRIO — evita duplicatas browser + servidor
|
|
1221
|
+
value: pedido.total, // OBRIGATÓRIO
|
|
1222
|
+
currency: 'BRL', // OBRIGATÓRIO
|
|
1223
|
+
content_ids: pedido.itens.map(i => i.id),
|
|
1224
|
+
email: pedido.email, // Enhanced Matching Meta + Google Enhanced Conversions
|
|
1225
|
+
phone: pedido.telefone,
|
|
1226
|
+
firstName: pedido.nome,
|
|
1227
|
+
_ga4Items: pedido.itens.map(i => ({
|
|
1228
|
+
item_id: i.id, item_name: i.nome, price: i.preco, quantity: i.qtd
|
|
1229
|
+
})),
|
|
1230
|
+
});
|
|
1231
|
+
}, []);
|
|
1232
|
+
|
|
1233
|
+
return (/* jsx */);
|
|
1234
|
+
}
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
---
|
|
1238
|
+
|
|
1239
|
+
### 🎓 TIPO 8 — Infoproduto / SaaS: Página de Obrigado (Lead)
|
|
1240
|
+
|
|
1241
|
+
```jsx
|
|
1242
|
+
// Quando o lead é capturado em outra página e confirmado aqui
|
|
1243
|
+
// ✅ PageView + Lead no load (se não foi disparado no formulário anterior)
|
|
1244
|
+
// ✅ SignUp se for cadastro de conta
|
|
1245
|
+
|
|
1246
|
+
import { useEffect, useRef } from 'react';
|
|
1247
|
+
import { trackPageView, trackLead, trackSignUp } from '../tracking/tracking';
|
|
1248
|
+
|
|
1249
|
+
export function ObrigadoLead({ dados }) {
|
|
1250
|
+
const fired = useRef(false);
|
|
1251
|
+
|
|
1252
|
+
useEffect(() => {
|
|
1253
|
+
if (fired.current) return;
|
|
1254
|
+
fired.current = true;
|
|
1255
|
+
|
|
1256
|
+
trackPageView();
|
|
1257
|
+
// Disparar Lead AQUI apenas se não foi disparado no formulário anterior
|
|
1258
|
+
// Se o formulário já chamou trackLead(), NÃO chamar novamente aqui
|
|
1259
|
+
trackLead({
|
|
1260
|
+
email: dados.email,
|
|
1261
|
+
phone: dados.phone,
|
|
1262
|
+
firstName: dados.nome,
|
|
1263
|
+
content_name: 'Lead Capturado',
|
|
1264
|
+
value: 0,
|
|
1265
|
+
currency: 'BRL',
|
|
1266
|
+
});
|
|
1267
|
+
}, []);
|
|
1268
|
+
|
|
1269
|
+
return (/* jsx */);
|
|
1270
|
+
}
|
|
1271
|
+
```
|
|
1272
|
+
|
|
1273
|
+
---
|
|
1274
|
+
|
|
1275
|
+
### 📧 TIPO 9 — Página de Assinatura / Newsletter / SaaS Trial
|
|
1276
|
+
|
|
1277
|
+
```jsx
|
|
1278
|
+
// ✅ PageView + ViewContent no load
|
|
1279
|
+
// ✅ Subscribe no onSubmit do formulário de assinatura
|
|
1280
|
+
// ✅ StartTrial no início de período de teste
|
|
1281
|
+
// ✅ SignUp na criação de conta
|
|
1282
|
+
|
|
1283
|
+
import {
|
|
1284
|
+
trackPageView, trackViewContent, trackSubscribe, trackStartTrial, trackSignUp
|
|
1285
|
+
} from '../tracking/tracking';
|
|
1286
|
+
|
|
1287
|
+
export function PaginaAssinatura({ plano }) {
|
|
1288
|
+
useEffect(() => {
|
|
1289
|
+
trackPageView();
|
|
1290
|
+
trackViewContent({ content_name: plano.nome, value: plano.preco, currency: 'BRL' });
|
|
1291
|
+
}, []);
|
|
1292
|
+
|
|
1293
|
+
const onAssinar = async (e, dados) => {
|
|
1294
|
+
e.preventDefault();
|
|
1295
|
+
await trackSubscribe({ value: plano.preco, currency: 'BRL', email: dados.email });
|
|
1296
|
+
};
|
|
1297
|
+
|
|
1298
|
+
const onIniciarTrial = async (e, dados) => {
|
|
1299
|
+
e.preventDefault();
|
|
1300
|
+
await trackStartTrial({ value: 0, currency: 'BRL', email: dados.email });
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
const onCriarConta = (metodo = 'email') => trackSignUp(metodo);
|
|
1304
|
+
|
|
1305
|
+
return (/* jsx */);
|
|
1306
|
+
}
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
---
|
|
1310
|
+
|
|
1311
|
+
### 📰 TIPO 10 — Blog / Conteúdo / Artigo
|
|
1312
|
+
|
|
1313
|
+
```jsx
|
|
1314
|
+
// ✅ PageView no load
|
|
1315
|
+
// ✅ ViewContent no load (o artigo é o conteúdo)
|
|
1316
|
+
// ✅ ScrollTracker para medir engajamento com o conteúdo
|
|
1317
|
+
// ✅ Download se houver material para baixar
|
|
1318
|
+
// ✅ Lead se houver formulário de newsletter inline
|
|
1319
|
+
|
|
1320
|
+
import {
|
|
1321
|
+
trackPageView, trackViewContent, trackDownload, trackLead, scrollTracker
|
|
1322
|
+
} from '../tracking/tracking';
|
|
1323
|
+
|
|
1324
|
+
export function PaginaArtigo({ artigo }) {
|
|
1325
|
+
useEffect(() => {
|
|
1326
|
+
trackPageView();
|
|
1327
|
+
trackViewContent({
|
|
1328
|
+
content_ids: [artigo.slug],
|
|
1329
|
+
content_type: 'article',
|
|
1330
|
+
content_name: artigo.titulo,
|
|
1331
|
+
});
|
|
1332
|
+
}, []);
|
|
1333
|
+
|
|
1334
|
+
useEffect(() => {
|
|
1335
|
+
// Medir profundidade de leitura — útil para remarketing de leitores engajados
|
|
1336
|
+
return scrollTracker([25, 50, 75, 90], (pct) =>
|
|
1337
|
+
trackCustom('LeituraArtigo', { artigo: artigo.slug, scroll_depth: pct }));
|
|
1338
|
+
}, []);
|
|
1339
|
+
|
|
1340
|
+
const onDownload = (arquivo) =>
|
|
1341
|
+
trackDownload({ content_name: arquivo.nome, content_ids: [arquivo.id] });
|
|
1342
|
+
|
|
1343
|
+
const onNewsletterSubmit = async (e, email) => {
|
|
1344
|
+
e.preventDefault();
|
|
1345
|
+
await trackLead({ email, content_name: 'Newsletter', value: 0, currency: 'BRL' });
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
return (/* jsx */);
|
|
1349
|
+
}
|
|
1350
|
+
```
|
|
1351
|
+
|
|
1352
|
+
---
|
|
1353
|
+
|
|
1354
|
+
### 📍 TIPO 11 — Página de Localização / Franquia / Loja Física
|
|
1355
|
+
|
|
1356
|
+
```jsx
|
|
1357
|
+
// ✅ PageView + ViewContent no load
|
|
1358
|
+
// ✅ FindLocation no clique em "Ver no mapa" ou "Como chegar"
|
|
1359
|
+
// ✅ Contact no clique em WhatsApp/telefone da unidade
|
|
1360
|
+
|
|
1361
|
+
import {
|
|
1362
|
+
trackPageView, trackViewContent, trackFindLocation, trackContact
|
|
1363
|
+
} from '../tracking/tracking';
|
|
1364
|
+
|
|
1365
|
+
export function PaginaUnidade({ unidade }) {
|
|
1366
|
+
useEffect(() => {
|
|
1367
|
+
trackPageView();
|
|
1368
|
+
trackViewContent({ content_name: unidade.nome, content_type: 'local' });
|
|
1369
|
+
}, []);
|
|
1370
|
+
|
|
1371
|
+
const onVerMapa = () => trackFindLocation();
|
|
1372
|
+
const onWhatsApp = async () => { await trackContact('whatsapp'); window.open(unidade.whatsapp); };
|
|
1373
|
+
|
|
1374
|
+
return (/* jsx */);
|
|
1375
|
+
}
|
|
1376
|
+
```
|
|
1377
|
+
|
|
1378
|
+
---
|
|
1379
|
+
|
|
1380
|
+
### 🔁 SEQUÊNCIA CORRETA DO FUNIL DE E-COMMERCE (visão geral)
|
|
1381
|
+
|
|
1382
|
+
```
|
|
1383
|
+
Listagem → PageView + ViewItemList
|
|
1384
|
+
↓ clique
|
|
1385
|
+
Produto → PageView + ViewContent
|
|
1386
|
+
↓ ação
|
|
1387
|
+
Carrinho → PageView + ViewCart + AddToCart (vem do clique anterior)
|
|
1388
|
+
↓ ação
|
|
1389
|
+
Checkout → PageView + InitiateCheckout + AddShippingInfo + AddPaymentInfo
|
|
1390
|
+
↓ confirmação
|
|
1391
|
+
Obrigado → PageView + Purchase (com order_id único)
|
|
1392
|
+
```
|
|
1393
|
+
|
|
1394
|
+
### 🔁 SEQUÊNCIA CORRETA DO FUNIL DE LEAD (visão geral)
|
|
1395
|
+
|
|
1396
|
+
```
|
|
1397
|
+
Landing Page → PageView + ViewContent
|
|
1398
|
+
↓ interação
|
|
1399
|
+
scrollTracker (micro-conversão de interesse)
|
|
1400
|
+
onFocus formulário → trackCustom('FormStart')
|
|
1401
|
+
↓ submit
|
|
1402
|
+
onSubmit → trackLead() OU trackContact()
|
|
1403
|
+
↓ redirect
|
|
1404
|
+
Obrigado → PageView (+ Lead apenas se não foi disparado no formulário)
|
|
1405
|
+
```
|
|
1406
|
+
|
|
1407
|
+
---
|
|
1408
|
+
|
|
1409
|
+
## PASSO 5 — Mapeamento de eventos por plataforma
|
|
1410
|
+
|
|
1411
|
+
### 📄 Eventos de Navegação
|
|
1412
|
+
|
|
1413
|
+
| Função CDP Edge | Meta | GA4 | TikTok | Pinterest | Bing | Reddit |
|
|
1414
|
+
|---|---|---|---|---|---|---|
|
|
1415
|
+
| `trackPageView()` | PageView | page_view | PageView | pagevisit | pageLoad | PageVisit |
|
|
1416
|
+
| `trackViewContent(params)` | ViewContent | view_item | ViewContent | viewcategory | view_content | ViewContent |
|
|
1417
|
+
| `trackSearch(query)` | Search | search | Search | — | search | Search |
|
|
1418
|
+
| `trackFindLocation()` | FindLocation | find_location | FindLocation | — | find_location | — |
|
|
1419
|
+
|
|
1420
|
+
### 🛒 Eventos de E-commerce
|
|
1421
|
+
|
|
1422
|
+
| Função CDP Edge | Meta | GA4 | TikTok | Pinterest | Bing | Reddit |
|
|
1423
|
+
|---|---|---|---|---|---|---|
|
|
1424
|
+
| `trackAddToCart(params)` | AddToCart | add_to_cart | AddToCart | addtocart | add_to_cart | AddToCart |
|
|
1425
|
+
| `trackAddToWishlist(params)` | AddToWishlist | add_to_wishlist | AddToWishlist | — | add_to_wishlist | — |
|
|
1426
|
+
| `trackRemoveFromCart(params)` | — | remove_from_cart | — | — | remove_from_cart | — |
|
|
1427
|
+
| `trackViewCart(params)` | — | view_cart | — | — | view_cart | — |
|
|
1428
|
+
| `trackViewItemList(params)` | — | view_item_list | — | — | view_item_list | — |
|
|
1429
|
+
| `trackSelectItem(params)` | — | select_item | — | — | select_item | — |
|
|
1430
|
+
| `trackInitiateCheckout(params)` | InitiateCheckout | begin_checkout | InitiateCheckout | checkout | initiate_checkout | AddToCart |
|
|
1431
|
+
| `trackAddPaymentInfo(params)` | AddPaymentInfo | add_payment_info | AddPaymentInfo | — | add_payment_info | — |
|
|
1432
|
+
| `trackAddShippingInfo(params)` | — | add_shipping_info | — | — | add_shipping_info | — |
|
|
1433
|
+
| `trackPurchase(params)` | Purchase | purchase | CompletePayment *(+ ttq.identify())* | checkout | purchase | Purchase |
|
|
1434
|
+
| `trackCustomizeProduct(params)` | CustomizeProduct | customize_product | CustomizeProduct | — | — | — |
|
|
1435
|
+
|
|
1436
|
+
### 🏷️ Eventos de Promoção
|
|
1437
|
+
|
|
1438
|
+
| Função CDP Edge | Meta | GA4 | TikTok | Pinterest | Bing | Reddit |
|
|
1439
|
+
|---|---|---|---|---|---|---|
|
|
1440
|
+
| `trackViewPromotion(params)` | — | view_promotion | — | — | view_promotion | — |
|
|
1441
|
+
| `trackSelectPromotion(params)` | — | select_promotion | — | — | select_promotion | — |
|
|
1442
|
+
|
|
1443
|
+
### 🎯 Eventos de Conversão / Lead
|
|
1444
|
+
|
|
1445
|
+
| Função CDP Edge | Meta | GA4 | TikTok | Pinterest | Bing | Reddit |
|
|
1446
|
+
|---|---|---|---|---|---|---|
|
|
1447
|
+
| `trackLead(params)` | Lead | generate_lead | Lead *(+ ttq.identify())* | lead | submit_form | Lead |
|
|
1448
|
+
| `trackContact(params)` | Contact | generate_lead | Contact | lead | contact | Lead |
|
|
1449
|
+
| `trackSchedule(params)` | Schedule | generate_lead | Schedule | lead | schedule | Lead |
|
|
1450
|
+
| `trackSignUp(params)` | CompleteRegistration | sign_up | CompleteRegistration | signup | sign_up | SignUp |
|
|
1451
|
+
| `trackSubscribe(params)` | Subscribe | — | Subscribe | — | subscribe | Lead |
|
|
1452
|
+
| `trackStartTrial(params)` | StartTrial | — | StartTrial | — | start_trial | — |
|
|
1453
|
+
| `trackSubmitApplication(params)` | SubmitApplication | — | SubmitForm | — | submit_application | Lead |
|
|
1454
|
+
| `trackDonate(params)` | Donate | — | — | — | donate | Purchase |
|
|
1455
|
+
| `trackDownload(params)` | — | file_download | Download | — | download | — |
|
|
1456
|
+
|
|
1457
|
+
### ⚙️ Utilitários
|
|
1458
|
+
|
|
1459
|
+
| Função CDP Edge | Descrição |
|
|
1460
|
+
|---|---|
|
|
1461
|
+
| `trackCustom(name, params)` | Evento personalizado em todas as plataformas ativas |
|
|
1462
|
+
| `scrollTracker(thresholds, cb)` | Dispara callback em % de scroll (ex: 25%, 50%, 75%, 90%) |
|
|
1463
|
+
| `getCityFromGeolocation()` | Captura cidade do usuário via GPS + OpenStreetMap (gratuito) |
|
|
1464
|
+
| `sendServerEvent(name, params)` | Envia evento direto ao servidor (fire-and-forget, não bloqueia) |
|
|
1465
|
+
| `getUTMs()` | Retorna `{ utm_source, utm_medium, utm_campaign, utm_content, utm_term }` da URL — para rastreamento interno e CRM (independente da atribuição das plataformas) |
|
|
1466
|
+
| `getUserId()` | Retorna User ID first-party persistente (cookie 365 dias) — identifica o mesmo usuário entre sessões sem depender de cookies de terceiros |
|
|
1467
|
+
| `passCheckoutParams(options?)` | Injeta UTMs + User ID nos links de checkout externo (Hotmart, Kiwify, Eduzz, Monetizze, CartPanda). Hotmart: `xcod` + `sck` (UTMs pipe-separados). Kiwify/Eduzz: `src`. Usa MutationObserver para links carregados dinamicamente. Chamar no `useEffect` do layout ou no `<body onload>` |
|
|
1468
|
+
| `normalizePhone(tel)` | Remove caracteres não-numéricos e adiciona DDI Brasil `55`. Formato exigido pela Meta: `'5511999998888'`. Retorna `undefined` se vazio |
|
|
1469
|
+
| `normalizeCity(city)` | Normaliza cidade para campo `ct` do Meta Advanced Matching: lowercase, remove acentos e caracteres especiais. Ex: `'São Paulo'` → `'saopaulo'`. Deve ser aplicada ANTES do SHA256 no servidor (CAPI) |
|
|
1470
|
+
| `splitName(fullName)` | Divide nome completo em `{ firstName, lastName }` — alinha com campos `fn`/`ln` do Meta CAPI e Enhanced Conversions do Google |
|
|
1471
|
+
| `isValidEmail(email)` | Valida formato de email — retorna `true/false`. Usar como guarda antes de `trackLead()` |
|
|
1472
|
+
| `readField(selector)` | Lê valor de campo de formulário por `id`, `name` ou seletor CSS. Usa fallback automático entre os dois |
|
|
1473
|
+
| `fixItems(raw, fieldMap?)` | Converte items MAL FORMATADOS para o padrão GA4. Recebe objeto ou array com qualquer estrutura de campo |
|
|
1474
|
+
| `buildItems(params)` | Monta array GA4 a partir de parâmetros SOLTOS (product_id, product_name, price, etc.) |
|
|
1475
|
+
| `ga4ItemsToMeta(items)` | Converte GA4 items → `{ content_ids, contents, num_items, value }` do Meta Pixel (Universal Conversion). Cada `contents[i]` inclui `id`, `quantity`, `item_price`, `title`, `category`, `brand` — formato completo exigido pelo Meta para e-commerce |
|
|
1476
|
+
| `ga4ItemsToTikTok(items)` | Converte GA4 items → `{ contents, value }` do TikTok Pixel (Universal Conversion) |
|
|
1477
|
+
|
|
1478
|
+
### 📋 Parâmetros comuns
|
|
1479
|
+
|
|
1480
|
+
| Parâmetro | Tipo | Usado em |
|
|
1481
|
+
|---|---|---|
|
|
1482
|
+
| `value` | number | Purchase, Lead, Subscribe, Donate, etc. |
|
|
1483
|
+
| `currency` | string | Purchase, AddToCart, etc. (ex: `'BRL'`) |
|
|
1484
|
+
| `content_ids` | string[] | ViewContent, AddToCart, Purchase |
|
|
1485
|
+
| `content_name` | string | ViewContent, AddToCart, etc. |
|
|
1486
|
+
| `content_type` | string | ViewContent (`'product'`, `'home_listing'`, etc.) |
|
|
1487
|
+
| `order_id` | string | Purchase (único por pedido — evita duplicatas) |
|
|
1488
|
+
| `email` | string | Lead, Purchase (será hashed SHA256 no server-side) |
|
|
1489
|
+
| `phone` | string | Lead, Purchase (será hashed SHA256 no server-side) |
|
|
1490
|
+
| `firstName` | string | Lead, Purchase (será hashed SHA256 no server-side) |
|
|
1491
|
+
| `items` | object[] | Eventos GA4 de e-commerce (name, id, price, quantity) |
|
|
1492
|
+
| `search_term` | string | trackSearch |
|
|
1493
|
+
| `promotion_id` | string | trackViewPromotion, trackSelectPromotion |
|
|
1494
|
+
| `item_list_id` | string | trackViewItemList, trackSelectItem |
|
|
1495
|
+
|
|
1496
|
+
---
|
|
1497
|
+
|
|
1498
|
+
## CHECKLIST ANTI-ERROS
|
|
1499
|
+
|
|
1500
|
+
- [ ] `ViewContent` no **carregamento** da página — nunca no scroll
|
|
1501
|
+
- [ ] Botão de CTA/orçamento → `trackLead()` ou `trackContact()`, **nunca** AddToWishlist
|
|
1502
|
+
- [ ] `trackPageView()` em **toda** página via `useEffect(()=>{}, [])`
|
|
1503
|
+
- [ ] `send_page_view: false` no gtag (SPA) — `true` / ausente em HTML puro
|
|
1504
|
+
- [ ] `value` + `currency` obrigatórios no `trackPurchase()`
|
|
1505
|
+
- [ ] `order_id` único em Purchase (evita duplicata)
|
|
1506
|
+
- [ ] Formulários com `trackLead()` no `onSubmit` (não no onClick do botão)
|
|
1507
|
+
- [ ] Credenciais null no config → plataforma ignorada sem erros
|
|
1508
|
+
- [ ] GA4 com tag ativa além do ID (erro clássico: ID configurado como variável mas sem tag)
|
|
1509
|
+
- [ ] `trackLead()` e `trackContact()` são **async** — usar `await` ou não bloquear a UX
|
|
1510
|
+
- [ ] Geolocalização é opcional para o usuário — se negar, o evento dispara normalmente sem `city`
|
|
1511
|
+
- [ ] Nunca usar geolocalização como **condição de disparo** — sempre disparar o evento, capturar cidade como dado extra
|
|
1512
|
+
- [ ] No Modo A (Aplicação Direta): nunca sobrescrever arquivos inteiros — usar `Edit` para inserir blocos cirurgicamente
|
|
1513
|
+
- [ ] UTMs são capturados automaticamente na chegada — para rastreamento interno usar `getUTMs()` e incluir no payload enviado ao servidor
|
|
1514
|
+
- [ ] UTMs **NÃO** são enviados para as plataformas de anúncio diretamente — servem para rastreamento interno (D1, CRM) independente da janela de atribuição das plataformas
|
|
1515
|
+
- [ ] Página SPA: UTMs capturados no primeiro carregamento são preservados mesmo após navegação entre rotas (ficam em `_utms` na memória)
|
|
1516
|
+
- [ ] `gclid`, `wbraid`, `gbraid` capturados da URL → atribuição a nível de anúncio do Google Ads
|
|
1517
|
+
- [ ] `ttclid` capturado da URL → atribuição a nível de anúncio do TikTok Ads
|
|
1518
|
+
- [ ] Enhanced Conversions (Google Ads): passar `email` e `phone` no `trackLead()` e `trackPurchase()` para ativar atribuição a nível de usuário
|
|
1519
|
+
- [ ] `allow_enhanced_conversions: true` na config do `gtag('config', 'AW-...')` — obrigatório para Enhanced Conversions funcionar
|
|
1520
|
+
- [ ] User ID first-party (`getUserId()`) — incluir em payloads de servidor para identificação cross-session
|
|
1521
|
+
|
|
1522
|
+
---
|
|
1523
|
+
|
|
1524
|
+
## PASSO 6 — MODO SERVER-SIDE (quando solicitado)
|
|
1525
|
+
|
|
1526
|
+
> **Quando usar:** o usuário pede tracking server-side, CAPI, Measurement Protocol, "evitar bloqueio de adblocker", ou quer o endpoint `/api/tracking`.
|
|
1527
|
+
|
|
1528
|
+
### Por que server-side existe
|
|
1529
|
+
|
|
1530
|
+
Adblockers e o ITP do Safari bloqueiam ou limitam pixels no browser. O server-side envia o mesmo evento direto das APIs das plataformas, sem depender do navegador. As plataformas deduplicam pelo `event_id` — o mesmo ID enviado pelo browser e pelo servidor.
|
|
1531
|
+
|
|
1532
|
+
### Arquitetura com server-side
|
|
1533
|
+
|
|
1534
|
+
```
|
|
1535
|
+
Browser (tracking.js)
|
|
1536
|
+
├── dispara evento client-side (Meta Pixel, GA4, TikTok...)
|
|
1537
|
+
├── gera event_id único
|
|
1538
|
+
├── lê cookies: _fbp, _fbc, _ttp, _ga
|
|
1539
|
+
├── captura UTMs da URL (utm_source, utm_medium, utm_campaign, utm_content, utm_term)
|
|
1540
|
+
└── POST /api/tracking → { event_name, event_id, cookies, utms, user_data, page_url }
|
|
1541
|
+
|
|
1542
|
+
Servidor Node.js (/api/tracking)
|
|
1543
|
+
├── recebe payload do browser
|
|
1544
|
+
├── adiciona: IP real, User-Agent do header
|
|
1545
|
+
├── SHA256 hash: email, phone, nome, cidade, estado, CEP, país
|
|
1546
|
+
└── envia simultaneamente para:
|
|
1547
|
+
├── Meta CAPI (graph.facebook.com)
|
|
1548
|
+
├── GA4 MP (apenas purchase/refund)
|
|
1549
|
+
└── TikTok Events API
|
|
1550
|
+
```
|
|
1551
|
+
|
|
1552
|
+
---
|
|
1553
|
+
|
|
1554
|
+
### PASSO 6.1 — Atualizar `tracking.config.js` com credenciais server-side
|
|
1555
|
+
|
|
1556
|
+
```js
|
|
1557
|
+
const TRACKING_CONFIG = {
|
|
1558
|
+
// ... credenciais existentes ...
|
|
1559
|
+
|
|
1560
|
+
// ── SERVER-SIDE (backend) ──────────────────────────────
|
|
1561
|
+
server: {
|
|
1562
|
+
metaCapi: {
|
|
1563
|
+
accessToken: null, // Meta CAPI Access Token (gerado no Events Manager)
|
|
1564
|
+
testEventCode: null, // ex: 'TEST12345' — só durante testes
|
|
1565
|
+
},
|
|
1566
|
+
ga4: {
|
|
1567
|
+
apiSecret: null, // GA4 Measurement Protocol API Secret
|
|
1568
|
+
// measurement_id já usa ga4Id acima
|
|
1569
|
+
},
|
|
1570
|
+
tiktok: {
|
|
1571
|
+
accessToken: null, // TikTok Events API Access Token
|
|
1572
|
+
testEventCode: null,
|
|
1573
|
+
},
|
|
1574
|
+
},
|
|
1575
|
+
};
|
|
1576
|
+
```
|
|
1577
|
+
|
|
1578
|
+
---
|
|
1579
|
+
|
|
1580
|
+
### PASSO 6.2 — Atualizar `tracking.js` para coletar cookies e enviar ao servidor
|
|
1581
|
+
|
|
1582
|
+
Adicionar ao `tracking.js` a função `sendServerEvent` que coleta os cookies do browser e faz POST ao endpoint:
|
|
1583
|
+
|
|
1584
|
+
```js
|
|
1585
|
+
// ── Captura cookies do browser para repassar ao servidor ──
|
|
1586
|
+
// Cookies gerados pelas plataformas (identificam usuário e anúncio de origem)
|
|
1587
|
+
const getCookies = () => ({
|
|
1588
|
+
// Meta
|
|
1589
|
+
fbp: document.cookie.match(/_fbp=([^;]+)/)?.[1] || '', // ID do browser pelo Meta Pixel
|
|
1590
|
+
fbc: document.cookie.match(/_fbc=([^;]+)/)?.[1] || '', // Click ID Meta (gerado do fbclid)
|
|
1591
|
+
// TikTok
|
|
1592
|
+
ttp: document.cookie.match(/_ttp=([^;]+)/)?.[1] || '', // ID do browser pelo TikTok Pixel
|
|
1593
|
+
// Google Analytics
|
|
1594
|
+
ga: document.cookie.match(/_ga=([^;]+)/)?.[1] || '', // Raw _ga cookie
|
|
1595
|
+
// GA4 sessão — necessário para GA4 Measurement Protocol em webhooks externos (PASSO 2.23)
|
|
1596
|
+
// _ga_PROPERTY: "GS1.1.{session_id}.{session_num}.1.{last}.0.0.0"
|
|
1597
|
+
gaClient: (document.cookie.match(/_ga=([^;]+)/)?.[1] || '').replace(/^GA\d+\.\d+\./, ''), // client_id limpo
|
|
1598
|
+
gaSessionId: (document.cookie.match(/_ga_[^=]+=([^;]+)/)?.[1] || '').replace(/^GS\d+\.\d+\./, '').split('.')[0] || '',
|
|
1599
|
+
gaSessionNum: (document.cookie.match(/_ga_[^=]+=([^;]+)/)?.[1] || '').replace(/^GS\d+\.\d+\./, '').split('.')[1] || '',
|
|
1600
|
+
// Google Ads
|
|
1601
|
+
gcl_aw: document.cookie.match(/_gcl_aw=([^;]+)/)?.[1] || '', // Click ID Google Ads (do gclid)
|
|
1602
|
+
|
|
1603
|
+
// Click IDs da URL (capturados no carregamento — não ficam em cookies por padrão)
|
|
1604
|
+
ctwaClid: _ctwaClid, // Meta Click-to-WhatsApp
|
|
1605
|
+
gclid: _gclid, // Google Ads standard
|
|
1606
|
+
wbraid: _wbraid, // Google Ads (iOS / privacy preserving)
|
|
1607
|
+
gbraid: _gbraid, // Google Ads (app campaigns)
|
|
1608
|
+
ttclid: _ttclid, // TikTok Ads
|
|
1609
|
+
|
|
1610
|
+
// User ID first-party (persistente entre sessões, não bloqueado por ad-blockers)
|
|
1611
|
+
userId: _userId,
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
// ── Envio server-side (fire and forget — não bloqueia UX) ──
|
|
1615
|
+
const sendServerEvent = (eventName, eventId, payload = {}) => {
|
|
1616
|
+
if (!isBrowser) return;
|
|
1617
|
+
// Não usar await — deixar rodar em paralelo com o event client-side
|
|
1618
|
+
fetch('/api/tracking', {
|
|
1619
|
+
method: 'POST',
|
|
1620
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1621
|
+
body: JSON.stringify({
|
|
1622
|
+
event_name: eventName,
|
|
1623
|
+
event_id: eventId,
|
|
1624
|
+
page_url: window.location.href,
|
|
1625
|
+
referrer: document.referrer,
|
|
1626
|
+
cookies: getCookies(),
|
|
1627
|
+
utms: _utms, // UTMs para rastreamento interno (atribuição própria)
|
|
1628
|
+
...payload,
|
|
1629
|
+
}),
|
|
1630
|
+
}).catch(() => {}); // silencia erros de rede
|
|
1631
|
+
};
|
|
1632
|
+
|
|
1633
|
+
// ── Gerador de event_id único (compartilhado browser ↔ servidor) ──
|
|
1634
|
+
const genId = () => `${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
1635
|
+
```
|
|
1636
|
+
|
|
1637
|
+
Atualizar `trackLead`, `trackPurchase` e `trackContact` para gerar `event_id` e chamar `sendServerEvent` em paralelo:
|
|
1638
|
+
|
|
1639
|
+
```js
|
|
1640
|
+
export const trackLead = async (params = {}) => {
|
|
1641
|
+
const eventId = genId();
|
|
1642
|
+
const cidade = await getCidade();
|
|
1643
|
+
const base = { value: 0, currency: 'BRL', ...params, ...(cidade && { city: cidade }) };
|
|
1644
|
+
|
|
1645
|
+
// Client-side (existente)
|
|
1646
|
+
if (active('metaPixelId')) window.fbq('track', 'Lead', base, { eventID: eventId });
|
|
1647
|
+
// ... demais plataformas ...
|
|
1648
|
+
|
|
1649
|
+
// Server-side (paralelo)
|
|
1650
|
+
sendServerEvent('Lead', eventId, {
|
|
1651
|
+
value: base.value, currency: base.currency,
|
|
1652
|
+
content_name: base.content_name || '',
|
|
1653
|
+
city: cidade || '',
|
|
1654
|
+
// email/phone: se disponíveis no formulário, passar via params
|
|
1655
|
+
email: params.email || '',
|
|
1656
|
+
phone: params.phone || '',
|
|
1657
|
+
});
|
|
1658
|
+
};
|
|
1659
|
+
```
|
|
1660
|
+
|
|
1661
|
+
> **Regra:** `eventID` deve ser passado como 3º argumento do `fbq('track', ...)` para deduplicação Meta. TikTok usa `event_id` nas `properties`.
|
|
1662
|
+
|
|
1663
|
+
---
|
|
1664
|
+
|
|
1665
|
+
### PASSO 6.3 — Endpoint `/api/tracking` (Node.js / Next.js API Route)
|
|
1666
|
+
|
|
1667
|
+
```js
|
|
1668
|
+
// Node.js Express: src/api/tracking.js
|
|
1669
|
+
// Next.js App Router: app/api/tracking/route.js
|
|
1670
|
+
// Next.js Pages Router: pages/api/tracking.js
|
|
1671
|
+
|
|
1672
|
+
import crypto from 'crypto';
|
|
1673
|
+
import CONFIG from '../tracking/tracking.config.js'; // ajustar path
|
|
1674
|
+
|
|
1675
|
+
// SHA256 com lowercase e trim — padrão exigido pela Meta e TikTok
|
|
1676
|
+
const sha256 = (val) => val
|
|
1677
|
+
? crypto.createHash('sha256').update(String(val).toLowerCase().trim()).digest('hex')
|
|
1678
|
+
: '';
|
|
1679
|
+
|
|
1680
|
+
// IP real — checka proxies e load balancers
|
|
1681
|
+
const getRealIp = (req) =>
|
|
1682
|
+
req.headers['x-forwarded-for']?.split(',')[0].trim() ||
|
|
1683
|
+
req.headers['x-real-ip'] ||
|
|
1684
|
+
req.socket?.remoteAddress ||
|
|
1685
|
+
req.ip || '';
|
|
1686
|
+
|
|
1687
|
+
// ── HANDLER PRINCIPAL ──
|
|
1688
|
+
export async function POST(req) { // Next.js App Router
|
|
1689
|
+
// export default async function handler(req, res) { // Express / Pages Router
|
|
1690
|
+
|
|
1691
|
+
const body = await req.json(); // Next.js App Router
|
|
1692
|
+
// const body = req.body; // Express / Pages Router
|
|
1693
|
+
|
|
1694
|
+
const {
|
|
1695
|
+
event_name, event_id, page_url, referrer,
|
|
1696
|
+
cookies = {},
|
|
1697
|
+
utms = {}, // UTMs para rastreamento interno (atribuição própria)
|
|
1698
|
+
email = '', phone = '', firstName = '', lastName = '',
|
|
1699
|
+
city = '', state = '', zipCode = '', country = 'br',
|
|
1700
|
+
value = 0, currency = 'BRL', content_name = '',
|
|
1701
|
+
content_ids = [], content_type = 'product',
|
|
1702
|
+
ga_client_id = '', // extraído do cookie _ga no browser
|
|
1703
|
+
method = '',
|
|
1704
|
+
} = body;
|
|
1705
|
+
|
|
1706
|
+
// Click IDs — identificam o anúncio que originou o evento (atribuição a nível de anúncio)
|
|
1707
|
+
const ctwaClid = cookies.ctwaClid || ''; // Meta Click-to-WhatsApp
|
|
1708
|
+
const gclid = cookies.gclid || ''; // Google Ads standard click ID → Enhanced Conversions
|
|
1709
|
+
const wbraid = cookies.wbraid || ''; // Google Ads (iOS / privacy preserving)
|
|
1710
|
+
const gbraid = cookies.gbraid || ''; // Google Ads (app campaigns)
|
|
1711
|
+
const ttclid = cookies.ttclid || ''; // TikTok Ads click ID
|
|
1712
|
+
const userId = cookies.userId || ''; // User ID first-party (cross-session)
|
|
1713
|
+
|
|
1714
|
+
// UTMs extraídos — disponíveis para salvar em CRM/banco com cada evento
|
|
1715
|
+
// Exemplo: salvar no D1 → { event_name, utm_source, utm_campaign, email, value, page_url }
|
|
1716
|
+
const { utm_source = '', utm_medium = '', utm_campaign = '', utm_content = '', utm_term = '' } = utms;
|
|
1717
|
+
|
|
1718
|
+
const userAgent = req.headers['user-agent'] || '';
|
|
1719
|
+
const ip = getRealIp(req);
|
|
1720
|
+
const eventTime = Math.floor(Date.now() / 1000);
|
|
1721
|
+
|
|
1722
|
+
const results = await Promise.allSettled([
|
|
1723
|
+
sendMetaCapi({ event_name, event_id, page_url, ip, userAgent,
|
|
1724
|
+
cookies, email, phone, firstName, lastName,
|
|
1725
|
+
city, state, zipCode, country,
|
|
1726
|
+
value, currency, content_name, content_ids, content_type,
|
|
1727
|
+
ctwaClid, userId, eventTime }),
|
|
1728
|
+
sendGA4Mp({ event_name, event_id, page_url,
|
|
1729
|
+
value, currency, content_name, ga_client_id, eventTime }),
|
|
1730
|
+
sendTiktokApi({ event_name, event_id, page_url, ip, userAgent,
|
|
1731
|
+
cookies, email, phone, userId, value, currency, content_name, eventTime }),
|
|
1732
|
+
]);
|
|
1733
|
+
|
|
1734
|
+
// Next.js App Router
|
|
1735
|
+
return Response.json({ ok: true });
|
|
1736
|
+
// Express: res.json({ ok: true });
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// ══════════════════════════════════════════════════════════
|
|
1740
|
+
// META CONVERSIONS API
|
|
1741
|
+
// ══════════════════════════════════════════════════════════
|
|
1742
|
+
async function sendMetaCapi(p) {
|
|
1743
|
+
if (!CONFIG.metaPixelId || !CONFIG.server?.metaCapi?.accessToken) return;
|
|
1744
|
+
|
|
1745
|
+
// user_data — todos os campos que existirem com SHA256
|
|
1746
|
+
// REGRA META: normalizar ANTES de hashear. Ordem importa:
|
|
1747
|
+
// email → lowercase + trim → sha256
|
|
1748
|
+
// phone → apenas dígitos (DDI incluído, ex: 5511999998888) → sha256
|
|
1749
|
+
// fn / ln → lowercase + trim → sha256
|
|
1750
|
+
// ct → lowercase, sem acentos, sem espaços, sem caracteres especiais → sha256
|
|
1751
|
+
// ex: 'São Paulo' → 'saopaulo' → sha256('saopaulo')
|
|
1752
|
+
// st → código 2 letras lowercase (ex: 'sp', 'rj') → sha256
|
|
1753
|
+
// zp → apenas dígitos (sem hífen) → sha256
|
|
1754
|
+
// country → código ISO 2 letras lowercase (ex: 'br') → sha256
|
|
1755
|
+
// fbp / fbc / ctwa_clid / external_id → NÃO hashear
|
|
1756
|
+
// external_id → sha256 do User ID first-party (_cdp_uid)
|
|
1757
|
+
|
|
1758
|
+
// normaliza cidade: lowercase, remove acentos, remove tudo que não é a-z0-9
|
|
1759
|
+
const normalizeCity = (city = '') =>
|
|
1760
|
+
String(city).normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
1761
|
+
|
|
1762
|
+
const user_data = {
|
|
1763
|
+
client_ip_address: p.ip, // obrigatório quando action_source=website
|
|
1764
|
+
client_user_agent: p.userAgent, // obrigatório quando action_source=website
|
|
1765
|
+
...(p.cookies.fbp && { fbp: p.cookies.fbp }), // cookie _fbp — não hashear
|
|
1766
|
+
...(p.cookies.fbc && { fbc: p.cookies.fbc }), // cookie _fbc — não hashear
|
|
1767
|
+
// ctwa_clid: Click ID de anúncio Click-to-WhatsApp — não hashear, enviar raw
|
|
1768
|
+
// Permite que a Meta atribua o evento ao anúncio CTWA específico
|
|
1769
|
+
...(p.ctwaClid && { ctwa_clid: p.ctwaClid }),
|
|
1770
|
+
// external_id: User ID first-party — sha256 do _cdp_uid cookie
|
|
1771
|
+
// Permite deduplicação cross-dispositivo e atribuição cross-session
|
|
1772
|
+
...(p.userId && { external_id: sha256(p.userId) }),
|
|
1773
|
+
...(p.email && { em: sha256(p.email.toLowerCase().trim()) }),
|
|
1774
|
+
...(p.phone && { ph: sha256(p.phone.replace(/\D/g, '')) }),
|
|
1775
|
+
...(p.firstName && { fn: sha256(p.firstName.toLowerCase().trim()) }),
|
|
1776
|
+
...(p.lastName && { ln: sha256(p.lastName.toLowerCase().trim()) }),
|
|
1777
|
+
...(p.city && { ct: sha256(normalizeCity(p.city)) }),
|
|
1778
|
+
...(p.state && { st: sha256(p.state.toLowerCase().trim()) }),
|
|
1779
|
+
...(p.zipCode && { zp: sha256(p.zipCode.replace(/\D/g, '')) }),
|
|
1780
|
+
...(p.country && { country: sha256(p.country.toLowerCase().trim()) }),
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1783
|
+
const custom_data = {
|
|
1784
|
+
value: p.value,
|
|
1785
|
+
currency: p.currency,
|
|
1786
|
+
content_name: p.content_name,
|
|
1787
|
+
content_ids: p.content_ids,
|
|
1788
|
+
content_type: p.content_type,
|
|
1789
|
+
};
|
|
1790
|
+
|
|
1791
|
+
const serverEvent = {
|
|
1792
|
+
event_name: p.event_name,
|
|
1793
|
+
event_time: p.eventTime,
|
|
1794
|
+
event_id: p.event_id, // deduplicação com browser
|
|
1795
|
+
action_source: 'website', // obrigatório
|
|
1796
|
+
event_source_url: p.page_url, // obrigatório quando action_source=website
|
|
1797
|
+
user_data,
|
|
1798
|
+
custom_data,
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
const url = `https://graph.facebook.com/v22.0/${CONFIG.metaPixelId}/events`;
|
|
1802
|
+
const params = new URLSearchParams({ access_token: CONFIG.server.metaCapi.accessToken });
|
|
1803
|
+
if (CONFIG.server.metaCapi.testEventCode) {
|
|
1804
|
+
serverEvent.test_event_code = CONFIG.server.metaCapi.testEventCode;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
await fetch(`${url}?${params}`, {
|
|
1808
|
+
method: 'POST',
|
|
1809
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1810
|
+
body: JSON.stringify({ data: [serverEvent] }),
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
// ══════════════════════════════════════════════════════════
|
|
1815
|
+
// GA4 MEASUREMENT PROTOCOL
|
|
1816
|
+
// Nota: GA4 MP só é confiável para Purchase e eventos com transaction_id.
|
|
1817
|
+
// Para outros eventos, o GA4 coleta via gtag no browser é suficiente.
|
|
1818
|
+
// ══════════════════════════════════════════════════════════
|
|
1819
|
+
async function sendGA4Mp(p) {
|
|
1820
|
+
if (!CONFIG.ga4Id || !CONFIG.server?.ga4?.apiSecret) return;
|
|
1821
|
+
// GA4 MP só para purchase (padrão PYS e recomendação Google)
|
|
1822
|
+
if (p.event_name !== 'purchase') return;
|
|
1823
|
+
|
|
1824
|
+
// client_id vem do cookie _ga: GA1.1.XXXXXXXXXX.YYYYYYYYYY → pegar as partes
|
|
1825
|
+
const clientId = p.ga_client_id
|
|
1826
|
+
? p.ga_client_id.replace(/^GA\d+\.\d+\./, '') // remove prefixo GA1.1.
|
|
1827
|
+
: `${Date.now()}.${Math.floor(Math.random()*1e9)}`;
|
|
1828
|
+
|
|
1829
|
+
const url = `https://www.google-analytics.com/mp/collect` +
|
|
1830
|
+
`?measurement_id=${CONFIG.ga4Id}&api_secret=${CONFIG.server.ga4.apiSecret}`;
|
|
1831
|
+
|
|
1832
|
+
await fetch(url, {
|
|
1833
|
+
method: 'POST',
|
|
1834
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1835
|
+
body: JSON.stringify({
|
|
1836
|
+
client_id: clientId,
|
|
1837
|
+
events: [{
|
|
1838
|
+
name: 'purchase',
|
|
1839
|
+
params: {
|
|
1840
|
+
transaction_id: p.event_id,
|
|
1841
|
+
value: p.value,
|
|
1842
|
+
currency: p.currency,
|
|
1843
|
+
engagement_time_msec: 100,
|
|
1844
|
+
}
|
|
1845
|
+
}]
|
|
1846
|
+
}),
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// ══════════════════════════════════════════════════════════
|
|
1851
|
+
// TIKTOK EVENTS API
|
|
1852
|
+
// ══════════════════════════════════════════════════════════
|
|
1853
|
+
async function sendTiktokApi(p) {
|
|
1854
|
+
if (!CONFIG.tiktokPixelId || !CONFIG.server?.tiktok?.accessToken) return;
|
|
1855
|
+
|
|
1856
|
+
// Mapeamento de nomes Meta → TikTok
|
|
1857
|
+
const eventMap = {
|
|
1858
|
+
Lead: 'SubmitForm', Purchase: 'CompletePayment',
|
|
1859
|
+
ViewContent: 'ViewContent', Contact: 'Contact',
|
|
1860
|
+
Schedule: 'Schedule', InitiateCheckout: 'InitiateCheckout',
|
|
1861
|
+
AddToCart: 'AddToCart', CompleteRegistration: 'CompleteRegistration',
|
|
1862
|
+
PageView: 'PageView',
|
|
1863
|
+
};
|
|
1864
|
+
|
|
1865
|
+
const tiktokEvent = {
|
|
1866
|
+
event: eventMap[p.event_name] || p.event_name,
|
|
1867
|
+
event_id: p.event_id, // deduplicação com browser
|
|
1868
|
+
timestamp: new Date().toISOString(), // obrigatório ISO 8601
|
|
1869
|
+
context: {
|
|
1870
|
+
ip: p.ip,
|
|
1871
|
+
user_agent: p.userAgent,
|
|
1872
|
+
page: {
|
|
1873
|
+
url: p.page_url,
|
|
1874
|
+
referrer: p.referrer || '',
|
|
1875
|
+
},
|
|
1876
|
+
user: {
|
|
1877
|
+
// SHA256 para TikTok
|
|
1878
|
+
// phone: normalizar (apenas dígitos, sem +) → sha256
|
|
1879
|
+
// email: lowercase + trim → sha256
|
|
1880
|
+
...(p.email && { email: sha256(p.email.toLowerCase().trim()) }),
|
|
1881
|
+
...(p.phone && { phone_number: sha256(p.phone.replace(/\D/g, '')) }),
|
|
1882
|
+
// external_id: User ID first-party (_cdp_uid) → sha256 — Advanced Matching cross-session
|
|
1883
|
+
...(p.userId && { external_id: sha256(p.userId) }),
|
|
1884
|
+
// cookies — enviar raw, sem hash
|
|
1885
|
+
...(p.cookies.ttp && { ttp: p.cookies.ttp }), // _ttp cookie → 'ttp' no payload
|
|
1886
|
+
...(p.cookies.ttclid && { ttclid: p.cookies.ttclid }), // TikTok click ID
|
|
1887
|
+
},
|
|
1888
|
+
},
|
|
1889
|
+
properties: {
|
|
1890
|
+
value: p.value,
|
|
1891
|
+
currency: p.currency,
|
|
1892
|
+
content_name: p.content_name,
|
|
1893
|
+
content_type: 'product',
|
|
1894
|
+
},
|
|
1895
|
+
};
|
|
1896
|
+
|
|
1897
|
+
if (CONFIG.server.tiktok.testEventCode) {
|
|
1898
|
+
tiktokEvent.test_event_code = CONFIG.server.tiktok.testEventCode;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
await fetch('https://business-api.tiktok.com/open_api/v1.3/event/track/', {
|
|
1902
|
+
method: 'POST',
|
|
1903
|
+
headers: {
|
|
1904
|
+
'Content-Type': 'application/json',
|
|
1905
|
+
'Access-Token': CONFIG.server.tiktok.accessToken,
|
|
1906
|
+
},
|
|
1907
|
+
body: JSON.stringify({
|
|
1908
|
+
pixel_code: CONFIG.tiktokPixelId,
|
|
1909
|
+
event: tiktokEvent,
|
|
1910
|
+
}),
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
---
|
|
1915
|
+
|
|
1916
|
+
### PASSO 6.4 — Campos SHA256 obrigatórios (padrão Meta e TikTok)
|
|
1917
|
+
|
|
1918
|
+
| Campo | Antes de hashear |
|
|
1919
|
+
|---|---|
|
|
1920
|
+
| `email` | lowercase + trim |
|
|
1921
|
+
| `phone` | só dígitos (remover `+`, `-`, `(`, `)`, espaços) |
|
|
1922
|
+
| `firstName` | lowercase + trim |
|
|
1923
|
+
| `lastName` | lowercase + trim |
|
|
1924
|
+
| `city` | lowercase + sem acentos + sem espaços + sem caracteres especiais (`normalizeCity()`) — ex: `'São Paulo'` → `'saopaulo'` |
|
|
1925
|
+
| `state` | lowercase + trim (código ISO do estado, ex: `sp`) |
|
|
1926
|
+
| `zipCode` | só dígitos |
|
|
1927
|
+
| `country` | código ISO 2 letras lowercase (ex: `br`) |
|
|
1928
|
+
| `_ttp` (TikTok) | **NÃO hashear** — enviar raw |
|
|
1929
|
+
| `_fbp`, `_fbc` | **NÃO hashear** — enviar raw |
|
|
1930
|
+
| `ctwa_clid` | **NÃO hashear** — enviar raw (lido da URL `?ctwa_clid=xxx`) |
|
|
1931
|
+
|
|
1932
|
+
---
|
|
1933
|
+
|
|
1934
|
+
### PASSO 6.5 — Como testar server-side
|
|
1935
|
+
|
|
1936
|
+
**Meta:**
|
|
1937
|
+
1. Adicionar `test_event_code` nas credenciais (`CONFIG.server.metaCapi.testEventCode`)
|
|
1938
|
+
2. Acessar Meta Events Manager → Testar Eventos → filtrar pelo test code
|
|
1939
|
+
|
|
1940
|
+
**TikTok:**
|
|
1941
|
+
1. Adicionar `test_event_code` nas credenciais TikTok
|
|
1942
|
+
2. Acessar TikTok Events Manager → Testar Eventos
|
|
1943
|
+
|
|
1944
|
+
**GA4 Measurement Protocol:**
|
|
1945
|
+
Trocar URL de coleta para:
|
|
1946
|
+
`https://www.google-analytics.com/debug/mp/collect?measurement_id=...&api_secret=...`
|
|
1947
|
+
Retorna JSON com `validationMessages` indicando erros.
|
|
1948
|
+
|
|
1949
|
+
---
|
|
1950
|
+
|
|
1951
|
+
### Checklist server-side
|
|
1952
|
+
|
|
1953
|
+
- [ ] `ctwa_clid` capturado de `URLSearchParams` no carregamento da página e enviado no POST ao endpoint — **não hashear, não colocar no cookie**
|
|
1954
|
+
- [ ] `ctwa_clid` enviado em `user_data.ctwa_clid` na Meta CAPI — permite atribuição a anúncios Click-to-WhatsApp
|
|
1955
|
+
- [ ] `event_id` é o MESMO no browser e no servidor para deduplicação correta
|
|
1956
|
+
- [ ] `action_source: 'website'` + `event_source_url` + `client_ip_address` + `client_user_agent` — todos obrigatórios na Meta CAPI
|
|
1957
|
+
- [ ] Cookies `_fbp` e `_fbc` coletados no browser e repassados no POST
|
|
1958
|
+
- [ ] Cookie `_ttp` coletado e repassado para TikTok (sem hash)
|
|
1959
|
+
- [ ] `_ga` cookie extraído e `client_id` parseado corretamente para GA4 MP
|
|
1960
|
+
- [ ] SHA256 aplicado em todos os campos de user_data — Meta e TikTok
|
|
1961
|
+
- [ ] Telefone normalizado (só dígitos) antes do SHA256
|
|
1962
|
+
- [ ] Email em lowercase antes do SHA256
|
|
1963
|
+
- [ ] `sendServerEvent` no browser é fire-and-forget (sem await que bloqueie UX)
|
|
1964
|
+
- [ ] Em produção: remover `testEventCode` das configurações
|
|
1965
|
+
|
|
1966
|
+
---
|
|
1967
|
+
|
|
1968
|
+
---
|
|
1969
|
+
|
|
1970
|
+
### Arquitetura de Servidor (Cloudflare Workers)
|
|
1971
|
+
|
|
1972
|
+
O CDP Edge opera exclusivamente sobre a infraestrutura **Cloudflare Workers (Quantum Tier)**.
|
|
1973
|
+
|
|
1974
|
+
O CDP Edge recomenda o uso de **Cloudflare Workers (Quantum Tier)** como endpoint de rastreamento principal. Ele oferece latência global de ~10ms, banco de dados D1 nativo e custo zero para a maioria dos funis ($0 para 100k req/dia).
|
|
1975
|
+
|
|
1976
|
+
| Recurso | Cloudflare Workers | Benefício Quantum Tier |
|
|
1977
|
+
|---|---|---|
|
|
1978
|
+
| Custo | Grátis / $5 pago | Zero barreira de entrada |
|
|
1979
|
+
| Banco de Dados | D1 SQLite ✅ | Persistência de leads e match |
|
|
1980
|
+
| SHA256 | WebCrypto Native | Segurança Enterprise |
|
|
1981
|
+
| Resiliência | Edge Global | Escala infinita |
|
|
1982
|
+
|
|
1983
|
+
---
|
|
1984
|
+
|
|
1985
|
+
## PASSO 6.6 — Cloudflare Workers + D1 (Full Core Tracking)
|
|
1986
|
+
|
|
1987
|
+
> **Quando usar:** O padrão ouro do CDP Edge. Roda o endpoint `/api/tracking` no Cloudflare Workers e salva todos os eventos e perfis no banco D1 para atribuição precisa.
|
|
1988
|
+
|
|
1989
|
+
### Vantagens da Infraestrutura Cloudflare Native
|
|
1990
|
+
|
|
1991
|
+
| Recurso | Workers (free) |
|
|
1992
|
+
|---|---|
|
|
1993
|
+
| Custo | Grátis até 100k req/dia |
|
|
1994
|
+
| SHA256 | WebCrypto API (nativa) |
|
|
1995
|
+
| IP do usuário | Header `CF-Connecting-IP` (automático) |
|
|
1996
|
+
| Banco de dados | D1 SQLite (grátis até 5M reads/dia) |
|
|
1997
|
+
| Deploy | `wrangler deploy` (30s) |
|
|
1998
|
+
| Manutenção | Zero |
|
|
1999
|
+
| SSL | Automático |
|
|
2000
|
+
|
|
2001
|
+
> ⚠️ **Diferença crítica de código:** Workers usa **WebCrypto API** (assíncrona) para SHA256 em vez do módulo `crypto` do Node. Tudo mais é quase idêntico — `fetch()` é nativo, `Request`/`Response` são padrão Web API.
|
|
2002
|
+
|
|
2003
|
+
---
|
|
2004
|
+
|
|
2005
|
+
### 1. Estrutura do projeto Workers
|
|
2006
|
+
|
|
2007
|
+
```
|
|
2008
|
+
cdp-edge-worker/
|
|
2009
|
+
├── src/
|
|
2010
|
+
│ └── worker.js ← endpoint principal
|
|
2011
|
+
├── wrangler.toml ← configuração Cloudflare
|
|
2012
|
+
└── schema.sql ← schema do banco D1
|
|
2013
|
+
```
|
|
2014
|
+
|
|
2015
|
+
---
|
|
2016
|
+
|
|
2017
|
+
### 2. `wrangler.toml` — configuração completa
|
|
2018
|
+
|
|
2019
|
+
```toml
|
|
2020
|
+
name = "cdp-edge-tracking"
|
|
2021
|
+
main = "src/worker.js"
|
|
2022
|
+
compatibility_date = "2024-01-01"
|
|
2023
|
+
|
|
2024
|
+
# ── D1 Database ──────────────────────────────────────────────
|
|
2025
|
+
[[d1_databases]]
|
|
2026
|
+
binding = "DB"
|
|
2027
|
+
database_name = "cdp-edge"
|
|
2028
|
+
database_id = "COLE_AQUI_O_ID_DO_D1" # gerado após: wrangler d1 create cdp-edge
|
|
2029
|
+
|
|
2030
|
+
# ── Variáveis públicas (não sensíveis) ───────────────────────
|
|
2031
|
+
[vars]
|
|
2032
|
+
META_PIXEL_ID = "SEU_META_PIXEL_ID"
|
|
2033
|
+
GA4_ID = "G-XXXXXXXXXX"
|
|
2034
|
+
TIKTOK_PIXEL_ID = "SEU_TIKTOK_PIXEL_ID"
|
|
2035
|
+
|
|
2036
|
+
# Secrets (nunca colocar aqui — usar: wrangler secret put NOME):
|
|
2037
|
+
# META_ACCESS_TOKEN
|
|
2038
|
+
# GA4_API_SECRET
|
|
2039
|
+
# TIKTOK_ACCESS_TOKEN
|
|
2040
|
+
```
|
|
2041
|
+
|
|
2042
|
+
**Criar D1 e adicionar ID ao wrangler.toml:**
|
|
2043
|
+
```bash
|
|
2044
|
+
wrangler d1 create cdp-edge
|
|
2045
|
+
# Saída: database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
2046
|
+
# Copiar esse ID para o wrangler.toml
|
|
2047
|
+
```
|
|
2048
|
+
|
|
2049
|
+
wrangler secret put META_ACCESS_TOKEN
|
|
2050
|
+
wrangler secret put TIKTOK_ACCESS_TOKEN
|
|
2051
|
+
wrangler secret put GA4_API_SECRET
|
|
2052
|
+
|
|
2053
|
+
---
|
|
2054
|
+
|
|
2055
|
+
### 3. `schema.sql` — banco D1 para leads
|
|
2056
|
+
|
|
2057
|
+
```sql
|
|
2058
|
+
CREATE TABLE IF NOT EXISTS leads (
|
|
2059
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2060
|
+
event_id TEXT NOT NULL,
|
|
2061
|
+
event_name TEXT NOT NULL,
|
|
2062
|
+
email TEXT DEFAULT '',
|
|
2063
|
+
phone TEXT DEFAULT '',
|
|
2064
|
+
first_name TEXT DEFAULT '',
|
|
2065
|
+
last_name TEXT DEFAULT '',
|
|
2066
|
+
city TEXT DEFAULT '',
|
|
2067
|
+
value REAL DEFAULT 0,
|
|
2068
|
+
currency TEXT DEFAULT 'BRL',
|
|
2069
|
+
utm_source TEXT DEFAULT '',
|
|
2070
|
+
utm_medium TEXT DEFAULT '',
|
|
2071
|
+
utm_campaign TEXT DEFAULT '',
|
|
2072
|
+
utm_content TEXT DEFAULT '',
|
|
2073
|
+
utm_term TEXT DEFAULT '',
|
|
2074
|
+
page_url TEXT DEFAULT '',
|
|
2075
|
+
user_id TEXT DEFAULT '',
|
|
2076
|
+
ip TEXT DEFAULT '',
|
|
2077
|
+
created_at TEXT NOT NULL
|
|
2078
|
+
);
|
|
2079
|
+
|
|
2080
|
+
CREATE INDEX IF NOT EXISTS idx_leads_email ON leads(email);
|
|
2081
|
+
CREATE INDEX IF NOT EXISTS idx_leads_created_at ON leads(created_at);
|
|
2082
|
+
CREATE INDEX IF NOT EXISTS idx_leads_event_name ON leads(event_name);
|
|
2083
|
+
|
|
2084
|
+
-- ── Perfis de usuário (enriquecimento de webhooks) ──────────
|
|
2085
|
+
-- Salva cookies do browser quando o usuário está no site.
|
|
2086
|
+
-- Permite recuperar fbp/fbc/gclid ao receber webhooks externos (Hotmart, Kiwify etc.)
|
|
2087
|
+
CREATE TABLE IF NOT EXISTS user_profiles (
|
|
2088
|
+
email TEXT PRIMARY KEY,
|
|
2089
|
+
fbp TEXT DEFAULT '',
|
|
2090
|
+
fbc TEXT DEFAULT '',
|
|
2091
|
+
ttp TEXT DEFAULT '',
|
|
2092
|
+
gclid TEXT DEFAULT '',
|
|
2093
|
+
user_id TEXT DEFAULT '',
|
|
2094
|
+
city TEXT DEFAULT '',
|
|
2095
|
+
state TEXT DEFAULT '',
|
|
2096
|
+
country TEXT DEFAULT 'br',
|
|
2097
|
+
user_agent TEXT DEFAULT '',
|
|
2098
|
+
ga_client TEXT DEFAULT '',
|
|
2099
|
+
updated_at TEXT NOT NULL
|
|
2100
|
+
);
|
|
2101
|
+
|
|
2102
|
+
CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON user_profiles(user_id);
|
|
2103
|
+
```
|
|
2104
|
+
|
|
2105
|
+
**Criar as tabelas:**
|
|
2106
|
+
```bash
|
|
2107
|
+
wrangler d1 execute cdp-edge --file=schema.sql
|
|
2108
|
+
```
|
|
2109
|
+
|
|
2110
|
+
---
|
|
2111
|
+
|
|
2112
|
+
### 4. `src/worker.js` — endpoint completo para Cloudflare Workers
|
|
2113
|
+
|
|
2114
|
+
```js
|
|
2115
|
+
// ============================================================
|
|
2116
|
+
// Recebe eventos do browser → Meta CAPI + TikTok Events API + D1 Database
|
|
2117
|
+
// ============================================================
|
|
2118
|
+
|
|
2119
|
+
// SHA256 com WebCrypto API (nativa no Workers — não usar node:crypto)
|
|
2120
|
+
const sha256 = async (val) => {
|
|
2121
|
+
if (!val) return '';
|
|
2122
|
+
const data = new TextEncoder().encode(String(val).toLowerCase().trim());
|
|
2123
|
+
const buf = await crypto.subtle.digest('SHA-256', data);
|
|
2124
|
+
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
2125
|
+
};
|
|
2126
|
+
|
|
2127
|
+
// SHA256 SEM lowercase (para campos que já chegam normalizados)
|
|
2128
|
+
const sha256Raw = async (val) => {
|
|
2129
|
+
if (!val) return '';
|
|
2130
|
+
const data = new TextEncoder().encode(String(val));
|
|
2131
|
+
const buf = await crypto.subtle.digest('SHA-256', data);
|
|
2132
|
+
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
2133
|
+
};
|
|
2134
|
+
|
|
2135
|
+
// Normaliza cidade: 'São Paulo' → 'saopaulo'
|
|
2136
|
+
const normalizeCity = (city = '') =>
|
|
2137
|
+
String(city).normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
2138
|
+
|
|
2139
|
+
// IP real — Cloudflare injeta CF-Connecting-IP automaticamente
|
|
2140
|
+
const getRealIp = (request) =>
|
|
2141
|
+
request.headers.get('CF-Connecting-IP') ||
|
|
2142
|
+
request.headers.get('X-Forwarded-For')?.split(',')[0].trim() || '';
|
|
2143
|
+
|
|
2144
|
+
// ── HANDLER PRINCIPAL ────────────────────────────────────────
|
|
2145
|
+
export default {
|
|
2146
|
+
async fetch(request, env, ctx) {
|
|
2147
|
+
// CORS para o site poder chamar este Worker
|
|
2148
|
+
if (request.method === 'OPTIONS') {
|
|
2149
|
+
return new Response(null, {
|
|
2150
|
+
headers: {
|
|
2151
|
+
'Access-Control-Allow-Origin': '*',
|
|
2152
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
2153
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
2154
|
+
},
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
if (request.method !== 'POST') {
|
|
2159
|
+
return new Response('Method Not Allowed', { status: 405 });
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
const body = await request.json().catch(() => ({}));
|
|
2163
|
+
|
|
2164
|
+
const {
|
|
2165
|
+
event_name, event_id, page_url, referrer,
|
|
2166
|
+
cookies = {},
|
|
2167
|
+
utms = {},
|
|
2168
|
+
email = '', phone = '', firstName = '', lastName = '',
|
|
2169
|
+
city = '', state = '', zipCode = '', country = 'br',
|
|
2170
|
+
value = 0, currency = 'BRL', content_name = '',
|
|
2171
|
+
content_ids = [], content_type = 'product',
|
|
2172
|
+
method = '',
|
|
2173
|
+
} = body;
|
|
2174
|
+
|
|
2175
|
+
const ip = getRealIp(request);
|
|
2176
|
+
const userAgent = request.headers.get('User-Agent') || '';
|
|
2177
|
+
const eventTime = Math.floor(Date.now() / 1000);
|
|
2178
|
+
|
|
2179
|
+
const { utm_source = '', utm_medium = '', utm_campaign = '',
|
|
2180
|
+
utm_content = '', utm_term = '' } = utms;
|
|
2181
|
+
|
|
2182
|
+
// ── Geo: browser tem prioridade (GPS real via getCidade())
|
|
2183
|
+
// request.cf.country disponível grátis em todos os planos
|
|
2184
|
+
// request.cf.city/region só no plano Business ($200/mês) — não usar no free/paid
|
|
2185
|
+
const geoCountry = country || request.cf?.country?.toLowerCase() || 'br';
|
|
2186
|
+
// Fallback por IP somente se browser não enviou cidade (usuário negou geolocalização)
|
|
2187
|
+
let geoCity = city;
|
|
2188
|
+
let geoState = state;
|
|
2189
|
+
if (!geoCity && ip && ip !== '127.0.0.1') {
|
|
2190
|
+
try {
|
|
2191
|
+
const geoRes = await fetch(`https://ipapi.co/${ip}/json/`, { headers: { 'User-Agent': 'CDP Edge/1.0' } });
|
|
2192
|
+
const geoData = await geoRes.json();
|
|
2193
|
+
geoCity = geoData.city || '';
|
|
2194
|
+
geoState = geoData.region_code?.toLowerCase() || '';
|
|
2195
|
+
} catch { /* silencia falha no lookup */ }
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
// Disparar tudo em paralelo (não bloquear a resposta)
|
|
2199
|
+
const tasks = [
|
|
2200
|
+
sendMetaCapi({ event_name, event_id, page_url, referrer,
|
|
2201
|
+
ip, userAgent, cookies, email, phone, firstName, lastName,
|
|
2202
|
+
city: geoCity, state: geoState, zipCode, country: geoCountry,
|
|
2203
|
+
value, currency, content_name, content_ids, content_type,
|
|
2204
|
+
ctwaClid: cookies.ctwaClid || '',
|
|
2205
|
+
userId: cookies.userId || '',
|
|
2206
|
+
eventTime }, env),
|
|
2207
|
+
|
|
2208
|
+
sendTiktokApi({ event_name, event_id, page_url, referrer,
|
|
2209
|
+
ip, userAgent, cookies, email, phone,
|
|
2210
|
+
userId: cookies.userId || '',
|
|
2211
|
+
value, currency, content_name, eventTime }, env),
|
|
2212
|
+
|
|
2213
|
+
sendGA4Mp({ event_name, event_id, page_url,
|
|
2214
|
+
value, currency, content_name,
|
|
2215
|
+
ga_client_id: cookies.gaClient || '',
|
|
2216
|
+
ga_session_id: cookies.gaSessionId || '',
|
|
2217
|
+
ga_session_num:cookies.gaSessionNum || '',
|
|
2218
|
+
eventTime }, env),
|
|
2219
|
+
];
|
|
2220
|
+
|
|
2221
|
+
// D1: salvar lead (fire-and-forget — não bloqueia resposta)
|
|
2222
|
+
if (env.DB && event_name) {
|
|
2223
|
+
tasks.push(
|
|
2224
|
+
env.DB.prepare(
|
|
2225
|
+
`INSERT INTO leads
|
|
2226
|
+
(event_id, event_name, email, phone, first_name, last_name, city,
|
|
2227
|
+
value, currency, utm_source, utm_medium, utm_campaign,
|
|
2228
|
+
utm_content, utm_term, page_url, user_id, ip, created_at)
|
|
2229
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
|
|
2230
|
+
).bind(
|
|
2231
|
+
event_id, event_name, email, phone, firstName, lastName, city,
|
|
2232
|
+
value, currency, utm_source, utm_medium, utm_campaign,
|
|
2233
|
+
utm_content, utm_term, page_url,
|
|
2234
|
+
cookies.userId || '', ip, new Date().toISOString()
|
|
2235
|
+
).run().catch(() => {})
|
|
2236
|
+
);
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
// D1: salvar/enriquecer perfil do usuário (UPSERT preservando campos não-vazios)
|
|
2240
|
+
// Garante que fbp/fbc/ga_session salvos aqui podem ser recuperados em webhooks futuros (Hotmart etc.)
|
|
2241
|
+
if (env.DB && email) {
|
|
2242
|
+
tasks.push(
|
|
2243
|
+
env.DB.prepare(`
|
|
2244
|
+
INSERT INTO user_profiles
|
|
2245
|
+
(email, fbp, fbc, ttp, gclid, user_id, city, state, country, user_agent,
|
|
2246
|
+
ga_client, ga_session_id, ga_session_num, page_location, timestamp_ms, updated_at)
|
|
2247
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
2248
|
+
ON CONFLICT(email) DO UPDATE SET
|
|
2249
|
+
fbp = CASE WHEN excluded.fbp != '' THEN excluded.fbp ELSE fbp END,
|
|
2250
|
+
fbc = CASE WHEN excluded.fbc != '' THEN excluded.fbc ELSE fbc END,
|
|
2251
|
+
ttp = CASE WHEN excluded.ttp != '' THEN excluded.ttp ELSE ttp END,
|
|
2252
|
+
gclid = CASE WHEN excluded.gclid != '' THEN excluded.gclid ELSE gclid END,
|
|
2253
|
+
user_id = CASE WHEN excluded.user_id != '' THEN excluded.user_id ELSE user_id END,
|
|
2254
|
+
city = CASE WHEN excluded.city != '' THEN excluded.city ELSE city END,
|
|
2255
|
+
state = CASE WHEN excluded.state != '' THEN excluded.state ELSE state END,
|
|
2256
|
+
ga_client = CASE WHEN excluded.ga_client != '' THEN excluded.ga_client ELSE ga_client END,
|
|
2257
|
+
ga_session_id = CASE WHEN excluded.ga_session_id != '' THEN excluded.ga_session_id ELSE ga_session_id END,
|
|
2258
|
+
ga_session_num= CASE WHEN excluded.ga_session_num!= '' THEN excluded.ga_session_num ELSE ga_session_num END,
|
|
2259
|
+
page_location = excluded.page_location,
|
|
2260
|
+
timestamp_ms = excluded.timestamp_ms,
|
|
2261
|
+
updated_at = excluded.updated_at
|
|
2262
|
+
`).bind(
|
|
2263
|
+
email,
|
|
2264
|
+
cookies.fbp || '', cookies.fbc || '', cookies.ttp || '',
|
|
2265
|
+
cookies.gclid || '', cookies.userId || '',
|
|
2266
|
+
geoCity || '', geoState || '', geoCountry || 'br',
|
|
2267
|
+
userAgent,
|
|
2268
|
+
cookies.gaClient || '',
|
|
2269
|
+
cookies.gaSessionId || '',
|
|
2270
|
+
cookies.gaSessionNum || '',
|
|
2271
|
+
page_url || '',
|
|
2272
|
+
String(Date.now()),
|
|
2273
|
+
new Date().toISOString()
|
|
2274
|
+
).run().catch(() => {})
|
|
2275
|
+
);
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// ctx.waitUntil: permite que as tasks terminem após a resposta ser enviada
|
|
2279
|
+
ctx.waitUntil(Promise.allSettled(tasks));
|
|
2280
|
+
|
|
2281
|
+
return Response.json({ ok: true }, {
|
|
2282
|
+
headers: { 'Access-Control-Allow-Origin': '*' },
|
|
2283
|
+
});
|
|
2284
|
+
},
|
|
2285
|
+
};
|
|
2286
|
+
|
|
2287
|
+
// ══════════════════════════════════════════════════════════
|
|
2288
|
+
// META CONVERSIONS API
|
|
2289
|
+
// ══════════════════════════════════════════════════════════
|
|
2290
|
+
async function sendMetaCapi(p, env) {
|
|
2291
|
+
if (!env.META_PIXEL_ID || !env.META_ACCESS_TOKEN) return;
|
|
2292
|
+
|
|
2293
|
+
const user_data = {
|
|
2294
|
+
client_ip_address: p.ip,
|
|
2295
|
+
client_user_agent: p.userAgent,
|
|
2296
|
+
...(p.cookies.fbp && { fbp: p.cookies.fbp }),
|
|
2297
|
+
...(p.cookies.fbc && { fbc: p.cookies.fbc }),
|
|
2298
|
+
...(p.ctwaClid && { ctwa_clid: p.ctwaClid }),
|
|
2299
|
+
...(p.userId && { external_id: await sha256Raw(p.userId) }),
|
|
2300
|
+
...(p.email && { em: await sha256(p.email) }),
|
|
2301
|
+
...(p.phone && { ph: await sha256Raw(p.phone.replace(/\D/g, '')) }),
|
|
2302
|
+
...(p.firstName && { fn: await sha256(p.firstName) }),
|
|
2303
|
+
...(p.lastName && { ln: await sha256(p.lastName) }),
|
|
2304
|
+
...(p.city && { ct: await sha256Raw(normalizeCity(p.city)) }),
|
|
2305
|
+
...(p.state && { st: await sha256(p.state) }),
|
|
2306
|
+
...(p.zipCode && { zp: await sha256Raw(p.zipCode.replace(/\D/g, '')) }),
|
|
2307
|
+
...(p.country && { country: await sha256(p.country) }),
|
|
2308
|
+
};
|
|
2309
|
+
|
|
2310
|
+
const serverEvent = {
|
|
2311
|
+
event_name: p.event_name,
|
|
2312
|
+
event_time: p.eventTime,
|
|
2313
|
+
event_id: p.event_id,
|
|
2314
|
+
action_source: 'website',
|
|
2315
|
+
event_source_url: p.page_url,
|
|
2316
|
+
user_data,
|
|
2317
|
+
custom_data: {
|
|
2318
|
+
value: p.value,
|
|
2319
|
+
currency: p.currency,
|
|
2320
|
+
content_name: p.content_name,
|
|
2321
|
+
content_ids: p.content_ids,
|
|
2322
|
+
content_type: p.content_type,
|
|
2323
|
+
},
|
|
2324
|
+
};
|
|
2325
|
+
|
|
2326
|
+
if (env.META_TEST_EVENT_CODE) serverEvent.test_event_code = env.META_TEST_EVENT_CODE;
|
|
2327
|
+
|
|
2328
|
+
await fetch(
|
|
2329
|
+
`https://graph.facebook.com/v22.0/${env.META_PIXEL_ID}/events?access_token=${env.META_ACCESS_TOKEN}`,
|
|
2330
|
+
{
|
|
2331
|
+
method: 'POST',
|
|
2332
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2333
|
+
body: JSON.stringify({ data: [serverEvent] }),
|
|
2334
|
+
}
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
// ══════════════════════════════════════════════════════════
|
|
2339
|
+
// GA4 MEASUREMENT PROTOCOL (somente purchase)
|
|
2340
|
+
// ══════════════════════════════════════════════════════════
|
|
2341
|
+
async function sendGA4Mp(p, env) {
|
|
2342
|
+
if (!env.GA4_ID || !env.GA4_API_SECRET) return;
|
|
2343
|
+
if (p.event_name !== 'purchase') return;
|
|
2344
|
+
|
|
2345
|
+
// client_id vem do cookie _ga (sem prefixo GA1.1.)
|
|
2346
|
+
const clientId = p.ga_client_id
|
|
2347
|
+
? p.ga_client_id.replace(/^GA\d+\.\d+\./, '')
|
|
2348
|
+
: `${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
|
|
2349
|
+
|
|
2350
|
+
// session_id e session_number vêm do cookie _ga_PROPERTY (sem prefixo GS1.1.)
|
|
2351
|
+
const sessionId = p.ga_session_id || '';
|
|
2352
|
+
const sessionNum = p.ga_session_num || '1';
|
|
2353
|
+
|
|
2354
|
+
const payload = {
|
|
2355
|
+
client_id: clientId,
|
|
2356
|
+
// timestamp_micros: vincula o evento à sessão correta (obrigatório para MP)
|
|
2357
|
+
...(p.eventTime && { timestamp_micros: String(p.eventTime * 1_000_000) }),
|
|
2358
|
+
events: [{
|
|
2359
|
+
name: 'purchase',
|
|
2360
|
+
params: {
|
|
2361
|
+
transaction_id: p.event_id,
|
|
2362
|
+
value: p.value,
|
|
2363
|
+
currency: p.currency,
|
|
2364
|
+
engagement_time_msec: 100,
|
|
2365
|
+
// session_id e session_number conectam ao relatório de GA4 correto
|
|
2366
|
+
...(sessionId && { session_id: sessionId }),
|
|
2367
|
+
...(sessionNum && { session_number: Number(sessionNum) }),
|
|
2368
|
+
},
|
|
2369
|
+
}],
|
|
2370
|
+
};
|
|
2371
|
+
|
|
2372
|
+
await fetch(
|
|
2373
|
+
`https://www.google-analytics.com/mp/collect?measurement_id=${env.GA4_ID}&api_secret=${env.GA4_API_SECRET}`,
|
|
2374
|
+
{
|
|
2375
|
+
method: 'POST',
|
|
2376
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2377
|
+
body: JSON.stringify(payload),
|
|
2378
|
+
}
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// ══════════════════════════════════════════════════════════
|
|
2383
|
+
// TIKTOK EVENTS API
|
|
2384
|
+
// ══════════════════════════════════════════════════════════
|
|
2385
|
+
async function sendTiktokApi(p, env) {
|
|
2386
|
+
if (!env.TIKTOK_PIXEL_ID || !env.TIKTOK_ACCESS_TOKEN) return;
|
|
2387
|
+
|
|
2388
|
+
const eventMap = {
|
|
2389
|
+
Lead: 'SubmitForm', Purchase: 'CompletePayment',
|
|
2390
|
+
ViewContent: 'ViewContent', Contact: 'Contact',
|
|
2391
|
+
Schedule: 'Schedule', InitiateCheckout: 'InitiateCheckout',
|
|
2392
|
+
AddToCart: 'AddToCart', CompleteRegistration: 'CompleteRegistration',
|
|
2393
|
+
PageView: 'PageView',
|
|
2394
|
+
};
|
|
2395
|
+
|
|
2396
|
+
const tiktokEvent = {
|
|
2397
|
+
event: eventMap[p.event_name] || p.event_name,
|
|
2398
|
+
event_id: p.event_id,
|
|
2399
|
+
timestamp: new Date().toISOString(),
|
|
2400
|
+
context: {
|
|
2401
|
+
ip: p.ip,
|
|
2402
|
+
user_agent: p.userAgent,
|
|
2403
|
+
page: { url: p.page_url, referrer: p.referrer || '' },
|
|
2404
|
+
user: {
|
|
2405
|
+
...(p.email && { email: await sha256(p.email) }),
|
|
2406
|
+
...(p.phone && { phone_number: await sha256Raw(p.phone.replace(/\D/g, '')) }),
|
|
2407
|
+
...(p.userId && { external_id: await sha256Raw(p.userId) }),
|
|
2408
|
+
...(p.cookies.ttp && { ttp: p.cookies.ttp }),
|
|
2409
|
+
...(p.cookies.ttclid && { ttclid: p.cookies.ttclid }),
|
|
2410
|
+
},
|
|
2411
|
+
},
|
|
2412
|
+
properties: {
|
|
2413
|
+
value: p.value,
|
|
2414
|
+
currency: p.currency,
|
|
2415
|
+
content_name: p.content_name,
|
|
2416
|
+
content_type: 'product',
|
|
2417
|
+
},
|
|
2418
|
+
};
|
|
2419
|
+
|
|
2420
|
+
if (env.TIKTOK_TEST_EVENT_CODE) tiktokEvent.test_event_code = env.TIKTOK_TEST_EVENT_CODE;
|
|
2421
|
+
|
|
2422
|
+
await fetch('https://business-api.tiktok.com/open_api/v1.3/event/track/', {
|
|
2423
|
+
method: 'POST',
|
|
2424
|
+
headers: {
|
|
2425
|
+
'Content-Type': 'application/json',
|
|
2426
|
+
'Access-Token': env.TIKTOK_ACCESS_TOKEN,
|
|
2427
|
+
},
|
|
2428
|
+
body: JSON.stringify({
|
|
2429
|
+
pixel_code: env.TIKTOK_PIXEL_ID,
|
|
2430
|
+
event: tiktokEvent,
|
|
2431
|
+
}),
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
```
|
|
2435
|
+
|
|
2436
|
+
---
|
|
2437
|
+
|
|
2438
|
+
|
|
2439
|
+
---
|
|
2440
|
+
|
|
2441
|
+
### 6. Deploy do Worker
|
|
2442
|
+
|
|
2443
|
+
```bash
|
|
2444
|
+
# Instalar Wrangler (CLI Cloudflare)
|
|
2445
|
+
npm install -g wrangler
|
|
2446
|
+
|
|
2447
|
+
# Login (abre browser)
|
|
2448
|
+
wrangler login
|
|
2449
|
+
|
|
2450
|
+
# Criar banco D1
|
|
2451
|
+
wrangler d1 create cdp-edge
|
|
2452
|
+
# Copiar o database_id gerado → colar em wrangler.toml
|
|
2453
|
+
|
|
2454
|
+
# Criar tabelas
|
|
2455
|
+
wrangler d1 execute cdp-edge --file=schema.sql
|
|
2456
|
+
|
|
2457
|
+
# Adicionar secrets
|
|
2458
|
+
wrangler secret put META_ACCESS_TOKEN
|
|
2459
|
+
wrangler secret put TIKTOK_ACCESS_TOKEN
|
|
2460
|
+
wrangler secret put GA4_API_SECRET
|
|
2461
|
+
|
|
2462
|
+
# Deploy
|
|
2463
|
+
wrangler deploy
|
|
2464
|
+
|
|
2465
|
+
# URL gerada: https://cdp-edge-tracking.SEU_USUARIO.workers.dev
|
|
2466
|
+
```
|
|
2467
|
+
|
|
2468
|
+
> Após o deploy, o Worker fica disponível em `https://cdp-edge-tracking.SEU_USUARIO.workers.dev`. Para usar `/api/tracking` no site, criar um **Custom Domain** nas configurações do Worker ou usar a URL completa.
|
|
2469
|
+
|
|
2470
|
+
---
|
|
2471
|
+
|
|
2472
|
+
### 7. Adaptar `sendServerEvent` no browser para apontar ao Worker
|
|
2473
|
+
|
|
2474
|
+
Se o site e o Worker estão em domínios diferentes, mudar a URL no `tracking.js`:
|
|
2475
|
+
|
|
2476
|
+
```js
|
|
2477
|
+
// Em vez de '/api/tracking' (mesmo domínio — Next.js / Vite server)
|
|
2478
|
+
// usar a URL do Worker:
|
|
2479
|
+
const TRACKING_ENDPOINT = 'https://cdp-edge-tracking.SEU_USUARIO.workers.dev';
|
|
2480
|
+
|
|
2481
|
+
const sendServerEvent = (eventName, eventId, payload = {}) => {
|
|
2482
|
+
if (!isBrowser) return;
|
|
2483
|
+
fetch(TRACKING_ENDPOINT, {
|
|
2484
|
+
method: 'POST',
|
|
2485
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2486
|
+
body: JSON.stringify({
|
|
2487
|
+
event_name: eventName,
|
|
2488
|
+
event_id: eventId,
|
|
2489
|
+
page_url: window.location.href,
|
|
2490
|
+
referrer: document.referrer,
|
|
2491
|
+
cookies: getCookies(),
|
|
2492
|
+
utms: _utms,
|
|
2493
|
+
...payload,
|
|
2494
|
+
}),
|
|
2495
|
+
}).catch(() => {});
|
|
2496
|
+
};
|
|
2497
|
+
```
|
|
2498
|
+
|
|
2499
|
+
> Se o site rodar em **Cloudflare Pages**, usar `functions/api/tracking.js` — o código do Worker é idêntico, só muda o handler para `export async function onRequestPost({ request, env, waitUntil })`.
|
|
2500
|
+
|
|
2501
|
+
---
|
|
2502
|
+
|
|
2503
|
+
### 8. Checklist deploy Cloudflare Workers
|
|
2504
|
+
|
|
2505
|
+
```
|
|
2506
|
+
☐ wrangler.toml criado com name, main, d1_databases
|
|
2507
|
+
☐ D1 criado: wrangler d1 create cdp-edge → ID copiado para wrangler.toml
|
|
2508
|
+
☐ Schema criado: wrangler d1 execute cdp-edge --file=schema.sql
|
|
2509
|
+
☐ Secrets adicionados via wrangler secret put
|
|
2510
|
+
☐ wrangler deploy executado com sucesso
|
|
2511
|
+
☐ URL do Worker testada com curl ou Postman
|
|
2512
|
+
☐ Meta Events Manager → Testar Eventos → evento chegando
|
|
2513
|
+
☐ TikTok Events Manager → evento chegando
|
|
2514
|
+
☐ Em produção: remover META_TEST_EVENT_CODE e TIKTOK_TEST_EVENT_CODE
|
|
2515
|
+
```
|
|
2516
|
+
|
|
2517
|
+
---
|
|
2518
|
+
|
|
2519
|
+
## PASSO 7 — CTWA SEM LANDING PAGE (Cloudflare Worker + WhatsApp Business API)
|
|
2520
|
+
|
|
2521
|
+
> **Quando usar:** usuário roda anúncios Click-to-WhatsApp que vão direto para o WhatsApp, sem passar por nenhuma página do site. Não existe pixel no browser — o rastreamento é 100% server-side via webhook.
|
|
2522
|
+
|
|
2523
|
+
### Arquitetura
|
|
2524
|
+
|
|
2525
|
+
```
|
|
2526
|
+
Anúncio Meta (Instagram / Facebook)
|
|
2527
|
+
↓ clique em "Enviar Mensagem"
|
|
2528
|
+
WhatsApp abre com conversa iniciada
|
|
2529
|
+
↓ usuário manda primeira mensagem
|
|
2530
|
+
Meta Cloud API (webhook oficial)
|
|
2531
|
+
↓ POST para Cloudflare Worker
|
|
2532
|
+
Worker (edge, ~5ms)
|
|
2533
|
+
├── Meta CAPI (action_source: 'business_messaging')
|
|
2534
|
+
└── Cloudflare D1 (Identity Graph)
|
|
2535
|
+
```
|
|
2536
|
+
|
|
2537
|
+
### Diferença crítica de action_source
|
|
2538
|
+
|
|
2539
|
+
| Cenário | action_source | ctwa_clid vem de |
|
|
2540
|
+
|---|---|---|
|
|
2541
|
+
| Botão WhatsApp no site | `website` | `?ctwa_clid=` na URL (JavaScript) |
|
|
2542
|
+
| CTWA → site → WhatsApp | `website` | `?ctwa_clid=` na URL (JavaScript) |
|
|
2543
|
+
| CTWA → WhatsApp direto | `business_messaging` | `message.referral.ctwa_clid` (webhook) |
|
|
2544
|
+
|
|
2545
|
+
### PASSO 7.1 — Cloudflare Worker completo
|
|
2546
|
+
|
|
2547
|
+
```js
|
|
2548
|
+
// worker.js — Cloudflare Worker
|
|
2549
|
+
// Deploy: wrangler deploy
|
|
2550
|
+
// Variáveis de ambiente: META_PIXEL_ID, META_CAPI_TOKEN, VERIFY_TOKEN, SHEET_WEBHOOK_URL
|
|
2551
|
+
|
|
2552
|
+
const sha256 = async (val) => {
|
|
2553
|
+
const data = new TextEncoder().encode(String(val).toLowerCase().trim());
|
|
2554
|
+
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
2555
|
+
return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2,'0')).join('');
|
|
2556
|
+
};
|
|
2557
|
+
|
|
2558
|
+
export default {
|
|
2559
|
+
async fetch(request, env) {
|
|
2560
|
+
|
|
2561
|
+
// ── Verificação do webhook (GET — Meta envia uma vez ao configurar) ──
|
|
2562
|
+
if (request.method === 'GET') {
|
|
2563
|
+
const url = new URL(request.url);
|
|
2564
|
+
const mode = url.searchParams.get('hub.mode');
|
|
2565
|
+
const token = url.searchParams.get('hub.verify_token');
|
|
2566
|
+
const challenge = url.searchParams.get('hub.challenge');
|
|
2567
|
+
|
|
2568
|
+
if (mode === 'subscribe' && token === env.VERIFY_TOKEN) {
|
|
2569
|
+
return new Response(challenge, { status: 200 });
|
|
2570
|
+
}
|
|
2571
|
+
return new Response('Forbidden', { status: 403 });
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
// ── Receber mensagem do WhatsApp (POST) ──
|
|
2575
|
+
if (request.method === 'POST') {
|
|
2576
|
+
const body = await request.json();
|
|
2577
|
+
const change = body.entry?.[0]?.changes?.[0]?.value;
|
|
2578
|
+
const message = change?.messages?.[0];
|
|
2579
|
+
|
|
2580
|
+
// Responde 200 imediatamente — Meta exige resposta em < 20s
|
|
2581
|
+
if (!message || message.type === 'status') {
|
|
2582
|
+
return new Response('ok', { status: 200 });
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
// ── Extrair dados da conversa ──
|
|
2586
|
+
const phone = message.from; // ex: '5511999999999'
|
|
2587
|
+
const name = change?.contacts?.[0]?.profile?.name || '';
|
|
2588
|
+
const ctwaClid = message.referral?.ctwa_clid || ''; // só existe se veio de anúncio CTWA
|
|
2589
|
+
const adId = message.referral?.source_id || '';
|
|
2590
|
+
const adUrl = message.referral?.source_url || '';
|
|
2591
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
2592
|
+
const eventId = `wa_${phone}_${timestamp}`;
|
|
2593
|
+
|
|
2594
|
+
const lead = {
|
|
2595
|
+
phone,
|
|
2596
|
+
name,
|
|
2597
|
+
ctwa_clid: ctwaClid,
|
|
2598
|
+
ad_id: adId,
|
|
2599
|
+
ad_url: adUrl,
|
|
2600
|
+
event_id: eventId,
|
|
2601
|
+
timestamp: new Date().toISOString(),
|
|
2602
|
+
source: ctwaClid ? 'ctwa_ad' : 'organico',
|
|
2603
|
+
};
|
|
2604
|
+
|
|
2605
|
+
// Rodar em paralelo sem bloquear a resposta ao WhatsApp
|
|
2606
|
+
// (usar waitUntil para não cancelar após o return)
|
|
2607
|
+
const ctx = { waitUntil: (p) => p }; // fallback se não tiver ExecutionContext
|
|
2608
|
+
await Promise.allSettled([
|
|
2609
|
+
sendMetaCapi(lead, env, await sha256(phone.replace(/\D/g, ''))),
|
|
2610
|
+
]);
|
|
2611
|
+
|
|
2612
|
+
return new Response('ok', { status: 200 });
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
return new Response('Method Not Allowed', { status: 405 });
|
|
2616
|
+
}
|
|
2617
|
+
};
|
|
2618
|
+
|
|
2619
|
+
// ── META CAPI (action_source: business_messaging) ─────────
|
|
2620
|
+
async function sendMetaCapi(lead, env, hashedPhone) {
|
|
2621
|
+
if (!env.META_PIXEL_ID || !env.META_CAPI_TOKEN) return;
|
|
2622
|
+
|
|
2623
|
+
const user_data = {
|
|
2624
|
+
ph: hashedPhone,
|
|
2625
|
+
...(lead.ctwa_clid && { ctwa_clid: lead.ctwa_clid }), // não hashear
|
|
2626
|
+
};
|
|
2627
|
+
|
|
2628
|
+
await fetch(
|
|
2629
|
+
`https://graph.facebook.com/v22.0/${env.META_PIXEL_ID}/events?access_token=${env.META_CAPI_TOKEN}`,
|
|
2630
|
+
{
|
|
2631
|
+
method: 'POST',
|
|
2632
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2633
|
+
body: JSON.stringify({
|
|
2634
|
+
data: [{
|
|
2635
|
+
event_name: 'Contact',
|
|
2636
|
+
event_time: Math.floor(Date.now() / 1000),
|
|
2637
|
+
event_id: lead.event_id,
|
|
2638
|
+
action_source: 'business_messaging', // ← não é 'website'
|
|
2639
|
+
messaging_channel: 'whatsapp', // ← obrigatório
|
|
2640
|
+
user_data,
|
|
2641
|
+
}]
|
|
2642
|
+
}),
|
|
2643
|
+
}
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
```
|
|
2647
|
+
|
|
2648
|
+
### PASSO 7.3 — Variáveis de ambiente no Cloudflare
|
|
2649
|
+
|
|
2650
|
+
No painel: Workers & Pages → seu Worker → Settings → Variables and Secrets
|
|
2651
|
+
|
|
2652
|
+
```
|
|
2653
|
+
META_PIXEL_ID = 1234567890123456
|
|
2654
|
+
META_CAPI_TOKEN = seu_token_capi_do_events_manager
|
|
2655
|
+
VERIFY_TOKEN = qualquer_palavra_secreta (ex: cdp-edge2025)
|
|
2656
|
+
SHEET_WEBHOOK_URL = https://script.google.com/macros/s/SEU_ID/exec
|
|
2657
|
+
```
|
|
2658
|
+
|
|
2659
|
+
### PASSO 7.4 — Conectar o Worker ao WhatsApp Business API
|
|
2660
|
+
|
|
2661
|
+
**Meta Cloud API (gratuita):**
|
|
2662
|
+
1. [developers.facebook.com](https://developers.facebook.com) → App → WhatsApp → Configuração
|
|
2663
|
+
2. Webhook URL: `https://seu-worker.workers.dev`
|
|
2664
|
+
3. Verify Token: mesmo valor de `VERIFY_TOKEN`
|
|
2665
|
+
4. Assinar campo: `messages`
|
|
2666
|
+
|
|
2667
|
+
**Meta Cloud API (única opção suportada):**
|
|
2668
|
+
developers.facebook.com → App → WhatsApp → Configuração → Webhook URL: `https://seu-worker.workers.dev` → Assinar campo: `messages`
|
|
2669
|
+
|
|
2670
|
+
### Checklist CTWA sem landing page
|
|
2671
|
+
|
|
2672
|
+
- [ ] Worker responde `200 OK` imediatamente — Meta cancela webhook se demorar mais de 20s
|
|
2673
|
+
- [ ] Verificar se `message.referral` existe antes de ler `ctwa_clid` — nem toda mensagem vem de anúncio
|
|
2674
|
+
- [ ] `action_source: 'business_messaging'` + `messaging_channel: 'whatsapp'` — obrigatórios juntos
|
|
2675
|
+
- [ ] `ctwa_clid` enviado raw em `user_data.ctwa_clid` — não hashear
|
|
2676
|
+
- [ ] Telefone normalizado (só dígitos) antes do SHA256
|
|
2677
|
+
- [ ] `event_id` único por mensagem para evitar duplicatas (ex: `wa_{phone}_{timestamp}`)
|
|
2678
|
+
- [ ] Eventos chegando no Meta Events Manager
|
|
2679
|
+
- [ ] `VERIFY_TOKEN` configurado no Worker antes de cadastrar o webhook na Meta
|
|
2680
|
+
- [ ] Testar com Meta Events Manager → Testar Eventos usando `testEventCode`
|
|
2681
|
+
|
|
2682
|
+
---
|
|
2683
|
+
|
|
2684
|
+
## PASSO 8.4 — Pixel de Mensagens Meta (WhatsApp)
|
|
2685
|
+
|
|
2686
|
+
#### 1. Criar App no Meta Developers
|
|
2687
|
+
|
|
2688
|
+
1. Acessar https://developers.facebook.com
|
|
2689
|
+
2. Login (se pedir conta: conectar com telefone; se der erro: adicionar cartão como método de pagamento)
|
|
2690
|
+
3. Meus Apps → Criar App:
|
|
2691
|
+
|
|
2692
|
+
| Campo | Valor |
|
|
2693
|
+
|---|---|
|
|
2694
|
+
| Nome | qualquer nome descritivo |
|
|
2695
|
+
| Caso de uso | Outro |
|
|
2696
|
+
| Tipo | Business (Empresa) |
|
|
2697
|
+
| Portfólio | selecionar o BM que contém a conta de anúncios |
|
|
2698
|
+
|
|
2699
|
+
#### 2. Configurar API de Marketing
|
|
2700
|
+
|
|
2701
|
+
API DE MARKETING → Configurar → Ferramentas → Obter token de acesso
|
|
2702
|
+
|
|
2703
|
+
Permissões obrigatórias:
|
|
2704
|
+
- `ads_management`
|
|
2705
|
+
- `ads_read`
|
|
2706
|
+
- `read_insights`
|
|
2707
|
+
|
|
2708
|
+
Gerar token → **salvar imediatamente** (não é exibido novamente)
|
|
2709
|
+
|
|
2710
|
+
#### 3. Criar Pixel de Mensagens
|
|
2711
|
+
|
|
2712
|
+
No Gerenciador de Eventos:
|
|
2713
|
+
1. Criar pixel de mensagens
|
|
2714
|
+
2. Vincular a: uma Página do Facebook **ou** um perfil do Instagram
|
|
2715
|
+
3. Se não aparecer páginas → desvincular a página de pixels antigos antes de tentar
|
|
2716
|
+
|
|
2717
|
+
#### 4. Criar Usuário do Sistema (BM)
|
|
2718
|
+
|
|
2719
|
+
URL: https://business.facebook.com/latest/settings/system_users
|
|
2720
|
+
|
|
2721
|
+
1. Usuários → Usuários do sistema → Adicionar
|
|
2722
|
+
2. Definir nome, System user role = `admin`
|
|
2723
|
+
3. Atribuir todos os ativos
|
|
2724
|
+
4. Gerar token com expiração **nunca** → Copiar token
|
|
2725
|
+
|
|
2726
|
+
#### 5. Salvar token no Cloudflare Secrets
|
|
2727
|
+
|
|
2728
|
+
Use o Wrangler para persistir o token de forma segura no ambiente do Worker:
|
|
2729
|
+
|
|
2730
|
+
```bash
|
|
2731
|
+
wrangler secret put META_MESSAGING_TOKEN
|
|
2732
|
+
# Cole o token gerado no passo anterior
|
|
2733
|
+
```
|
|
2734
|
+
|
|
2735
|
+
---
|
|
2736
|
+
|
|
2737
|
+
### PASSO 8.6 — Arquitetura Cloudflare Native (Quantum Tier) 🛡️⚓🚀
|
|
2738
|
+
|
|
2739
|
+
```
|
|
2740
|
+
┌─────────────────────────────────────┐
|
|
2741
|
+
│ CLOUDFLARE ECOSYSTEM │
|
|
2742
|
+
│ Tudo num único Domínio Raiz │
|
|
2743
|
+
└──────────────┬──────────────────────┘
|
|
2744
|
+
│
|
|
2745
|
+
┌──────────────▼──────────────────────┐
|
|
2746
|
+
│ Cloudflare Edge │
|
|
2747
|
+
│ (Workers + D1 + R2 + Queues) │
|
|
2748
|
+
│ │
|
|
2749
|
+
│ ┌──────────┐ ┌──────────┐ ┌─────┐ │
|
|
2750
|
+
│ │ Payments │ │ Messaging│ │ CRM │ │
|
|
2751
|
+
│ │ (Ticto) │ │ (Resend) │ │ (D1)│ │
|
|
2752
|
+
│ └─────┬────┘ └─────┬────┘ └──┬──┘ │
|
|
2753
|
+
│ │ │ │ │
|
|
2754
|
+
│ ┌─────▼─────────────▼──────────▼──┐ │
|
|
2755
|
+
│ │ Identity Graph (D1) │ │
|
|
2756
|
+
│ │ (FBP / FBC / GCLID / UTMs) │ │
|
|
2757
|
+
│ └─────────────────────────────────┘ │
|
|
2758
|
+
└─────────────────────────────────────┘
|
|
2759
|
+
│
|
|
2760
|
+
┌───────────────┼──────────────────┐
|
|
2761
|
+
│ │ │
|
|
2762
|
+
┌──────────▼──┐ ┌────────▼────────┐ ┌─────▼──────────┐
|
|
2763
|
+
│ WhatsApp │ │ Meta CAPI │ │ A/B Edge │
|
|
2764
|
+
│ (Cloud API) │ │ v22.0 │ │ Routing │
|
|
2765
|
+
└─────────────┘ └─────────────────┘ └────────────────┘
|
|
2766
|
+
```
|
|
2767
|
+
|
|
2768
|
+
**Fluxo de Dados Autônomo:**
|
|
2769
|
+
1. **Inbound**: O Worker intercepta Webhooks (Ticto) ou Cliques no Site.
|
|
2770
|
+
2. **Process**: IA Preditiva (LTV) e Fingerprinting cruzam dados no D1.
|
|
2771
|
+
3. **Outbound**: Disparos simultâneos via Queues para Meta CAPI, Google Ads, TikTok e Mensageria (WhatsApp/Resend).
|
|
2772
|
+
4. **Resgate**: UTMs perdidas são resgatadas via Identity Graph persistente no domínio.
|
|
2773
|
+
|
|
2774
|
+
---
|
|
2775
|
+
|
|
2776
|
+
### PASSO 8.7 — Excelência em Infraestrutura (Quantum Tier)
|
|
2777
|
+
|
|
2778
|
+
A arquitetura CDP Edge Quantum Tier é otimizada para máxima performance com redundância global, operando exclusivamente na borda (Edge).
|
|
2779
|
+
|
|
2780
|
+
---
|
|
2781
|
+
|
|
2782
|
+
**Cloudflare DNS:**
|
|
2783
|
+
- [ ] Nameservers apontados para Cloudflare
|
|
2784
|
+
- [ ] Proxy ☁️ ativado para o domínio principal (Segurança & Performance)
|
|
2785
|
+
- [ ] Registros CNAME configurados para os subdomínios de tracking
|
|
2786
|
+
- [ ] SSL configurado como "Full (Strict)"
|
|
2787
|
+
|
|
2788
|
+
---
|
|
2789
|
+
|
|
2790
|
+
## PASSO 2.21 — Webhooks Externos: Ticto (v2.0)
|
|
2791
|
+
|
|
2792
|
+
> Use esta seção para orientar o usuário na configuração da Ticto como gateway principal de conversões offline e ROI.
|
|
2793
|
+
|
|
2794
|
+
### Configuração na Ticto
|
|
2795
|
+
1. Acesse **Tictools > Webhooks** no menu lateral esquerdo.
|
|
2796
|
+
2. Clique em **+ Nova Pasta** (opcional para organização).
|
|
2797
|
+
3. Clique em **+ Criar Webhook**.
|
|
2798
|
+
4. **Endpoint URL:** `https://seu-worker.workers.dev/api/wh/ticto`
|
|
2799
|
+
5. **Versão:** Selecione **2.0** (Recomendado).
|
|
2800
|
+
6. **Formato:** Selecione **JSON**.
|
|
2801
|
+
7. **Eventos Críticos (Mapeamento CDP Edge):**
|
|
2802
|
+
- `15. Venda Realizada` -> `Purchase`
|
|
2803
|
+
- `1. Abandono de Carrinho` -> `AddToCart` / CRM
|
|
2804
|
+
- `4. Pix Gerado` / `2. Boleto Impresso` -> `InitiateCheckout`
|
|
2805
|
+
8. **Token:** Copie o Token gerado e configure no Worker via `wrangler secret put TICTO_TOKEN`.
|
|
2806
|
+
|
|
2807
|
+
### Validação Automática
|
|
2808
|
+
A Ticto enviará um POST de teste imediatamente. O Worker **DEVE** retornar status `200 OK` para que a integração seja salva com sucesso.
|
|
2809
|
+
|
|
2810
|
+
### Payload de Referência (v2.0)
|
|
2811
|
+
```json
|
|
2812
|
+
{
|
|
2813
|
+
"event_id": 15,
|
|
2814
|
+
"event_name": "Venda Realizada",
|
|
2815
|
+
"customer": {
|
|
2816
|
+
"email": "cliente@email.com",
|
|
2817
|
+
"phone": "5511999999999"
|
|
2818
|
+
},
|
|
2819
|
+
"transaction": {
|
|
2820
|
+
"total_amount": 197.00,
|
|
2821
|
+
"currency": "BRL"
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
```
|
|
2825
|
+
|
|
2826
|
+
---
|
|
2827
|
+
|
|
2828
|
+
## PASSO 2.22 — E-mail Transacional: Resend API
|
|
2829
|
+
|
|
2830
|
+
> Use esta seção para configurar o envio de e-mails automáticos (Venda, Abandono, Bônus) diretamente do Cloudflare Worker.
|
|
2831
|
+
|
|
2832
|
+
### Configuração no Resend
|
|
2833
|
+
1. Acesse **Resend > Domains** e adicione seu domínio.
|
|
2834
|
+
2. No Cloudflare, adicione os registros **MX, TXT (SPF/DKIM)** que o Resend fornecerá.
|
|
2835
|
+
3. Gere uma **API Key** e salve via `wrangler secret put RESEND_API_KEY`.
|
|
2836
|
+
4. Envie e-mails via `POST https://api.resend.com/emails`.
|
|
2837
|
+
|
|
2838
|
+
---
|
|
2839
|
+
|
|
2840
|
+
## PASSO 2.23 — Spotify Ads: Pixel + Conversions API (v1)
|
|
2841
|
+
|
|
2842
|
+
> O Spotify Ads é a peça final do Quantum Tier. Permite rastrear conversões originadas de áudio e display no Spotify.
|
|
2843
|
+
|
|
2844
|
+
### Especificações Técnicas
|
|
2845
|
+
- **Endpoint**: `https://advertising-api.spotify.com/conversion/v1/accounts/{ACCOUNT_ID}/events`
|
|
2846
|
+
- **Autenticação**: Bearer Token (`SPOTIFY_ACCESS_TOKEN`)
|
|
2847
|
+
- **Deduplicação**: `event_id` obrigatório entre Browser e Server
|
|
2848
|
+
- **PII Hashing**: SHA-256 obrigatório para E-mail e Telefone (Advanced Matching)
|
|
2849
|
+
|
|
2850
|
+
### Eventos Principais
|
|
2851
|
+
| Evento | Conversão | Descrição |
|
|
2852
|
+
|---|---|---|
|
|
2853
|
+
| `ViewContent` | Milestone | Visualização de página de produto/serviço |
|
|
2854
|
+
| `AddToCart` | Intenção | Adicionado ao carrinho |
|
|
2855
|
+
| `Purchase` | Conversão | Compra realizada via Spotify Ads |
|
|
2856
|
+
| `Lead` | Conversão | Captação de lead via áudio/display |
|
|
2857
|
+
|
|
2858
|
+
### Implementação de Borda (Worker)
|
|
2859
|
+
Sempre use `ctx.waitUntil` para despachar o evento para o Spotify sem atrasar a resposta ao usuário.
|
|
2860
|
+
|
|
2861
|
+
---
|
|
2862
|
+
|
|
2863
|
+
### 2.3 Segurança e Hashing (WebCrypto API)
|
|
2864
|
+
O hashing de PII (e-mail, telefone, nome) deve ser feito obrigatoriamente no servidor usando a API nativa do Cloudflare Workers:
|
|
2865
|
+
```javascript
|
|
2866
|
+
async function sha256(data) {
|
|
2867
|
+
const msgBuffer = new TextEncoder().encode(data.toLowerCase().trim());
|
|
2868
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
|
|
2869
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
2870
|
+
.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
2871
|
+
}
|
|
2872
|
+
```
|
|
2873
|
+
|
|
2874
|
+
---
|
|
2875
|
+
|
|
2876
|
+
## 📋 3. MAPEAMENTO DE APIs
|
|
2877
|
+
|
|
2878
|
+
| Plataforma | Versão | Endpoint Principal |
|
|
2879
|
+
|---|---|---|
|
|
2880
|
+
| **Meta (CAPI)** | v22.0 | `https://graph.facebook.com/v22.0/{PIXEL_ID}/events` |
|
|
2881
|
+
| **TikTok (Events)** | v1.3 | `https://business-api.tiktok.com/open_api/v1.3/event/track/` |
|
|
2882
|
+
| **Google (GA4)** | MP | `https://www.google-analytics.com/mp/collect` |
|
|
2883
|
+
| **Spotify (Ads)** | v1 | `https://advertising-api.spotify.com/conversion/v1/accounts/{ACC_ID}/events` |
|
|
2884
|
+
|
|
2885
|
+
---
|
|
2886
|
+
|
|
2887
|
+
## 🔄 4. PROCESSO DE CONFIGURAÇÃO
|
|
2888
|
+
|
|
2889
|
+
1. **Setup Worker**: Criar Worker no Cloudflare e vincular banco D1.
|
|
2890
|
+
2. **Schema D1**: Executar `schema.sql` para criar as tabelas de identidade e logs.
|
|
2891
|
+
3. **Wrangler Config**: Configurar `wrangler.toml` com os bindings e secrets (tokens de API).
|
|
2892
|
+
4. **SDK Site**: Inserir `cdpTrack.js` e `tracking.config.js` no site.
|
|
2893
|
+
5. **Event Mapping**: Configurar gatilhos de clique e formulário no site.
|
|
2894
|
+
6. **Webhooks**: Configurar o endpoint de webhook na plataforma de vendas (Ticto/Hotmart).
|