cdp-edge 1.9.0 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdp-edge",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "CDP Edge - Quantum Tracking - Sistema multi-agente para tracking digital Cloudflare Native (Workers + D1)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -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,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
+ ```