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,672 @@
|
|
|
1
|
+
# Modelo: Multi-Step Checkout
|
|
2
|
+
|
|
3
|
+
> ✅ **STATUS: Quantum Tier (Server-Side Completo)**
|
|
4
|
+
>
|
|
5
|
+
> Checkout multi-etapa com rastreamento 100% Cloudflare Native (Workers + D1).
|
|
6
|
+
> Inclui enriquecimento progressivo de identidade, deduplicação e dispatch server-side para Meta CAPI v22.0, GA4 MP e TikTok API v1.3.
|
|
7
|
+
|
|
8
|
+
Checkout próprio dividido em múltiplas etapas: dados pessoais → endereço → pagamento → confirmação.
|
|
9
|
+
|
|
10
|
+
**Quando usar:** checkout integrado na página com múltiplas telas/abas (cartão de crédito, boleto, PIX), sem redirecionamento para plataforma externa.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 🏗️ ARQUITETURA TÉCNICA (Quantum Tier)
|
|
15
|
+
|
|
16
|
+
O rastreamento segue a lógica de progressão de checkout com enriquecimento de identidade:
|
|
17
|
+
1. **Site**: Captura progressivamente dados de identidade ao preencher campos.
|
|
18
|
+
2. **Worker**: Recebe cada etapa, enriquece Identity Graph e despacha para APIs.
|
|
19
|
+
3. **D1 Database**: Mantém estado do checkout vinculado ao `cdp_uid`.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Estrutura típica
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Step 1: Dados pessoais (nome, email, CPF)
|
|
27
|
+
Step 2: Dados de entrega (endereço) — opcional em infoprodutos
|
|
28
|
+
Step 3: Pagamento (cartão/PIX/boleto)
|
|
29
|
+
Step 4: Confirmação / Obrigado
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 📘 EVENTOS PRINCIPAIS
|
|
35
|
+
|
|
36
|
+
| Evento | Trigger | Meta | GA4 | TikTok | Prioridade |
|
|
37
|
+
|--------|---------|------|-----|--------|-----------|
|
|
38
|
+
| `InitiateCheckout` | Chegada no step 1 | `InitiateCheckout` | `begin_checkout` | `InitiateCheckout` | 🔴 Crítico |
|
|
39
|
+
| `AddPaymentInfo` | Preenchimento dos dados de pagamento (step 3) | `AddPaymentInfo` | `add_payment_info` | `AddPaymentInfo` | 🟡 Importante |
|
|
40
|
+
| `Purchase` | Confirmação de pagamento (step 4) | `Purchase` (CAPI) | `purchase` (MP) | `CompletePayment` | 🔴 Crítico |
|
|
41
|
+
| `CheckoutAbandonment` | Saída sem completar (visibilitychange) | `CustomEvent` | `checkout_abandon` | — | 🟢 Recomendado |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 🛠️ PASSO 1: CONFIGURAÇÃO DO SITE
|
|
46
|
+
|
|
47
|
+
### 1.1 SDK de Rastreamento (Header)
|
|
48
|
+
```html
|
|
49
|
+
<!-- Inserir no <head> -->
|
|
50
|
+
<script src="/js/cdpTrack.js" async></script>
|
|
51
|
+
<script>
|
|
52
|
+
window.cdpConfig = {
|
|
53
|
+
workerUrl: '/api/tracking', // Same-Domain (furtivo)
|
|
54
|
+
metaId: 'SEU_PIXEL_ID',
|
|
55
|
+
ga4Id: 'G-XXXXXXXX',
|
|
56
|
+
tiktokId: 'C4XXXXXXXXXXXXXXX',
|
|
57
|
+
checkout: {
|
|
58
|
+
value: 197.00, // ⚠️ substituir pelo valor real
|
|
59
|
+
currency: 'BRL',
|
|
60
|
+
productId: 'PROD_001', // ⚠️ substituir pelo ID do produto
|
|
61
|
+
productName: 'Produto Premium' // ⚠️ substituir pelo nome do produto
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
</script>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 1.2 Geração de cdp_uid (First Access)
|
|
68
|
+
```javascript
|
|
69
|
+
// Inserir no início do <body> (primeiro script)
|
|
70
|
+
<script>
|
|
71
|
+
(function() {
|
|
72
|
+
// Gera cdp_uid se não existir (1 ano de expiração)
|
|
73
|
+
const existing = document.cookie.match(/cdp_uid=([^;]+)/);
|
|
74
|
+
if (!existing) {
|
|
75
|
+
const uid = 'usr_' + Math.random().toString(36).substring(2, 10) + Date.now();
|
|
76
|
+
const expiry = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toUTCString();
|
|
77
|
+
document.cookie = `cdp_uid=${encodeURIComponent(uid)}; expires=${expiry}; path=/; SameSite=Lax`;
|
|
78
|
+
}
|
|
79
|
+
})();
|
|
80
|
+
</script>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 1.3 STEP 1: InitiateCheckout (Chegada na página)
|
|
84
|
+
```javascript
|
|
85
|
+
// Ao carregar a página de checkout
|
|
86
|
+
<script>
|
|
87
|
+
window.addEventListener('DOMContentLoaded', async () => {
|
|
88
|
+
const cdp_uid = document.cookie.match(/cdp_uid=([^;]+)/)?.[1] || '';
|
|
89
|
+
|
|
90
|
+
await cdpTrack.track('InitiateCheckout', {
|
|
91
|
+
cdp_uid,
|
|
92
|
+
step: 1,
|
|
93
|
+
value: window.cdpConfig.checkout.value,
|
|
94
|
+
currency: window.cdpConfig.checkout.currency,
|
|
95
|
+
content_name: window.cdpConfig.checkout.productName,
|
|
96
|
+
content_id: window.cdpConfig.checkout.productId
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
</script>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 1.4 STEP 2: Captura Progressiva de Identidade
|
|
103
|
+
```javascript
|
|
104
|
+
// Captura email ao preencher (enrichment para Advanced Matching)
|
|
105
|
+
document.querySelector('[name="email"], [type="email"]')?.addEventListener('blur', async function() {
|
|
106
|
+
const email = this.value.trim();
|
|
107
|
+
|
|
108
|
+
// Envia identidade atualizada para o Worker
|
|
109
|
+
await cdpTrack.track('IdentityUpdate', {
|
|
110
|
+
email
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Captura telefone ao preencher
|
|
115
|
+
document.querySelector('[name="phone"], [name="telefone"], [type="tel"]')?.addEventListener('blur', async function() {
|
|
116
|
+
const phone = this.value.trim();
|
|
117
|
+
|
|
118
|
+
await cdpTrack.track('IdentityUpdate', {
|
|
119
|
+
phone
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Captura nome ao preencher
|
|
124
|
+
document.querySelector('[name="name"], [name="nome"]')?.addEventListener('blur', async function() {
|
|
125
|
+
const full = this.value.trim();
|
|
126
|
+
const parts = full.split(/\s+/);
|
|
127
|
+
|
|
128
|
+
await cdpTrack.track('IdentityUpdate', {
|
|
129
|
+
first_name: parts[0],
|
|
130
|
+
last_name: parts.slice(1).join(' ')
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Captura CPF (opcional)
|
|
135
|
+
document.querySelector('[name="cpf"], [name="documento"]')?.addEventListener('blur', async function() {
|
|
136
|
+
const cpf = this.value.replace(/\D/g, '');
|
|
137
|
+
|
|
138
|
+
await cdpTrack.track('IdentityUpdate', {
|
|
139
|
+
cpf
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 1.5 STEP 3: AddPaymentInfo (Chegada na tela de pagamento)
|
|
145
|
+
```javascript
|
|
146
|
+
// Adaptar trigger conforme estrutura do checkout (clique em aba, next button, etc.)
|
|
147
|
+
document.querySelector('#btn-pagamento, [data-step="pagamento"]')?.addEventListener('click', async function() {
|
|
148
|
+
const cdp_uid = document.cookie.match(/cdp_uid=([^;]+)/)?.[1] || '';
|
|
149
|
+
|
|
150
|
+
// Capturar método de pagamento selecionado
|
|
151
|
+
const paymentMethod = document.querySelector('[name="payment-method"]:checked')?.value ||
|
|
152
|
+
document.querySelector('.payment-tab.active')?.dataset?.method ||
|
|
153
|
+
'unknown';
|
|
154
|
+
|
|
155
|
+
await cdpTrack.track('AddPaymentInfo', {
|
|
156
|
+
cdp_uid,
|
|
157
|
+
step: 3,
|
|
158
|
+
payment_method: paymentMethod
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 1.6 STEP 4: Purchase (Confirmação de pagamento)
|
|
164
|
+
```javascript
|
|
165
|
+
// Disparar na página de obrigado OU ao receber callback de sucesso da API de pagamento
|
|
166
|
+
<script>
|
|
167
|
+
async function trackPurchaseConfirmed(transactionId) {
|
|
168
|
+
const cdp_uid = document.cookie.match(/cdp_uid=([^;]+)/)?.[1] || '';
|
|
169
|
+
|
|
170
|
+
await cdpTrack.track('Purchase', {
|
|
171
|
+
cdp_uid,
|
|
172
|
+
transaction_id: transactionId,
|
|
173
|
+
step: 4,
|
|
174
|
+
value: window.cdpConfig.checkout.value,
|
|
175
|
+
currency: window.cdpConfig.checkout.currency,
|
|
176
|
+
content_id: window.cdpConfig.checkout.productId,
|
|
177
|
+
content_name: window.cdpConfig.checkout.productName
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Exemplo: callback do gateway de pagamento
|
|
182
|
+
// paymentGateway.onSuccess((transactionId) => {
|
|
183
|
+
// trackPurchaseConfirmed(transactionId);
|
|
184
|
+
// });
|
|
185
|
+
</script>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 1.7 Checkout Abandonment (Opcional)
|
|
189
|
+
```javascript
|
|
190
|
+
// Detecta quando usuário sai sem completar o checkout
|
|
191
|
+
<script>
|
|
192
|
+
let checkoutState = { step: 0 };
|
|
193
|
+
|
|
194
|
+
// Atualiza step conforme usuário avança
|
|
195
|
+
document.querySelectorAll('[data-step]').forEach(el => {
|
|
196
|
+
el.addEventListener('click', () => {
|
|
197
|
+
checkoutState.step = parseInt(el.dataset.step);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
document.addEventListener('visibilitychange', async () => {
|
|
202
|
+
if (document.visibilityState === 'hidden' && checkoutState.step > 0 && checkoutState.step < 4) {
|
|
203
|
+
// Usuário saiu sem completar
|
|
204
|
+
const cdp_uid = document.cookie.match(/cdp_uid=([^;]+)/)?.[1] || '';
|
|
205
|
+
|
|
206
|
+
// Usa navigator.sendBeacon para garantir envio mesmo se browser encerrar
|
|
207
|
+
navigator.sendBeacon('/api/tracking', JSON.stringify({
|
|
208
|
+
event_name: 'CheckoutAbandonment',
|
|
209
|
+
cdp_uid,
|
|
210
|
+
step: checkoutState.step,
|
|
211
|
+
value: window.cdpConfig.checkout.value
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
</script>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## ⚡ PASSO 2: SERVIDOR (CLOUDFLARE WORKER)
|
|
221
|
+
|
|
222
|
+
### 2.1 Handler Principal
|
|
223
|
+
```javascript
|
|
224
|
+
// worker.js
|
|
225
|
+
|
|
226
|
+
export default {
|
|
227
|
+
async fetch(request, env, ctx) {
|
|
228
|
+
const url = new URL(request.url);
|
|
229
|
+
const cors = {
|
|
230
|
+
'Access-Control-Allow-Origin': '*',
|
|
231
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
232
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
if (request.method === 'OPTIONS') {
|
|
236
|
+
return new Response(null, { headers: cors });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (url.pathname === '/api/tracking') {
|
|
240
|
+
return handleTracking(request, env, ctx);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return new Response('Not Found', { status: 404 });
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
async function handleTracking(request, env, ctx) {
|
|
248
|
+
const body = await request.json();
|
|
249
|
+
const cf = request.cf || {};
|
|
250
|
+
const ip = cf.colo === 'XX' ? '8.8.8.8' : request.headers.get('CF-Connecting-IP');
|
|
251
|
+
|
|
252
|
+
// Captura cookies do request
|
|
253
|
+
const fbp = request.headers.get('Cookie')?.match(/_fbp=([^;]+)/)?.[1];
|
|
254
|
+
const fbc = request.headers.get('Cookie')?.match(/_fbc=([^;]+)/)?.[1];
|
|
255
|
+
const ttp = request.headers.get('Cookie')?.match(/_ttp=([^;]+)/)?.[1];
|
|
256
|
+
|
|
257
|
+
const sessionData = {
|
|
258
|
+
cdp_uid: body.cdp_uid,
|
|
259
|
+
fbp,
|
|
260
|
+
fbc,
|
|
261
|
+
ttp,
|
|
262
|
+
ip,
|
|
263
|
+
user_agent: request.headers.get('User-Agent'),
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Atualiza Identity Graph no D1
|
|
267
|
+
await updateIdentity(env.DB, sessionData, body);
|
|
268
|
+
|
|
269
|
+
// Dispatch não-bloqueante para APIs
|
|
270
|
+
ctx.waitUntil(dispatchEvents(body, sessionData, env));
|
|
271
|
+
|
|
272
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
273
|
+
headers: { ...cors, 'Content-Type': 'application/json' }
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### 2.2 Atualização de Identity Graph (Enriquecimento Progressivo)
|
|
279
|
+
```javascript
|
|
280
|
+
async function updateIdentity(DB, sessionData, eventData) {
|
|
281
|
+
const { event_name } = eventData;
|
|
282
|
+
|
|
283
|
+
if (event_name === 'IdentityUpdate') {
|
|
284
|
+
// Enrichment progressivo: atualiza campos conforme usuário preenche
|
|
285
|
+
const updates = {};
|
|
286
|
+
if (eventData.email) updates.email_hash = await sha256(eventData.email);
|
|
287
|
+
if (eventData.phone) updates.phone_hash = await sha256('55' + eventData.phone.replace(/\D/g, ''));
|
|
288
|
+
if (eventData.first_name) updates.first_name_hash = await sha256(eventData.first_name);
|
|
289
|
+
if (eventData.last_name) updates.last_name_hash = await sha256(eventData.last_name);
|
|
290
|
+
if (eventData.cpf) updates.cpf_hash = await sha256(eventData.cpf);
|
|
291
|
+
|
|
292
|
+
await DB.prepare(`
|
|
293
|
+
INSERT INTO identity_graph (cdp_uid, fbp, fbc, ttp, ip, user_agent,
|
|
294
|
+
email_hash, phone_hash, first_name_hash, last_name_hash, cpf_hash, last_seen)
|
|
295
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
296
|
+
ON CONFLICT(cdp_uid) DO UPDATE SET
|
|
297
|
+
fbp = excluded.fbp,
|
|
298
|
+
fbc = excluded.fbc,
|
|
299
|
+
ttp = excluded.ttp,
|
|
300
|
+
ip = excluded.ip,
|
|
301
|
+
email_hash = COALESCE(excluded.email_hash, updates.email_hash),
|
|
302
|
+
phone_hash = COALESCE(excluded.phone_hash, updates.phone_hash),
|
|
303
|
+
first_name_hash = COALESCE(excluded.first_name_hash, updates.first_name_hash),
|
|
304
|
+
last_name_hash = COALESCE(excluded.last_name_hash, updates.last_name_hash),
|
|
305
|
+
cpf_hash = COALESCE(excluded.cpf_hash, updates.cpf_hash),
|
|
306
|
+
last_seen = datetime('now')
|
|
307
|
+
`).bind(
|
|
308
|
+
sessionData.cdp_uid, sessionData.fbp, sessionData.fbc, sessionData.ttp, sessionData.ip, sessionData.user_agent,
|
|
309
|
+
updates.email_hash || null, updates.phone_hash || null,
|
|
310
|
+
updates.first_name_hash || null, updates.last_name_hash || null, updates.cpf_hash || null
|
|
311
|
+
).run();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### 2.3 Dispatch de Eventos
|
|
317
|
+
```javascript
|
|
318
|
+
async function dispatchEvents(eventBody, sessionData, env) {
|
|
319
|
+
const { event_name } = eventBody;
|
|
320
|
+
|
|
321
|
+
if (event_name === 'InitiateCheckout') {
|
|
322
|
+
await Promise.all([
|
|
323
|
+
dispatchMeta('InitiateCheckout', sessionData, env, {
|
|
324
|
+
content_name: eventBody.content_name,
|
|
325
|
+
content_ids: [eventBody.content_id],
|
|
326
|
+
num_items: 1
|
|
327
|
+
}),
|
|
328
|
+
dispatchGA4('begin_checkout', sessionData, env, {
|
|
329
|
+
items: [{
|
|
330
|
+
item_id: eventBody.content_id,
|
|
331
|
+
item_name: eventBody.content_name,
|
|
332
|
+
price: eventBody.value,
|
|
333
|
+
quantity: 1
|
|
334
|
+
}]
|
|
335
|
+
}),
|
|
336
|
+
dispatchTikTok('InitiateCheckout', sessionData, env, {
|
|
337
|
+
content_id: eventBody.content_id
|
|
338
|
+
}),
|
|
339
|
+
]);
|
|
340
|
+
} else if (event_name === 'AddPaymentInfo') {
|
|
341
|
+
await Promise.all([
|
|
342
|
+
dispatchMeta('AddPaymentInfo', sessionData, env, {
|
|
343
|
+
value: eventBody.value || window.cdpConfig?.checkout?.value,
|
|
344
|
+
currency: eventBody.currency || 'BRL',
|
|
345
|
+
content_ids: [eventBody.content_id || window.cdpConfig?.checkout?.productId]
|
|
346
|
+
}),
|
|
347
|
+
dispatchGA4('add_payment_info', sessionData, env, {
|
|
348
|
+
payment_type: eventBody.payment_method
|
|
349
|
+
}),
|
|
350
|
+
dispatchTikTok('AddPaymentInfo', sessionData, env, {}),
|
|
351
|
+
]);
|
|
352
|
+
} else if (event_name === 'Purchase') {
|
|
353
|
+
await Promise.all([
|
|
354
|
+
dispatchMeta('Purchase', sessionData, env, {
|
|
355
|
+
content_ids: [eventBody.content_id],
|
|
356
|
+
num_items: 1
|
|
357
|
+
}),
|
|
358
|
+
dispatchGA4('purchase', sessionData, env, {
|
|
359
|
+
transaction_id: eventBody.transaction_id,
|
|
360
|
+
items: [{
|
|
361
|
+
item_id: eventBody.content_id,
|
|
362
|
+
item_name: eventBody.content_name,
|
|
363
|
+
price: eventBody.value,
|
|
364
|
+
quantity: 1
|
|
365
|
+
}]
|
|
366
|
+
}),
|
|
367
|
+
dispatchTikTok('CompletePayment', sessionData, env, {}),
|
|
368
|
+
]);
|
|
369
|
+
} else if (event_name === 'CheckoutAbandonment') {
|
|
370
|
+
// Apenas log no D1, não dispara para APIs
|
|
371
|
+
await logAbandonment(env.DB, eventBody, sessionData);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### 2.4 Meta CAPI v22.0
|
|
377
|
+
```javascript
|
|
378
|
+
async function dispatchMeta(eventName, sessionData, env, customData = {}) {
|
|
379
|
+
if (!env.META_ACCESS_TOKEN || !env.META_PIXEL_ID) return;
|
|
380
|
+
|
|
381
|
+
const identity = await getIdentity(env.DB, sessionData.cdp_uid);
|
|
382
|
+
|
|
383
|
+
const payload = {
|
|
384
|
+
data: [{
|
|
385
|
+
event_name: eventName,
|
|
386
|
+
event_time: Math.floor(Date.now() / 1000),
|
|
387
|
+
event_id: crypto.randomUUID(),
|
|
388
|
+
event_source_url: sessionData.page_url || '',
|
|
389
|
+
action_source: 'website',
|
|
390
|
+
user_data: {
|
|
391
|
+
em: identity.email_hash ? [identity.email_hash] : undefined,
|
|
392
|
+
ph: identity.phone_hash ? [identity.phone_hash] : undefined,
|
|
393
|
+
fn: identity.first_name_hash ? [identity.first_name_hash] : undefined,
|
|
394
|
+
ln: identity.last_name_hash ? [identity.last_name_hash] : undefined,
|
|
395
|
+
fbp: sessionData.fbp,
|
|
396
|
+
fbc: sessionData.fbc,
|
|
397
|
+
client_ip_address: sessionData.ip,
|
|
398
|
+
client_user_agent: sessionData.user_agent,
|
|
399
|
+
external_id: await sha256(sessionData.cdp_uid),
|
|
400
|
+
},
|
|
401
|
+
custom_data: {
|
|
402
|
+
value: customData.value || 0,
|
|
403
|
+
currency: customData.currency || 'BRL',
|
|
404
|
+
...customData
|
|
405
|
+
}
|
|
406
|
+
}]
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const res = await fetch(
|
|
410
|
+
`https://graph.facebook.com/v22.0/${env.META_PIXEL_ID}/events?access_token=${env.META_ACCESS_TOKEN}`,
|
|
411
|
+
{
|
|
412
|
+
method: 'POST',
|
|
413
|
+
headers: { 'Content-Type': 'application/json' },
|
|
414
|
+
body: JSON.stringify(payload)
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
if (!res.ok) {
|
|
419
|
+
console.error('Meta CAPI Error:', await res.text());
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### 2.5 GA4 Measurement Protocol
|
|
425
|
+
```javascript
|
|
426
|
+
async function dispatchGA4(eventName, sessionData, env, eventData = {}) {
|
|
427
|
+
if (!env.GA4_ID || !env.GA4_API_SECRET) return;
|
|
428
|
+
|
|
429
|
+
const payload = {
|
|
430
|
+
client_id: sessionData.cdp_uid,
|
|
431
|
+
user_id: sessionData.cdp_uid,
|
|
432
|
+
events: [{
|
|
433
|
+
name: eventName,
|
|
434
|
+
params: {
|
|
435
|
+
page_location: sessionData.page_url || '',
|
|
436
|
+
...eventData
|
|
437
|
+
}
|
|
438
|
+
}]
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const res = await fetch(
|
|
442
|
+
`https://www.google-analytics.com/mp/collect?measurement_id=${env.GA4_ID}&api_secret=${env.GA4_API_SECRET}`,
|
|
443
|
+
{
|
|
444
|
+
method: 'POST',
|
|
445
|
+
headers: { 'Content-Type': 'application/json' },
|
|
446
|
+
body: JSON.stringify(payload)
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
if (!res.ok) {
|
|
451
|
+
console.error('GA4 Error:', await res.text());
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### 2.6 TikTok Events API v1.3
|
|
457
|
+
```javascript
|
|
458
|
+
async function dispatchTikTok(eventName, sessionData, env, eventData = {}) {
|
|
459
|
+
if (!env.TIKTOK_PIXEL_ID || !env.TIKTOK_ACCESS_TOKEN) return;
|
|
460
|
+
|
|
461
|
+
const payload = {
|
|
462
|
+
pixel_code: env.TIKTOK_PIXEL_ID,
|
|
463
|
+
event: eventName,
|
|
464
|
+
event_id: crypto.randomUUID(),
|
|
465
|
+
timestamp: new Date().toISOString(),
|
|
466
|
+
context: {
|
|
467
|
+
ad: {
|
|
468
|
+
callback: sessionData.ttp
|
|
469
|
+
},
|
|
470
|
+
page: {
|
|
471
|
+
url: sessionData.page_url || ''
|
|
472
|
+
},
|
|
473
|
+
user: {
|
|
474
|
+
ip_address: sessionData.ip,
|
|
475
|
+
user_agent: sessionData.user_agent,
|
|
476
|
+
external_id: await sha256(sessionData.cdp_uid)
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
properties: {
|
|
480
|
+
value: eventData.value || 0,
|
|
481
|
+
currency: eventData.currency || 'BRL',
|
|
482
|
+
...eventData
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const res = await fetch(
|
|
487
|
+
'https://business-api.tiktok.com/open_api/v1.3/event/track/',
|
|
488
|
+
{
|
|
489
|
+
method: 'POST',
|
|
490
|
+
headers: {
|
|
491
|
+
'Content-Type': 'application/json',
|
|
492
|
+
'Access-Token': env.TIKTOK_ACCESS_TOKEN
|
|
493
|
+
},
|
|
494
|
+
body: JSON.stringify(payload)
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
if (!res.ok) {
|
|
499
|
+
console.error('TikTok Error:', await res.text());
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### 2.7 Helpers
|
|
505
|
+
```javascript
|
|
506
|
+
async function sha256(value) {
|
|
507
|
+
if (!value) return null;
|
|
508
|
+
const encoder = new TextEncoder();
|
|
509
|
+
const data = encoder.encode(value);
|
|
510
|
+
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
511
|
+
return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function getIdentity(DB, cdp_uid) {
|
|
515
|
+
return await DB.prepare(
|
|
516
|
+
'SELECT * FROM identity_graph WHERE cdp_uid = ?'
|
|
517
|
+
).bind(cdp_uid).first();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function logAbandonment(DB, eventData, sessionData) {
|
|
521
|
+
await DB.prepare(`
|
|
522
|
+
INSERT INTO events_log (event_id, event_name, session_id, heat_score, page_url, created_at)
|
|
523
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
|
524
|
+
`).bind(
|
|
525
|
+
crypto.randomUUID(), 'CheckoutAbandonment', sessionData.cdp_uid,
|
|
526
|
+
eventData.step || 1, sessionData.page_url || ''
|
|
527
|
+
).run();
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## 📊 PASSO 3: SCHEMA D1
|
|
534
|
+
|
|
535
|
+
```sql
|
|
536
|
+
-- Identity Graph (já definido no Server Agent)
|
|
537
|
+
CREATE TABLE IF NOT EXISTS identity_graph (
|
|
538
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
539
|
+
cdp_uid TEXT UNIQUE NOT NULL,
|
|
540
|
+
fbp TEXT,
|
|
541
|
+
fbc TEXT,
|
|
542
|
+
ttp TEXT,
|
|
543
|
+
ga_client_id TEXT,
|
|
544
|
+
external_id TEXT,
|
|
545
|
+
email_hash TEXT,
|
|
546
|
+
phone_hash TEXT,
|
|
547
|
+
first_name_hash TEXT,
|
|
548
|
+
last_name_hash TEXT,
|
|
549
|
+
cpf_hash TEXT,
|
|
550
|
+
first_utm TEXT,
|
|
551
|
+
heat_score_avg INTEGER DEFAULT 0,
|
|
552
|
+
visit_count INTEGER DEFAULT 1,
|
|
553
|
+
last_seen TEXT DEFAULT (datetime('now')),
|
|
554
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
-- Events Log (para Checkout Abandonment)
|
|
558
|
+
CREATE TABLE IF NOT EXISTS events_log (
|
|
559
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
560
|
+
event_id TEXT UNIQUE NOT NULL,
|
|
561
|
+
event_name TEXT NOT NULL,
|
|
562
|
+
platform TEXT,
|
|
563
|
+
session_id TEXT,
|
|
564
|
+
heat_score INTEGER DEFAULT 0,
|
|
565
|
+
page_url TEXT,
|
|
566
|
+
utm_source TEXT,
|
|
567
|
+
utm_campaign TEXT,
|
|
568
|
+
utm_medium TEXT,
|
|
569
|
+
status TEXT DEFAULT 'pending',
|
|
570
|
+
error_msg TEXT,
|
|
571
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
CREATE INDEX IF NOT EXISTS idx_cdp_uid ON identity_graph(cdp_uid);
|
|
575
|
+
CREATE INDEX IF NOT EXISTS idx_events_id ON events_log(event_id);
|
|
576
|
+
CREATE INDEX IF NOT EXISTS idx_events_created ON events_log(created_at);
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## ✅ CHECKLIST DE VERIFICAÇÃO
|
|
582
|
+
|
|
583
|
+
### Browser
|
|
584
|
+
- [ ] cdp_uid gerado no primeiro acesso e persistido por 1 ano
|
|
585
|
+
- [ ] InitiateCheckout disparado na chegada ao step 1
|
|
586
|
+
- [ ] IdentityUpdate disparado ao preencher cada campo (email, telefone, nome, CPF)
|
|
587
|
+
- [ ] AddPaymentInfo disparado na tela de pagamento (step 3)
|
|
588
|
+
- [ ] Purchase disparado na confirmação (step 4)
|
|
589
|
+
- [ ] CheckoutAbandonment usa `navigator.sendBeacon` para garantir envio
|
|
590
|
+
|
|
591
|
+
### Cloudflare Worker
|
|
592
|
+
- [ ] Endpoint `/api/tracking` configurado como Route no Cloudflare
|
|
593
|
+
- [ ] Identity Graph atualizado progressivamente conforme usuário preenche campos
|
|
594
|
+
- [ ] Meta CAPI v22.0 endpoint correto
|
|
595
|
+
- [ ] TikTok Events API v1.3 endpoint correto
|
|
596
|
+
- [ ] GA4 Measurement Protocol configurado
|
|
597
|
+
- [ ] CheckoutAbandonment logado apenas no D1 (não dispara para APIs)
|
|
598
|
+
|
|
599
|
+
### D1 Database
|
|
600
|
+
- [ ] Identity Graph com campos de hash (email, phone, first_name, last_name, cpf)
|
|
601
|
+
- [ ] Events Log para Checkout Abandonment
|
|
602
|
+
- [ ] Índices configurados para performance
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
## 🔄 FLUXO COMPLETO
|
|
607
|
+
|
|
608
|
+
```
|
|
609
|
+
1. Visitante acessa o checkout (Step 1)
|
|
610
|
+
└── JS: gera cdp_uid → salva cookie (1 ano)
|
|
611
|
+
└── JS: dispara InitiateCheckout → Worker
|
|
612
|
+
└── Worker: salva Identity Graph básica (fbp, fbc, ttp)
|
|
613
|
+
└── Worker: dispatch → Meta, GA4, TikTok (InitiateCheckout)
|
|
614
|
+
|
|
615
|
+
2. Visitante preenche dados pessoais (Step 2)
|
|
616
|
+
└── JS: blur no email → IdentityUpdate
|
|
617
|
+
└── Worker: atualiza Identity Graph com email_hash (enrichment)
|
|
618
|
+
└── JS: blur no telefone → IdentityUpdate
|
|
619
|
+
└── Worker: atualiza Identity Graph com phone_hash
|
|
620
|
+
└── JS: blur no nome → IdentityUpdate
|
|
621
|
+
└── Worker: atualiza Identity Graph com first/last_name_hash
|
|
622
|
+
|
|
623
|
+
3. Visitante vai para pagamento (Step 3)
|
|
624
|
+
└── JS: clique em botão de pagamento → AddPaymentInfo
|
|
625
|
+
└── Worker: dispatch → Meta, GA4, TikTok (AddPaymentInfo)
|
|
626
|
+
|
|
627
|
+
4. Visitante finaliza compra (Step 4)
|
|
628
|
+
└── Gateway: callback de sucesso → trackPurchaseConfirmed()
|
|
629
|
+
└── JS: dispara Purchase → Worker
|
|
630
|
+
└── Worker: recupera Identity Graph completa (todos os hashes)
|
|
631
|
+
└── Worker: dispatch → Meta Purchase CAPI (v22.0)
|
|
632
|
+
└── Worker: dispatch → GA4 Purchase (MP)
|
|
633
|
+
└── Worker: dispatch → TikTok CompletePayment (v1.3)
|
|
634
|
+
|
|
635
|
+
5. Visitante abandona checkout (opcional)
|
|
636
|
+
└── JS: visibilitychange + sendBeacon → CheckoutAbandonment
|
|
637
|
+
└── Worker: log apenas no D1 (não dispara para APIs)
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
## ⚠️ NOTAS CRÍTICAS
|
|
643
|
+
|
|
644
|
+
- **Enrichment Progressivo**: Identity Graph é atualizado conforme usuário preenche campos, melhorando Advanced Matching
|
|
645
|
+
- **Deduplicação**: Cada evento deve ter `event_id` único (usar `crypto.randomUUID()`)
|
|
646
|
+
- **Hashing**: SHA256 deve ser feito no Worker usando `crypto.subtle.digest` (WebCrypto API)
|
|
647
|
+
- **Privacy**: Nunca logar email/telefone/nome/CPF em texto claro no console do Worker
|
|
648
|
+
- **sendBeacon**: Checkout Abandonment deve usar `navigator.sendBeacon` para garantir envio mesmo se browser encerrar
|
|
649
|
+
- **Mesmo-Domain Protocol**: Worker DEVE rodar no mesmo domínio do site para evitar CORS e bloqueios
|
|
650
|
+
|
|
651
|
+
---
|
|
652
|
+
|
|
653
|
+
## 🚀 DEPLOY DO ZERO (Resumo)
|
|
654
|
+
|
|
655
|
+
1. **Criar Worker**: `npx wrangler init cdp-edge-ms`
|
|
656
|
+
2. **Configurar D1**: `npx wrangler d1 create cdp-edge-ms-db`
|
|
657
|
+
3. **Aplicar Schema**: `npx wrangler d1 execute cdp-edge-ms-db --file=schema.sql`
|
|
658
|
+
4. **Configurar Secrets**:
|
|
659
|
+
```bash
|
|
660
|
+
wrangler secret put META_ACCESS_TOKEN
|
|
661
|
+
wrangler secret put META_PIXEL_ID
|
|
662
|
+
wrangler secret put GA4_ID
|
|
663
|
+
wrangler secret put GA4_API_SECRET
|
|
664
|
+
wrangler secret put TIKTOK_PIXEL_ID
|
|
665
|
+
wrangler secret put TIKTOK_ACCESS_TOKEN
|
|
666
|
+
```
|
|
667
|
+
5. **Deploy**: `npx wrangler deploy`
|
|
668
|
+
6. **Configurar Route**: Cloudflare Dashboard → Workers & Pages → Routes → `seusite.com/api/*` → Worker
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
*Este modelo é 100% Cloudflare Native e elimina qualquer dependência de pixels diretos no browser.*
|