cdp-edge 1.8.1 → 1.10.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/package.json
CHANGED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# Template: Afiliado Sem Landing Page Própria
|
|
2
|
+
> Você é afiliado de um produto e envia tráfego direto para o checkout da plataforma
|
|
3
|
+
> (Hotmart, Kiwify, Eduzz, Monetizze, CartPanda, Ticto).
|
|
4
|
+
> Não tem domínio próprio — o link de afiliado é a URL de destino dos seus anúncios.
|
|
5
|
+
> Infraestrutura: Cloudflare Workers + D1 (100% Native)
|
|
6
|
+
|
|
7
|
+
**Quando usar este modelo:**
|
|
8
|
+
- Afiliado que divulga link de produto de terceiros
|
|
9
|
+
- Sem landing page: o anúncio aponta direto para `pay.hotmart.com/XXXXX`
|
|
10
|
+
- Quer rastrear vendas no Meta/TikTok/Google sem pixel na página do produtor
|
|
11
|
+
|
|
12
|
+
**O problema central:**
|
|
13
|
+
O pixel do Meta está no seu gerenciador, mas o checkout é do produtor — você não tem acesso à página. O produtor te dá um webhook de compra, mas esse webhook não tem fbp/fbc/UTMs porque o pixel nunca rodou na página de checkout.
|
|
14
|
+
|
|
15
|
+
**A solução:**
|
|
16
|
+
Criar uma página de ponte (bridge page) mínima no seu domínio. O usuário passa por ela, o CDP Edge captura todos os dados e injeta o `cdp_uid` no link de checkout. Quando o webhook de compra chegar, o D1 faz o lookup e envia o evento com atribuição completa.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Arquitetura
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Anúncio Meta/TikTok/Google
|
|
24
|
+
│
|
|
25
|
+
▼
|
|
26
|
+
┌───────────────────────────────┐
|
|
27
|
+
│ Bridge Page (seu domínio) │ ← leva 2s, redireciona auto
|
|
28
|
+
│ bridge.seudominio.com │
|
|
29
|
+
│ │
|
|
30
|
+
│ 1. Gera cdp_uid │
|
|
31
|
+
│ 2. Captura fbclid/ttclid │
|
|
32
|
+
│ 3. Salva fbp/fbc/UTMs no D1 │
|
|
33
|
+
│ 4. Injeta cdp_uid no link │
|
|
34
|
+
│ 5. Redireciona para checkout │
|
|
35
|
+
└───────────────────────────────┘
|
|
36
|
+
│
|
|
37
|
+
▼
|
|
38
|
+
┌──────────────────────────────────┐
|
|
39
|
+
│ Checkout da Plataforma │
|
|
40
|
+
│ pay.hotmart.com/XXXXX?xcod=uid │
|
|
41
|
+
│ │
|
|
42
|
+
│ Usuário compra │
|
|
43
|
+
│ Plataforma envia webhook → │
|
|
44
|
+
│ Worker → D1 lookup(uid) │
|
|
45
|
+
│ Meta Purchase CAPI ✅ │
|
|
46
|
+
└──────────────────────────────────┘
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Passo 1 — Bridge Page (index.html)
|
|
52
|
+
|
|
53
|
+
Crie uma página mínima no seu domínio. Pode ser hospedada no Cloudflare Pages (grátis).
|
|
54
|
+
|
|
55
|
+
```html
|
|
56
|
+
<!DOCTYPE html>
|
|
57
|
+
<html lang="pt-BR">
|
|
58
|
+
<head>
|
|
59
|
+
<meta charset="UTF-8">
|
|
60
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
61
|
+
<title>Redirecionando...</title>
|
|
62
|
+
<!-- Pixel Meta browser (para ViewContent + fbp/fbc) -->
|
|
63
|
+
<script>
|
|
64
|
+
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
|
65
|
+
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
|
|
66
|
+
n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
|
|
67
|
+
t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
|
|
68
|
+
document,'script','https://connect.facebook.net/en_US/fbevents.js');
|
|
69
|
+
fbq('init', 'SEU_PIXEL_ID');
|
|
70
|
+
fbq('track', 'PageView');
|
|
71
|
+
</script>
|
|
72
|
+
</head>
|
|
73
|
+
<body>
|
|
74
|
+
<p style="font-family:sans-serif;text-align:center;margin-top:40px">
|
|
75
|
+
Aguarde, você está sendo redirecionado...
|
|
76
|
+
</p>
|
|
77
|
+
|
|
78
|
+
<script type="module">
|
|
79
|
+
import { init, track, getUserIdWithFallback, getUTMsWithFallback } from '/js/cdpTrack.js';
|
|
80
|
+
|
|
81
|
+
await init();
|
|
82
|
+
|
|
83
|
+
const uid = getUserIdWithFallback();
|
|
84
|
+
const utms = getUTMsWithFallback();
|
|
85
|
+
|
|
86
|
+
// Disparar ViewContent (sinal de interesse antes do checkout)
|
|
87
|
+
await track('ViewContent', {
|
|
88
|
+
content_name: 'Bridge Page - Afiliado',
|
|
89
|
+
content_type: 'product',
|
|
90
|
+
uid,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Construir URL de checkout com cdp_uid injetado
|
|
94
|
+
// passCheckoutParams() já faz isso para os links na página,
|
|
95
|
+
// mas para redirect direto usamos a função abaixo:
|
|
96
|
+
const checkoutUrl = buildCheckoutUrl('https://pay.hotmart.com/SEU_LINK_DE_AFILIADO', uid, utms);
|
|
97
|
+
|
|
98
|
+
// Redireciona após 800ms (tempo suficiente para o evento ser enviado)
|
|
99
|
+
setTimeout(() => { window.location.href = checkoutUrl; }, 800);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Constrói a URL de checkout com parâmetros de atribuição
|
|
103
|
+
*/
|
|
104
|
+
function buildCheckoutUrl(baseUrl, uid, utms) {
|
|
105
|
+
const url = new URL(baseUrl);
|
|
106
|
+
|
|
107
|
+
// Hotmart: xcod = user_id, sck = UTMs pipe-separados
|
|
108
|
+
if (baseUrl.includes('hotmart.com')) {
|
|
109
|
+
if (uid) url.searchParams.set('xcod', uid);
|
|
110
|
+
const sck = [utms.utm_source, utms.utm_medium, utms.utm_campaign, utms.utm_content, utms.utm_term]
|
|
111
|
+
.map(v => v || 'direto').join('|');
|
|
112
|
+
if (utms.utm_source) url.searchParams.set('sck', sck);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Kiwify: src = utm_source
|
|
116
|
+
else if (baseUrl.includes('kiwify.com.br')) {
|
|
117
|
+
if (utms.utm_source) url.searchParams.set('src', utms.utm_source);
|
|
118
|
+
if (utms.utm_medium) url.searchParams.set('utm_medium', utms.utm_medium);
|
|
119
|
+
if (utms.utm_campaign) url.searchParams.set('utm_campaign', utms.utm_campaign);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Eduzz / Monetizze: src = utm_source
|
|
123
|
+
else if (baseUrl.includes('eduzz.com') || baseUrl.includes('monetizze.com.br')) {
|
|
124
|
+
if (utms.utm_source) url.searchParams.set('src', utms.utm_source);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// CartPanda: utm_* direto
|
|
128
|
+
else if (baseUrl.includes('cartpanda.com')) {
|
|
129
|
+
Object.entries(utms).forEach(([k, v]) => { if (v) url.searchParams.set(k, v); });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Ticto: utm_* + user_id
|
|
133
|
+
else if (baseUrl.includes('ticto.app')) {
|
|
134
|
+
Object.entries(utms).forEach(([k, v]) => { if (v) url.searchParams.set(k, v); });
|
|
135
|
+
if (uid) url.searchParams.set('user_id', uid);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Genérico: repassa tudo
|
|
139
|
+
else {
|
|
140
|
+
Object.entries(utms).forEach(([k, v]) => { if (v) url.searchParams.set(k, v); });
|
|
141
|
+
if (uid) url.searchParams.set('cdp_uid', uid);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return url.toString();
|
|
145
|
+
}
|
|
146
|
+
</script>
|
|
147
|
+
</body>
|
|
148
|
+
</html>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Passo 2 — Worker Route (Same-Domain)
|
|
154
|
+
|
|
155
|
+
O Worker precisa estar no mesmo domínio da bridge page para capturar fbp/fbc como first-party cookies.
|
|
156
|
+
|
|
157
|
+
```toml
|
|
158
|
+
# wrangler.toml — adicionar route da bridge page
|
|
159
|
+
[[routes]]
|
|
160
|
+
pattern = "bridge.seudominio.com/*"
|
|
161
|
+
zone_name = "seudominio.com"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
wrangler deploy
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Passo 3 — Configurar Webhook na Plataforma
|
|
171
|
+
|
|
172
|
+
### Hotmart
|
|
173
|
+
```
|
|
174
|
+
Hotmart → Ferramentas → Webhooks → Adicionar Webhook
|
|
175
|
+
URL: https://bridge.seudominio.com/webhook/hotmart
|
|
176
|
+
Eventos: Compra Aprovada, Compra Cancelada, Reembolso
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Kiwify
|
|
180
|
+
```
|
|
181
|
+
Kiwify → Configurações → Webhooks
|
|
182
|
+
URL: https://bridge.seudominio.com/webhook/kiwify
|
|
183
|
+
Status: Ativo
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Ticto
|
|
187
|
+
```
|
|
188
|
+
Ticto → Configurações → Webhook
|
|
189
|
+
URL: https://bridge.seudominio.com/webhook/ticto
|
|
190
|
+
Eventos: purchase_approved
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
O Worker (`worker.js`) já tem handlers para `/webhook/hotmart`, `/webhook/kiwify` e `/webhook/ticto`. Eles:
|
|
194
|
+
1. Extraem `xcod`/`sck`/`user_id` do payload
|
|
195
|
+
2. Fazem D1 lookup pelo `cdp_uid`
|
|
196
|
+
3. Recuperam `fbp`, `fbc`, `ttp`, UTMs
|
|
197
|
+
4. Enviam `Purchase` para Meta CAPI + GA4 + TikTok
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Passo 4 — Fallback de Fingerprint (Automático)
|
|
202
|
+
|
|
203
|
+
O SDK já salva automaticamente uid + UTMs no `localStorage` (`_cdp_aff`).
|
|
204
|
+
|
|
205
|
+
**Cenário coberto:** usuário vê o anúncio, clica, a página de bridge abre em nova aba — o cookie pode não ser lido em alguns navegadores nesse contexto.
|
|
206
|
+
|
|
207
|
+
```javascript
|
|
208
|
+
// Automaticamente chamado em init():
|
|
209
|
+
_saveAffiliateContext(); // salva uid + UTMs no localStorage
|
|
210
|
+
|
|
211
|
+
// Ao montar a bridge page:
|
|
212
|
+
const uid = getUserIdWithFallback(); // cookie → localStorage → ''
|
|
213
|
+
const utms = getUTMsWithFallback(); // URL params → localStorage → {}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
O `getUserIdWithFallback()` tenta em ordem:
|
|
217
|
+
1. Cookie `_cdp_uid` (primeira opção)
|
|
218
|
+
2. localStorage `_cdp_aff.uid` (fallback — 30 dias de validade)
|
|
219
|
+
3. String vazia (sem identificação)
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Passo 5 — Anúncio Meta: URL de Destino
|
|
224
|
+
|
|
225
|
+
Configure o anúncio apontando para a bridge page, não para o checkout direto:
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
URL de Destino do Anúncio:
|
|
229
|
+
https://bridge.seudominio.com/?utm_source=meta&utm_medium=paid&utm_campaign=NOME_CAMP&utm_content=NOME_AD
|
|
230
|
+
|
|
231
|
+
❌ NÃO usar:
|
|
232
|
+
https://pay.hotmart.com/XXXXX (perde fbp/fbc/UTMs)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
O Meta injeta o `fbclid` automaticamente na URL. O SDK captura e salva no D1.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Passo 6 — Verificar Atribuição
|
|
240
|
+
|
|
241
|
+
Após uma venda de teste:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
# Ver se o cdp_uid chegou no webhook e fez lookup
|
|
245
|
+
wrangler d1 execute cdp-edge-db --command="
|
|
246
|
+
SELECT
|
|
247
|
+
w.raw_payload,
|
|
248
|
+
l.fbp,
|
|
249
|
+
l.fbc,
|
|
250
|
+
l.utm_source,
|
|
251
|
+
l.utm_campaign
|
|
252
|
+
FROM webhook_events w
|
|
253
|
+
LEFT JOIN leads l ON l.user_id = json_extract(w.raw_payload, '$.xcod')
|
|
254
|
+
ORDER BY w.created_at DESC
|
|
255
|
+
LIMIT 1;
|
|
256
|
+
"
|
|
257
|
+
|
|
258
|
+
# Confirmar evento no Meta Events Manager
|
|
259
|
+
# Gerenciador de Eventos → filtrar por Purchase → verificar match_keys
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Checklist de Verificação
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
[ ] Bridge page no seu domínio (Cloudflare Pages recomendado — grátis)
|
|
268
|
+
[ ] Worker Route configurado: bridge.seudominio.com/*
|
|
269
|
+
[ ] SDK instalado na bridge page com getUserIdWithFallback()
|
|
270
|
+
[ ] URL de destino do anúncio aponta para bridge page (não checkout direto)
|
|
271
|
+
[ ] UTMs na URL do anúncio (utm_source, utm_medium, utm_campaign)
|
|
272
|
+
[ ] Webhook configurado na plataforma para o Worker
|
|
273
|
+
[ ] Teste: compra de desenvolvimento → D1 lookup → Purchase no Meta Events Manager
|
|
274
|
+
[ ] Verificar fbp no D1 (confirma que fbclid foi capturado)
|
|
275
|
+
[ ] Verificar utm_source no D1 (confirma que UTMs chegaram)
|
|
276
|
+
[ ] Meta Events Manager: compra com match_keys email/phone (se plataforma enviar no webhook)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Fluxo Completo
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
1. Anúncio Meta clicado
|
|
285
|
+
└─ URL: bridge.seudominio.com/?fbclid=XXX&utm_source=meta&utm_campaign=CAMP
|
|
286
|
+
|
|
287
|
+
2. Bridge Page carrega (800ms)
|
|
288
|
+
└─ cdp_uid gerado (cookie 365 dias + localStorage backup)
|
|
289
|
+
└─ fbclid capturado → _fbc calculado
|
|
290
|
+
└─ UTMs salvos no localStorage (_cdp_aff)
|
|
291
|
+
└─ PageView → Worker → D1 salva {fbp, fbc, utm_source, utm_campaign, ip, ua}
|
|
292
|
+
└─ ViewContent → Worker → Meta ViewContent CAPI
|
|
293
|
+
|
|
294
|
+
3. Redirect para checkout
|
|
295
|
+
└─ URL: pay.hotmart.com/XXX?xcod=cdp_uid&sck=meta|paid|CAMP||
|
|
296
|
+
|
|
297
|
+
4. Usuário compra
|
|
298
|
+
└─ Hotmart webhook → Worker /webhook/hotmart
|
|
299
|
+
└─ Extrai xcod = cdp_uid
|
|
300
|
+
└─ D1 lookup(cdp_uid) → fbp, fbc, utm_source, utm_campaign, ip
|
|
301
|
+
└─ Meta Purchase CAPI: fbp + fbc + utm_source → atribuição completa ✅
|
|
302
|
+
└─ GA4 Purchase + TikTok CompletePayment
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Notas de Privacidade / LGPD
|
|
308
|
+
|
|
309
|
+
- A bridge page não coleta email ou telefone
|
|
310
|
+
- Apenas identifica a sessão com um ID anônimo (`cdp_uid`)
|
|
311
|
+
- Os dados de compra (email, telefone) chegam pelo webhook da plataforma e são hasheados antes de enviar ao CAPI
|
|
312
|
+
- O redirecionamento automático é transparente — nenhum dado sensível é exposto na URL
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LinkedIn Insight Tag — Browser Template — CDP Edge Quantum Tier
|
|
3
|
+
*
|
|
4
|
+
* Inclui no <head> do projeto. Captura li_fat_id e dispara eventos
|
|
5
|
+
* de conversão via tag browser + envia server-side via CAPI v2.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
(function(l) {
|
|
9
|
+
if (!l) {
|
|
10
|
+
window.lintrk = function(a, b) { window.lintrk.q.push([a, b]) };
|
|
11
|
+
window.lintrk.q = [];
|
|
12
|
+
}
|
|
13
|
+
var s = document.getElementsByTagName("script")[0];
|
|
14
|
+
var b = document.createElement("script");
|
|
15
|
+
b.type = "text/javascript";
|
|
16
|
+
b.async = true;
|
|
17
|
+
b.src = "https://snap.licdn.com/li.lms-analytics/insight.beta.min.js";
|
|
18
|
+
s.parentNode.insertBefore(b, s);
|
|
19
|
+
})(window.lintrk);
|
|
20
|
+
|
|
21
|
+
// ── Inicialização com Partner ID ──────────────────────────────────────────────
|
|
22
|
+
window._linkedin_partner_id = "{{LINKEDIN_PARTNER_ID}}";
|
|
23
|
+
window._linkedin_data_partner_ids = window._linkedin_data_partner_ids || [];
|
|
24
|
+
window._linkedin_data_partner_ids.push(window._linkedin_partner_id);
|
|
25
|
+
|
|
26
|
+
// ── Captura li_fat_id (LinkedIn First-Party Cookie) ──────────────────────────
|
|
27
|
+
function getLinkedInFatId() {
|
|
28
|
+
const match = document.cookie.match(/li_fat_id=([^;]+)/);
|
|
29
|
+
return match ? match[1] : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Evento de Conversão Browser ──────────────────────────────────────────────
|
|
33
|
+
// Mapeamento CDP Edge → LinkedIn
|
|
34
|
+
// Lead → Lead Conversion
|
|
35
|
+
// Purchase → Purchase Conversion
|
|
36
|
+
// CompleteRegistration → Registration
|
|
37
|
+
|
|
38
|
+
function trackLinkedInEvent(conversionId) {
|
|
39
|
+
if (typeof window.lintrk === 'function') {
|
|
40
|
+
window.lintrk('track', { conversion_id: conversionId });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Integração com cdpTrack.js ────────────────────────────────────────────────
|
|
45
|
+
// O cdpTrack.js envia o li_fat_id para o Worker via payload.liFatId
|
|
46
|
+
// O Worker repassa ao LinkedIn CAPI v2 server-side
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spotify Pixel — Browser Template — CDP Edge Quantum Tier
|
|
3
|
+
*
|
|
4
|
+
* Inclui no <head> do projeto. Inicializa o Spotify Pixel SDK
|
|
5
|
+
* e captura eventos de conversão browser-side.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── Inicialização do Spotify Pixel ───────────────────────────────────────────
|
|
9
|
+
!function(e){
|
|
10
|
+
if (!window.SpotifyPixel) {
|
|
11
|
+
var n = window.SpotifyPixel = function() {
|
|
12
|
+
n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments)
|
|
13
|
+
};
|
|
14
|
+
n.push = n;
|
|
15
|
+
n.loaded = !0;
|
|
16
|
+
n.version = "1.0";
|
|
17
|
+
n.queue = [];
|
|
18
|
+
var t = document.createElement("script");
|
|
19
|
+
t.async = !0;
|
|
20
|
+
t.src = "https://pixel.byspotify.com/ping.min.js";
|
|
21
|
+
var r = document.getElementsByTagName("script")[0];
|
|
22
|
+
r.parentNode.insertBefore(t, r);
|
|
23
|
+
}
|
|
24
|
+
}();
|
|
25
|
+
|
|
26
|
+
window.SpotifyPixel('init', '{{SPOTIFY_PIXEL_ID}}');
|
|
27
|
+
window.SpotifyPixel('track', 'PAGE_VIEW');
|
|
28
|
+
|
|
29
|
+
// ── Mapeamento de Eventos CDP Edge → Spotify ──────────────────────────────────
|
|
30
|
+
// PageView → PAGE_VIEW
|
|
31
|
+
// ViewContent → VIEW_CONTENT
|
|
32
|
+
// Lead → LEAD
|
|
33
|
+
// Purchase → PURCHASE
|
|
34
|
+
// AddToCart → ADD_TO_CART
|
|
35
|
+
// InitiateCheckout → INITIATE_CHECKOUT
|
|
36
|
+
// CompleteRegistration → SIGN_UP
|
|
37
|
+
|
|
38
|
+
function trackSpotifyEvent(eventType, params) {
|
|
39
|
+
if (typeof window.SpotifyPixel === 'function') {
|
|
40
|
+
window.SpotifyPixel('track', eventType, params || {});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Integração com cdpTrack.js ────────────────────────────────────────────────
|
|
45
|
+
// O cdpTrack.js envia o evento para o Worker server-side via sendSpotifyCapi()
|
|
46
|
+
// O browser dispara via trackSpotifyEvent() em paralelo para deduplicação
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# Template: Página VSL — Video Sales Letter
|
|
2
|
+
> Página de vendas centrada em vídeo (YouTube, Vimeo ou player nativo).
|
|
3
|
+
> O vídeo é o principal elemento de conversão — tudo gira em torno de quem assistiu quanto.
|
|
4
|
+
> Infraestrutura: Cloudflare Workers + D1 (100% Native)
|
|
5
|
+
|
|
6
|
+
**Quando usar este modelo:**
|
|
7
|
+
- VSL (Video Sales Letter) com vídeo como principal argumento de venda
|
|
8
|
+
- Webclasse / Aula gratuita antes do pitch
|
|
9
|
+
- Lançamento com vídeo de antecipação + oferta no final
|
|
10
|
+
- Série de vídeos com desbloqueio progressivo
|
|
11
|
+
|
|
12
|
+
**Diferença dos outros modelos:**
|
|
13
|
+
- Engajamento medido por **progresso de vídeo**, não apenas por scroll
|
|
14
|
+
- `VideoDropout` registra o **segundo exato** em que cada usuário parou de assistir
|
|
15
|
+
- CTA só aparece/destrava após threshold de vídeo (ex: 75%)
|
|
16
|
+
- Lead capture disparado **dentro do vídeo** (ex: ao atingir 50% do progresso)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Arquitetura Técnica
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
24
|
+
│ Browser: Página VSL │
|
|
25
|
+
│ │
|
|
26
|
+
│ 1. PageView → Worker (entrada do usuário) │
|
|
27
|
+
│ 2. VideoPlay → Worker (interesse confirmado) │
|
|
28
|
+
│ 3. VideoProgress 25/50/75/100 → Worker (qualificação) │
|
|
29
|
+
│ 4. VideoDropout → Worker (segundo exato de abandono) │
|
|
30
|
+
│ 5. Lead (se formulário aparecer após threshold) → Worker │
|
|
31
|
+
│ 6. InitiateCheckout (CTA destravado pelo vídeo) → Worker │
|
|
32
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
33
|
+
│
|
|
34
|
+
▼
|
|
35
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
36
|
+
│ Cloudflare Worker │
|
|
37
|
+
│ │
|
|
38
|
+
│ /api/tracking: │
|
|
39
|
+
│ VideoProgress 75% → Meta: ViewContent + TikTok: ViewContent │
|
|
40
|
+
│ Lead → Meta: Lead + GA4: generate_lead + TikTok: SubmitForm │
|
|
41
|
+
│ InitiateCheckout → Meta + GA4 + TikTok │
|
|
42
|
+
│ │
|
|
43
|
+
│ D1: behavioral_events (dropout_percent, dropout_second) │
|
|
44
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Passo 1 — Instalação do SDK
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<!-- No <head> -->
|
|
53
|
+
<script type="module">
|
|
54
|
+
import { init, track, trackLead } from '/js/cdpTrack.js';
|
|
55
|
+
import { getVideoSummary } from '/js/micro-events.js';
|
|
56
|
+
|
|
57
|
+
window.cdpGetVideoSummary = getVideoSummary;
|
|
58
|
+
await init();
|
|
59
|
+
</script>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
O `initMicroEvents()` é chamado automaticamente dentro de `init()` e detecta todos os vídeos da página.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Passo 2 — Marcar os Vídeos para Tracking
|
|
67
|
+
|
|
68
|
+
### YouTube Embed
|
|
69
|
+
```html
|
|
70
|
+
<!-- Adicionar id para identificação -->
|
|
71
|
+
<iframe
|
|
72
|
+
id="vsl-main"
|
|
73
|
+
src="https://www.youtube.com/embed/XXXXXXXXX?enablejsapi=1"
|
|
74
|
+
data-video-id="vsl-principal"
|
|
75
|
+
allow="autoplay"
|
|
76
|
+
allowfullscreen>
|
|
77
|
+
</iframe>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
> O `enablejsapi=1` é necessário. O SDK injeta automaticamente se não estiver na URL.
|
|
81
|
+
|
|
82
|
+
### Vimeo Embed
|
|
83
|
+
```html
|
|
84
|
+
<iframe
|
|
85
|
+
id="vsl-main"
|
|
86
|
+
src="https://player.vimeo.com/video/XXXXXXXXX"
|
|
87
|
+
data-video-id="vsl-principal"
|
|
88
|
+
allow="autoplay; fullscreen">
|
|
89
|
+
</iframe>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Player HTML5 Nativo
|
|
93
|
+
```html
|
|
94
|
+
<video
|
|
95
|
+
id="vsl-main"
|
|
96
|
+
data-video-id="vsl-principal"
|
|
97
|
+
controls
|
|
98
|
+
preload="metadata">
|
|
99
|
+
<source src="/videos/vsl.mp4" type="video/mp4">
|
|
100
|
+
</video>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Passo 3 — Lógica de Desbloqueio de CTA por Progresso de Vídeo
|
|
106
|
+
|
|
107
|
+
A técnica mais eficaz: o botão de compra fica oculto e só aparece quando o usuário atinge um threshold do vídeo.
|
|
108
|
+
|
|
109
|
+
```html
|
|
110
|
+
<!-- CTA inicialmente oculto -->
|
|
111
|
+
<div id="cta-container" style="display:none; opacity:0; transition: opacity 0.5s;">
|
|
112
|
+
<a href="https://pay.hotmart.com/XXXXXX" class="btn-comprar">
|
|
113
|
+
Quero Garantir Minha Vaga →
|
|
114
|
+
</a>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<script>
|
|
118
|
+
// Ouve eventos do cdpTrack para destravar o CTA
|
|
119
|
+
window.addEventListener('cdp:VideoProgress', (e) => {
|
|
120
|
+
const { progress_percent, video_id } = e.detail;
|
|
121
|
+
|
|
122
|
+
// Destravar CTA ao atingir 75% do vídeo
|
|
123
|
+
if (progress_percent >= 75 && video_id === 'vsl-principal') {
|
|
124
|
+
const cta = document.getElementById('cta-container');
|
|
125
|
+
cta.style.display = 'block';
|
|
126
|
+
setTimeout(() => { cta.style.opacity = '1'; }, 100);
|
|
127
|
+
|
|
128
|
+
// Disparar ViewContent: sinal forte de intenção
|
|
129
|
+
window.cdpTrack.track('ViewContent', {
|
|
130
|
+
content_name: 'VSL 75% - CTA Destravado',
|
|
131
|
+
content_type: 'video',
|
|
132
|
+
video_progress: 75
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Formulário de lead que aparece a 50%
|
|
138
|
+
window.addEventListener('cdp:VideoProgress', (e) => {
|
|
139
|
+
if (e.detail.progress_percent >= 50 && e.detail.video_id === 'vsl-principal') {
|
|
140
|
+
document.getElementById('lead-form-container')?.classList.remove('hidden');
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
</script>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Para emitir o evento custom, adicionar no dispatcher de `micro-events.js` (ou escutar via worker callback):
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
// Em micro-events.js — dispatchEvent já chama cdpTrack.track()
|
|
150
|
+
// Para emitir evento DOM customizado em paralelo:
|
|
151
|
+
function dispatchEvent(eventName, data = {}) {
|
|
152
|
+
if (typeof cdpTrack !== 'undefined' && cdpTrack.track) {
|
|
153
|
+
cdpTrack.track(eventName, data);
|
|
154
|
+
}
|
|
155
|
+
// Evento DOM para código da página reagir
|
|
156
|
+
window.dispatchEvent(new CustomEvent(`cdp:${eventName}`, { detail: data }));
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Passo 4 — Mapeamento de Eventos de Vídeo para Plataformas
|
|
163
|
+
|
|
164
|
+
| Evento CDP | Meta CAPI | GA4 | TikTok | Gatilho |
|
|
165
|
+
|---|---|---|---|---|
|
|
166
|
+
| VideoPlay | — | video_start | — | Usuário deu play |
|
|
167
|
+
| VideoProgress 25% | — | video_progress | — | Assistiu 25% |
|
|
168
|
+
| VideoProgress 50% | ViewContent | video_progress | ViewContent | Assistiu 50% |
|
|
169
|
+
| VideoProgress 75% | ViewContent | video_progress | ViewContent | Assistiu 75% — CTA aparece |
|
|
170
|
+
| VideoProgress 100% | ViewContent | video_complete | ViewContent | Assistiu tudo |
|
|
171
|
+
| VideoDropout | — | (behavioral) | — | Parou antes do fim |
|
|
172
|
+
| Lead | Lead | generate_lead | SubmitForm | Formulário preenchido no vídeo |
|
|
173
|
+
| InitiateCheckout | InitiateCheckout | begin_checkout | InitiateCheckout | Clicou no CTA |
|
|
174
|
+
|
|
175
|
+
### Configurar no Worker (`worker.js`) — handler de VideoProgress:
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
// Dentro do handler /api/tracking, adicionar case para VideoProgress
|
|
179
|
+
case 'VideoProgress':
|
|
180
|
+
const vp = payload.behavioral_data?.progress_percent || 0;
|
|
181
|
+
// Só disparar plataformas em thresholds significativos
|
|
182
|
+
if (vp >= 50) {
|
|
183
|
+
await Promise.allSettled([
|
|
184
|
+
sendMetaCapi(env, 'ViewContent', payload, {
|
|
185
|
+
content_name: `VSL ${vp}%`,
|
|
186
|
+
content_type: 'video',
|
|
187
|
+
}),
|
|
188
|
+
sendGA4(env, 'video_progress', payload, { percent: vp }),
|
|
189
|
+
sendTikTokEvents(env, 'ViewContent', payload),
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
// Sempre salvar no D1 (para dropout heatmap)
|
|
193
|
+
await saveVideoEvent(env.DB, payload);
|
|
194
|
+
break;
|
|
195
|
+
|
|
196
|
+
case 'VideoDropout':
|
|
197
|
+
// Registrar no D1 para análise de abandono por minuto
|
|
198
|
+
await env.DB.prepare(`
|
|
199
|
+
INSERT INTO behavioral_events (uid, event_type, event_data, created_at)
|
|
200
|
+
VALUES (?, 'VideoDropout', ?, datetime('now'))
|
|
201
|
+
`).bind(
|
|
202
|
+
payload.user_id,
|
|
203
|
+
JSON.stringify({
|
|
204
|
+
video_id: payload.behavioral_data.video_id,
|
|
205
|
+
dropout_percent: payload.behavioral_data.dropout_percent,
|
|
206
|
+
dropout_second: payload.behavioral_data.dropout_second,
|
|
207
|
+
})
|
|
208
|
+
).run();
|
|
209
|
+
break;
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Passo 5 — Dashboard de Dropout Heatmap
|
|
215
|
+
|
|
216
|
+
Consulta D1 para ver onde os usuários abandonam o vídeo:
|
|
217
|
+
|
|
218
|
+
```sql
|
|
219
|
+
-- Distribuição de abandono por segmento de 10%
|
|
220
|
+
SELECT
|
|
221
|
+
ROUND(CAST(json_extract(event_data, '$.dropout_percent') AS INTEGER) / 10.0) * 10 AS percent_bucket,
|
|
222
|
+
COUNT(*) AS abandonos,
|
|
223
|
+
AVG(json_extract(event_data, '$.dropout_second')) AS media_segundo
|
|
224
|
+
FROM behavioral_events
|
|
225
|
+
WHERE event_type = 'VideoDropout'
|
|
226
|
+
AND json_extract(event_data, '$.video_id') = 'vsl-principal'
|
|
227
|
+
AND created_at >= datetime('now', '-7 days')
|
|
228
|
+
GROUP BY percent_bucket
|
|
229
|
+
ORDER BY percent_bucket;
|
|
230
|
+
|
|
231
|
+
-- Taxa de conclusão por cohort de tráfego
|
|
232
|
+
SELECT
|
|
233
|
+
l.utm_source,
|
|
234
|
+
COUNT(DISTINCT b.uid) AS viewers,
|
|
235
|
+
SUM(CASE WHEN json_extract(b.event_data, '$.dropout_percent') >= 75 THEN 1 ELSE 0 END) AS chegaram_75,
|
|
236
|
+
ROUND(
|
|
237
|
+
100.0 * SUM(CASE WHEN json_extract(b.event_data, '$.dropout_percent') >= 75 THEN 1 ELSE 0 END)
|
|
238
|
+
/ COUNT(DISTINCT b.uid), 1
|
|
239
|
+
) AS taxa_conclusao_75pct
|
|
240
|
+
FROM behavioral_events b
|
|
241
|
+
LEFT JOIN leads l ON l.user_id = b.uid
|
|
242
|
+
WHERE b.event_type = 'VideoDropout'
|
|
243
|
+
AND b.created_at >= datetime('now', '-30 days')
|
|
244
|
+
GROUP BY l.utm_source
|
|
245
|
+
ORDER BY viewers DESC;
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Checklist de Verificação
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
[ ] SDK instalado no <head> com type="module"
|
|
254
|
+
[ ] Vídeo com id ou data-video-id para identificação
|
|
255
|
+
[ ] enablejsapi=1 na URL do YouTube (ou deixar o SDK injetar)
|
|
256
|
+
[ ] CTA oculto inicialmente, destravado por VideoProgress
|
|
257
|
+
[ ] worker.js: case 'VideoProgress' e 'VideoDropout' implementados
|
|
258
|
+
[ ] D1: tabela behavioral_events existente (migrate-v2.sql ou superior)
|
|
259
|
+
[ ] Testar: abrir o vídeo, pausar a 30%, verificar VideoDropout no D1
|
|
260
|
+
[ ] Testar: assistir até 75%, verificar CTA aparecendo + ViewContent no D1
|
|
261
|
+
[ ] Query de dropout heatmap retornando dados
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Fluxo Completo de Uma Sessão VSL
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
1. Usuário chega via anúncio Meta
|
|
270
|
+
└─ cdp_uid gerado (cookie 365 dias)
|
|
271
|
+
└─ UTMs salvos (_cdp_aff no localStorage — fallback de afiliado)
|
|
272
|
+
└─ PageView → Worker → D1 + Meta PageView + GA4 page_view
|
|
273
|
+
|
|
274
|
+
2. Usuário dá play no vídeo
|
|
275
|
+
└─ VideoPlay → Worker → D1 behavioral_events
|
|
276
|
+
|
|
277
|
+
3. Usuário assiste até 50%
|
|
278
|
+
└─ VideoProgress 25% → Worker
|
|
279
|
+
└─ VideoProgress 50% → Worker → Meta ViewContent + Formulário de lead aparece
|
|
280
|
+
|
|
281
|
+
4. Usuário para de assistir a 62% (abandono)
|
|
282
|
+
└─ VideoPause → VideoDropout(62%, segundo 743) → Worker → D1
|
|
283
|
+
|
|
284
|
+
5. (Outro cenário) Usuário assiste até 75%
|
|
285
|
+
└─ VideoProgress 75% → Worker → Meta ViewContent + CTA destrava
|
|
286
|
+
└─ Usuário clica no CTA → InitiateCheckout → Worker
|
|
287
|
+
└─ Usuário compra na Hotmart → webhook → Worker → Meta Purchase CAPI
|
|
288
|
+
|
|
289
|
+
6. Otimização
|
|
290
|
+
└─ Dashboard mostra: 68% dos usuários abandonam entre 45-60% do vídeo
|
|
291
|
+
└─ Ação: testar pitch antecipado no minuto 40
|
|
292
|
+
```
|