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,992 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MICRO-EVENTS - CDP Edge (Quantum Tier)
|
|
3
|
+
*
|
|
4
|
+
* Módulo de captura de micro-eventos de engajamento para tracking premium.
|
|
5
|
+
* Implementa: Scroll, Time on Page, Video, Click Heatmap, Rapid Clicks, CTA Hover
|
|
6
|
+
*
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ── Guards — segurança em SSR e SDK não carregado ──
|
|
11
|
+
const isBrowser = typeof window !== 'undefined';
|
|
12
|
+
const has = (fn) => isBrowser && typeof window[fn] === 'function';
|
|
13
|
+
|
|
14
|
+
// ── Configuração de Scroll ─────────────────────────────────────────────
|
|
15
|
+
const SCROLL_CONFIG = {
|
|
16
|
+
thresholds: [25, 50, 75, 100],
|
|
17
|
+
signalStrength: {
|
|
18
|
+
'25%': 1.5,
|
|
19
|
+
'50%': 2.0,
|
|
20
|
+
'75%': 3.0,
|
|
21
|
+
'100%': 4.0
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// ── Configuração de Time on Page ───────────────────────────────────────────
|
|
26
|
+
const TIME_CONFIG = {
|
|
27
|
+
thresholds: {
|
|
28
|
+
curioso: 10000, // 10 segundos
|
|
29
|
+
interessado: 60000, // 60 segundos
|
|
30
|
+
comprador: 180000 // 180 segundos (3 minutos)
|
|
31
|
+
profundao: 60
|
|
32
|
+
},
|
|
33
|
+
signalStrength: {
|
|
34
|
+
curioso: 1.0,
|
|
35
|
+
interessado: 2.5,
|
|
36
|
+
comprador: 4.0,
|
|
37
|
+
profundao: 3.5
|
|
38
|
+
},
|
|
39
|
+
intentionLevels: {
|
|
40
|
+
curioso: 'curioso',
|
|
41
|
+
interessado: 'interessado',
|
|
42
|
+
comprador: 'comprador',
|
|
43
|
+
profundao: 'profundo'
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ── Configuração de Vídeo ─────────────────────────────────────────────────────
|
|
48
|
+
const VIDEO_CONFIG = {
|
|
49
|
+
progressThresholds: [25, 50, 75, 100],
|
|
50
|
+
// Dropout: captura o segundo exato em que o usuário parou de assistir
|
|
51
|
+
dropoutEnabled: true,
|
|
52
|
+
signalStrength: {
|
|
53
|
+
play: 2.0,
|
|
54
|
+
progress25: 3.0,
|
|
55
|
+
progress50: 4.0,
|
|
56
|
+
progress75: 5.0,
|
|
57
|
+
complete: 5.0,
|
|
58
|
+
pause: 1.0,
|
|
59
|
+
resume: 1.5,
|
|
60
|
+
seek: 0.5,
|
|
61
|
+
dropout: 2.5 // pausa final — não retomou até sair da página
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ── Configuração de Click Heatmap ───────────────────────────────────────────────
|
|
66
|
+
const CLICK_CONFIG = {
|
|
67
|
+
maxRapidClicks: 3,
|
|
68
|
+
rapidClickWindow: 1000, // 1 segundo
|
|
69
|
+
clickCategories: ['button', 'link', 'input', 'cta', 'price', 'generic'],
|
|
70
|
+
signalStrength: {
|
|
71
|
+
button: 2.0,
|
|
72
|
+
link: 1.5,
|
|
73
|
+
cta: 2.5,
|
|
74
|
+
input: 1.0,
|
|
75
|
+
price: 2.0,
|
|
76
|
+
generic: 0.5
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// ── Configuração de CTA Hover ───────────────────────────────────────────────────
|
|
81
|
+
const CTA_HOVER_CONFIG = {
|
|
82
|
+
minHoverTimeForSignal: 3000, // 3 segundos
|
|
83
|
+
maxHoverTime: 30000, // 30 segundos (para evitar overflow)
|
|
84
|
+
signalStrength: 3.0
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// ── Estado Global ──────────────────────────────────────────────────────────────
|
|
88
|
+
const state = {
|
|
89
|
+
pageLoadTime: Date.now(),
|
|
90
|
+
scrollTracking: {
|
|
91
|
+
lastScrollPercent: 0,
|
|
92
|
+
trackedThresholds: new Set()
|
|
93
|
+
},
|
|
94
|
+
timeTracking: {
|
|
95
|
+
lastCheckTime: Date.now(),
|
|
96
|
+
currentLevel: 'curioso',
|
|
97
|
+
timeOnPage: 0
|
|
98
|
+
},
|
|
99
|
+
// videoTracking: { [videoId]: { playTime, totalWatchTime, lastProgress, started, paused, dropoutPercent, dropoutSecond } }
|
|
100
|
+
videoTracking: {},
|
|
101
|
+
// Resumo agregado de todos os vídeos na página (para páginas VSL com múltiplos vídeos)
|
|
102
|
+
videoSummary: {
|
|
103
|
+
totalVideos: 0,
|
|
104
|
+
startedCount: 0,
|
|
105
|
+
completedCount: 0,
|
|
106
|
+
maxProgress: 0, // maior % atingida entre todos os vídeos
|
|
107
|
+
},
|
|
108
|
+
clickTracking: {
|
|
109
|
+
clicks: [],
|
|
110
|
+
lastClickTime: 0,
|
|
111
|
+
rapidClickDetected: false
|
|
112
|
+
},
|
|
113
|
+
ctaHoverTracking: {
|
|
114
|
+
hoverStartTime: null,
|
|
115
|
+
hoveredElement: null
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// ── Scroll Tracking ──────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Inicializa tracking de scroll
|
|
123
|
+
*/
|
|
124
|
+
function initScrollTracking() {
|
|
125
|
+
if (!isBrowser) return;
|
|
126
|
+
|
|
127
|
+
const observer = new IntersectionObserver((entries) => {
|
|
128
|
+
entries.forEach(entry => {
|
|
129
|
+
if (!entry.isIntersecting) return;
|
|
130
|
+
|
|
131
|
+
const scrollPercent = Math.round(
|
|
132
|
+
(window.scrollY + window.innerHeight) / document.body.scrollHeight * 100
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Track cada threshold apenas uma vez
|
|
136
|
+
SCROLL_CONFIG.thresholds.forEach(threshold => {
|
|
137
|
+
const key = `scroll_${threshold}`;
|
|
138
|
+
if (scrollPercent >= threshold && !state.scrollTracking.trackedThresholds.has(key)) {
|
|
139
|
+
const signal = SCROLL_CONFIG.signalStrength[`${threshold}%`] || 0;
|
|
140
|
+
|
|
141
|
+
dispatchEvent('Scroll', {
|
|
142
|
+
scroll_depth: threshold,
|
|
143
|
+
scroll_percent: scrollPercent,
|
|
144
|
+
signal_strength: signal,
|
|
145
|
+
engagement_type: 'scroll',
|
|
146
|
+
time_on_page: Math.round((Date.now() - state.pageLoadTime) / 1000)
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
state.scrollTracking.trackedThresholds.add(key);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}, { threshold: SCROLL_CONFIG.thresholds.map(t => t / 100) });
|
|
154
|
+
|
|
155
|
+
observer.observe(document.body);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Track evento de scroll
|
|
160
|
+
*/
|
|
161
|
+
function trackScroll(threshold, additionalData = {}) {
|
|
162
|
+
if (!isBrowser) return;
|
|
163
|
+
|
|
164
|
+
const scrollPercent = Math.round(
|
|
165
|
+
(window.scrollY + window.innerHeight) / document.body.scrollHeight * 100
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const signal = SCROLL_CONFIG.signalStrength[`${threshold}%`] || 0;
|
|
169
|
+
|
|
170
|
+
dispatchEvent('Scroll', {
|
|
171
|
+
scroll_depth: threshold,
|
|
172
|
+
scroll_percent: scrollPercent,
|
|
173
|
+
signal_strength: signal,
|
|
174
|
+
engagement_type: 'scroll',
|
|
175
|
+
time_on_page: Math.round((Date.now() - state.pageLoadTime) / 1000),
|
|
176
|
+
...additionalData
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Time on Page Tracking ───────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Inicializa tracking de tempo na página
|
|
184
|
+
*/
|
|
185
|
+
function initTimeOnPageTracking() {
|
|
186
|
+
if (!isBrowser) return;
|
|
187
|
+
|
|
188
|
+
// Check a cada 5 segundos
|
|
189
|
+
setInterval(() => {
|
|
190
|
+
const elapsed = Date.now() - state.pageLoadTime;
|
|
191
|
+
const timeOnPage = Math.round(elapsed / 1000);
|
|
192
|
+
|
|
193
|
+
// Verificar thresholds de intenção
|
|
194
|
+
let currentLevel = state.timeTracking.currentLevel;
|
|
195
|
+
let signal = state.timeTracking.currentLevel;
|
|
196
|
+
|
|
197
|
+
if (timeOnPage >= TIME_CONFIG.thresholds.comprador) {
|
|
198
|
+
currentLevel = 'comprador';
|
|
199
|
+
signal = TIME_CONFIG.signalStrength.comprador;
|
|
200
|
+
} else if (timeOnPage >= TIME_CONFIG.thresholds.interessado) {
|
|
201
|
+
currentLevel = 'interessado';
|
|
202
|
+
signal = TIME_CONFIG.signalStrength.interessado;
|
|
203
|
+
} else if (timeOnPage >= TIME_CONFIG.thresholds.curioso) {
|
|
204
|
+
currentLevel = 'curioso';
|
|
205
|
+
signal = TIME_CONFIG.signalStrength.curioso;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Se mudou de nível, disparar evento
|
|
209
|
+
if (currentLevel !== state.timeTracking.currentLevel) {
|
|
210
|
+
state.timeTracking.currentLevel = currentLevel;
|
|
211
|
+
|
|
212
|
+
dispatchEvent('TimeOnPage', {
|
|
213
|
+
time_on_page: timeOnPage,
|
|
214
|
+
intention_level: currentLevel,
|
|
215
|
+
signal_strength: signal,
|
|
216
|
+
engagement_type: 'time',
|
|
217
|
+
previous_level: state.timeTracking.previousLevel
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
state.timeTracking.previousLevel = currentLevel;
|
|
221
|
+
}
|
|
222
|
+
}, 5000);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Track evento de tempo na página
|
|
227
|
+
*/
|
|
228
|
+
function trackTimeOnPage(additionalData = {}) {
|
|
229
|
+
if (!isBrowser) return;
|
|
230
|
+
|
|
231
|
+
const elapsed = Date.now() - state.pageLoadTime;
|
|
232
|
+
const timeOnPage = Math.round(elapsed / 1000);
|
|
233
|
+
|
|
234
|
+
// Determinar nível de intenção
|
|
235
|
+
let intentionLevel;
|
|
236
|
+
let signal;
|
|
237
|
+
|
|
238
|
+
if (timeOnPage >= TIME_CONFIG.thresholds.comprador) {
|
|
239
|
+
intentionLevel = 'comprador';
|
|
240
|
+
signal = TIME_CONFIG.signalStrength.comprador;
|
|
241
|
+
} else if (timeOnPage >= TIME_CONFIG.thresholds.interessado) {
|
|
242
|
+
intentionLevel = 'interessado';
|
|
243
|
+
signal = TIME_CONFIG.signalStrength.interessado;
|
|
244
|
+
} else if (timeOnPage >= TIME_CONFIG.thresholds.curioso) {
|
|
245
|
+
intentionLevel = 'curioso';
|
|
246
|
+
signal = TIME_CONFIG.signalStrength.curioso;
|
|
247
|
+
} else {
|
|
248
|
+
intentionLevel = 'curioso';
|
|
249
|
+
signal = TIME_CONFIG.signalStrength.curioso;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
dispatchEvent('TimeOnPage', {
|
|
253
|
+
time_on_page: timeOnPage,
|
|
254
|
+
intention_level: intentionLevel,
|
|
255
|
+
signal_strength: signal,
|
|
256
|
+
engagement_type: 'time',
|
|
257
|
+
...additionalData
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Video Tracking ───────────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Cria o estado inicial de um vídeo
|
|
265
|
+
*/
|
|
266
|
+
function _createVideoState() {
|
|
267
|
+
return {
|
|
268
|
+
playTime: 0,
|
|
269
|
+
totalWatchTime: 0,
|
|
270
|
+
lastProgress: -1, // -1 = nenhum threshold disparado ainda (fix do bug original)
|
|
271
|
+
lastSecond: 0, // segundo exato do último timeupdate (para dropout heatmap)
|
|
272
|
+
started: false,
|
|
273
|
+
paused: false,
|
|
274
|
+
completed: false,
|
|
275
|
+
dropoutPercent: null, // % onde parou definitivamente
|
|
276
|
+
dropoutSecond: null, // segundo onde parou definitivamente
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Atualiza o resumo agregado de vídeos (VSL com múltiplos vídeos)
|
|
282
|
+
*/
|
|
283
|
+
function _updateVideoSummary(videoId) {
|
|
284
|
+
const s = state.videoTracking[videoId];
|
|
285
|
+
const sum = state.videoSummary;
|
|
286
|
+
|
|
287
|
+
sum.totalVideos = Object.keys(state.videoTracking).length;
|
|
288
|
+
sum.startedCount = Object.values(state.videoTracking).filter(v => v.started).length;
|
|
289
|
+
sum.completedCount = Object.values(state.videoTracking).filter(v => v.completed).length;
|
|
290
|
+
sum.maxProgress = Math.max(...Object.values(state.videoTracking).map(v => v.lastProgress < 0 ? 0 : v.lastProgress));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Registra dropout: momento exato em que o usuário parou de assistir
|
|
295
|
+
* Chamado no pause e no unload da página
|
|
296
|
+
*/
|
|
297
|
+
function _recordDropout(videoId, percent, second) {
|
|
298
|
+
const v = state.videoTracking[videoId];
|
|
299
|
+
if (!v || v.completed) return; // não registra dropout para vídeos completos
|
|
300
|
+
|
|
301
|
+
v.dropoutPercent = percent;
|
|
302
|
+
v.dropoutSecond = second;
|
|
303
|
+
|
|
304
|
+
if (VIDEO_CONFIG.dropoutEnabled) {
|
|
305
|
+
dispatchEvent('VideoDropout', {
|
|
306
|
+
video_id: videoId,
|
|
307
|
+
dropout_percent: percent,
|
|
308
|
+
dropout_second: second,
|
|
309
|
+
engagement_score: VIDEO_CONFIG.signalStrength.dropout,
|
|
310
|
+
engagement_type: 'video',
|
|
311
|
+
video_summary: { ...state.videoSummary }
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Inicializa tracking de vídeo
|
|
318
|
+
*/
|
|
319
|
+
function initVideoTracking() {
|
|
320
|
+
if (!isBrowser) return;
|
|
321
|
+
|
|
322
|
+
// Detectar vídeos na página
|
|
323
|
+
const videos = document.querySelectorAll('video, iframe[src*="youtube"], iframe[src*="vimeo"]');
|
|
324
|
+
|
|
325
|
+
videos.forEach((video, index) => {
|
|
326
|
+
const videoId = video.id || video.getAttribute('data-video-id') || `video_${index}`;
|
|
327
|
+
|
|
328
|
+
state.videoTracking[videoId] = _createVideoState();
|
|
329
|
+
_updateVideoSummary(videoId);
|
|
330
|
+
|
|
331
|
+
// YouTube tracking (iframe)
|
|
332
|
+
if (video.tagName === 'IFRAME' && video.src.includes('youtube')) {
|
|
333
|
+
initYouTubeTracking(video, videoId);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Vimeo tracking (iframe)
|
|
338
|
+
if (video.tagName === 'IFRAME' && video.src.includes('vimeo')) {
|
|
339
|
+
initVimeoTracking(video, videoId);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Video nativo HTML5
|
|
344
|
+
_attachHtml5Tracking(video, videoId);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Dropout no unload: registra onde cada vídeo estava quando o usuário saiu da página
|
|
348
|
+
window.addEventListener('beforeunload', () => {
|
|
349
|
+
Object.entries(state.videoTracking).forEach(([videoId, v]) => {
|
|
350
|
+
if (v.started && !v.completed) {
|
|
351
|
+
_recordDropout(videoId, v.lastProgress < 0 ? 0 : v.lastProgress, v.lastSecond);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Tracking HTML5 nativo
|
|
359
|
+
*/
|
|
360
|
+
function _attachHtml5Tracking(video, videoId) {
|
|
361
|
+
const v = () => state.videoTracking[videoId];
|
|
362
|
+
|
|
363
|
+
video.addEventListener('play', () => {
|
|
364
|
+
v().started = true;
|
|
365
|
+
v().paused = false;
|
|
366
|
+
if (!v().playTime) v().playTime = Date.now();
|
|
367
|
+
_updateVideoSummary(videoId);
|
|
368
|
+
|
|
369
|
+
dispatchEvent('VideoPlay', {
|
|
370
|
+
video_id: videoId,
|
|
371
|
+
video_platform: 'html5',
|
|
372
|
+
progress_percent: calculateVideoProgress(video),
|
|
373
|
+
engagement_score: VIDEO_CONFIG.signalStrength.play,
|
|
374
|
+
engagement_type: 'video'
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
video.addEventListener('pause', () => {
|
|
379
|
+
v().paused = true;
|
|
380
|
+
const progress = calculateVideoProgress(video);
|
|
381
|
+
const second = Math.round(video.currentTime);
|
|
382
|
+
|
|
383
|
+
dispatchEvent('VideoPause', {
|
|
384
|
+
video_id: videoId,
|
|
385
|
+
video_platform: 'html5',
|
|
386
|
+
progress_percent: progress,
|
|
387
|
+
pause_second: second,
|
|
388
|
+
engagement_score: VIDEO_CONFIG.signalStrength.pause,
|
|
389
|
+
engagement_type: 'video'
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Registra como dropout provisório — se o usuário retomar, será sobrescrito pelo próximo play
|
|
393
|
+
_recordDropout(videoId, progress, second);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
video.addEventListener('play', () => {
|
|
397
|
+
// Retomou — cancela o dropout provisório registrado no pause anterior
|
|
398
|
+
v().dropoutPercent = null;
|
|
399
|
+
v().dropoutSecond = null;
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
video.addEventListener('seeked', () => {
|
|
403
|
+
dispatchEvent('VideoSeek', {
|
|
404
|
+
video_id: videoId,
|
|
405
|
+
video_platform: 'html5',
|
|
406
|
+
progress_percent: calculateVideoProgress(video),
|
|
407
|
+
seek_to_second: Math.round(video.currentTime),
|
|
408
|
+
engagement_score: VIDEO_CONFIG.signalStrength.seek,
|
|
409
|
+
engagement_type: 'video'
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Monitorar progresso — BUG CORRIGIDO: lastProgress verificado antes de ser sobrescrito
|
|
414
|
+
video.addEventListener('timeupdate', () => {
|
|
415
|
+
if (video.paused || video.ended) return;
|
|
416
|
+
|
|
417
|
+
const progress = calculateVideoProgress(video);
|
|
418
|
+
const second = Math.round(video.currentTime);
|
|
419
|
+
v().lastSecond = second;
|
|
420
|
+
|
|
421
|
+
VIDEO_CONFIG.progressThresholds.forEach(threshold => {
|
|
422
|
+
// Verificação correta: comparar ANTES de sobrescrever lastProgress
|
|
423
|
+
if (progress >= threshold && v().lastProgress < threshold) {
|
|
424
|
+
v().lastProgress = threshold; // atualiza DEPOIS da comparação
|
|
425
|
+
|
|
426
|
+
dispatchEvent('VideoProgress', {
|
|
427
|
+
video_id: videoId,
|
|
428
|
+
video_platform: 'html5',
|
|
429
|
+
progress_percent: threshold,
|
|
430
|
+
progress_second: second,
|
|
431
|
+
engagement_score: VIDEO_CONFIG.signalStrength[`progress${threshold}`] || 0,
|
|
432
|
+
engagement_type: 'video',
|
|
433
|
+
video_summary: { ...state.videoSummary }
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
_updateVideoSummary(videoId);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
video.addEventListener('ended', () => {
|
|
442
|
+
v().paused = false;
|
|
443
|
+
v().completed = true;
|
|
444
|
+
v().dropoutPercent = null;
|
|
445
|
+
v().dropoutSecond = null;
|
|
446
|
+
const totalTime = v().playTime ? Math.round((Date.now() - v().playTime) / 1000) : 0;
|
|
447
|
+
_updateVideoSummary(videoId);
|
|
448
|
+
|
|
449
|
+
dispatchEvent('VideoComplete', {
|
|
450
|
+
video_id: videoId,
|
|
451
|
+
video_platform: 'html5',
|
|
452
|
+
progress_percent: 100,
|
|
453
|
+
total_watch_time: totalTime,
|
|
454
|
+
engagement_score: VIDEO_CONFIG.signalStrength.complete,
|
|
455
|
+
engagement_type: 'video',
|
|
456
|
+
video_summary: { ...state.videoSummary }
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Calcular progresso do vídeo (%)
|
|
463
|
+
*/
|
|
464
|
+
function calculateVideoProgress(video) {
|
|
465
|
+
if (!video.duration || video.duration === 0) return 0;
|
|
466
|
+
return Math.round((video.currentTime / video.duration) * 100);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* YouTube IFrame API — tracking real via postMessage
|
|
471
|
+
* Suporta: play, pause, progress (25/50/75/100), complete, dropout
|
|
472
|
+
*/
|
|
473
|
+
function initYouTubeTracking(iframe, videoId) {
|
|
474
|
+
// Garante que o iframe tenha enablejsapi=1 na URL
|
|
475
|
+
try {
|
|
476
|
+
const url = new URL(iframe.src);
|
|
477
|
+
if (!url.searchParams.get('enablejsapi')) {
|
|
478
|
+
url.searchParams.set('enablejsapi', '1');
|
|
479
|
+
iframe.src = url.toString();
|
|
480
|
+
}
|
|
481
|
+
} catch { /* URL inválida */ }
|
|
482
|
+
|
|
483
|
+
// Carrega a YouTube IFrame API uma vez
|
|
484
|
+
if (!window._ytApiLoading) {
|
|
485
|
+
window._ytApiLoading = true;
|
|
486
|
+
const tag = document.createElement('script');
|
|
487
|
+
tag.src = 'https://www.youtube.com/iframe_api';
|
|
488
|
+
document.head.appendChild(tag);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Aguarda a API estar pronta e inicializa o player
|
|
492
|
+
const initPlayer = () => {
|
|
493
|
+
if (typeof YT === 'undefined' || !YT.Player) {
|
|
494
|
+
setTimeout(initPlayer, 300);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const v = () => state.videoTracking[videoId];
|
|
499
|
+
let progressInterval = null;
|
|
500
|
+
|
|
501
|
+
new YT.Player(iframe, {
|
|
502
|
+
events: {
|
|
503
|
+
onStateChange: (event) => {
|
|
504
|
+
const YTState = YT.PlayerState;
|
|
505
|
+
const player = event.target;
|
|
506
|
+
const duration = player.getDuration() || 0;
|
|
507
|
+
const currentTime = player.getCurrentTime() || 0;
|
|
508
|
+
const progress = duration > 0 ? Math.round((currentTime / duration) * 100) : 0;
|
|
509
|
+
const second = Math.round(currentTime);
|
|
510
|
+
|
|
511
|
+
if (event.data === YTState.PLAYING) {
|
|
512
|
+
v().started = true;
|
|
513
|
+
v().paused = false;
|
|
514
|
+
if (!v().playTime) v().playTime = Date.now();
|
|
515
|
+
v().dropoutPercent = null;
|
|
516
|
+
v().dropoutSecond = null;
|
|
517
|
+
_updateVideoSummary(videoId);
|
|
518
|
+
|
|
519
|
+
dispatchEvent('VideoPlay', {
|
|
520
|
+
video_id: videoId,
|
|
521
|
+
video_platform: 'youtube',
|
|
522
|
+
progress_percent: progress,
|
|
523
|
+
engagement_score: VIDEO_CONFIG.signalStrength.play,
|
|
524
|
+
engagement_type: 'video'
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Polling de progresso a cada 2s (YT não tem timeupdate)
|
|
528
|
+
clearInterval(progressInterval);
|
|
529
|
+
progressInterval = setInterval(() => {
|
|
530
|
+
if (!player || typeof player.getPlayerState !== 'function') return;
|
|
531
|
+
if (player.getPlayerState() !== YTState.PLAYING) return;
|
|
532
|
+
|
|
533
|
+
const ct = player.getCurrentTime() || 0;
|
|
534
|
+
const dur = player.getDuration() || 0;
|
|
535
|
+
const pct = dur > 0 ? Math.round((ct / dur) * 100) : 0;
|
|
536
|
+
v().lastSecond = Math.round(ct);
|
|
537
|
+
|
|
538
|
+
VIDEO_CONFIG.progressThresholds.forEach(threshold => {
|
|
539
|
+
if (pct >= threshold && v().lastProgress < threshold) {
|
|
540
|
+
v().lastProgress = threshold;
|
|
541
|
+
|
|
542
|
+
dispatchEvent('VideoProgress', {
|
|
543
|
+
video_id: videoId,
|
|
544
|
+
video_platform: 'youtube',
|
|
545
|
+
progress_percent: threshold,
|
|
546
|
+
progress_second: Math.round(ct),
|
|
547
|
+
engagement_score: VIDEO_CONFIG.signalStrength[`progress${threshold}`] || 0,
|
|
548
|
+
engagement_type: 'video',
|
|
549
|
+
video_summary: { ...state.videoSummary }
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
_updateVideoSummary(videoId);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
}, 2000);
|
|
556
|
+
|
|
557
|
+
} else if (event.data === YTState.PAUSED) {
|
|
558
|
+
clearInterval(progressInterval);
|
|
559
|
+
v().paused = true;
|
|
560
|
+
|
|
561
|
+
dispatchEvent('VideoPause', {
|
|
562
|
+
video_id: videoId,
|
|
563
|
+
video_platform: 'youtube',
|
|
564
|
+
progress_percent: progress,
|
|
565
|
+
pause_second: second,
|
|
566
|
+
engagement_score: VIDEO_CONFIG.signalStrength.pause,
|
|
567
|
+
engagement_type: 'video'
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
_recordDropout(videoId, progress, second);
|
|
571
|
+
|
|
572
|
+
} else if (event.data === YTState.ENDED) {
|
|
573
|
+
clearInterval(progressInterval);
|
|
574
|
+
v().completed = true;
|
|
575
|
+
v().dropoutPercent = null;
|
|
576
|
+
v().dropoutSecond = null;
|
|
577
|
+
const totalTime = v().playTime ? Math.round((Date.now() - v().playTime) / 1000) : 0;
|
|
578
|
+
_updateVideoSummary(videoId);
|
|
579
|
+
|
|
580
|
+
dispatchEvent('VideoComplete', {
|
|
581
|
+
video_id: videoId,
|
|
582
|
+
video_platform: 'youtube',
|
|
583
|
+
progress_percent: 100,
|
|
584
|
+
total_watch_time: totalTime,
|
|
585
|
+
engagement_score: VIDEO_CONFIG.signalStrength.complete,
|
|
586
|
+
engagement_type: 'video',
|
|
587
|
+
video_summary: { ...state.videoSummary }
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// YT.onYouTubeIframeAPIReady pode já ter disparado
|
|
596
|
+
if (typeof YT !== 'undefined' && YT.Player) {
|
|
597
|
+
initPlayer();
|
|
598
|
+
} else {
|
|
599
|
+
const prev = window.onYouTubeIframeAPIReady;
|
|
600
|
+
window.onYouTubeIframeAPIReady = () => {
|
|
601
|
+
if (prev) prev();
|
|
602
|
+
initPlayer();
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Vimeo Player API — tracking via postMessage
|
|
609
|
+
* Suporta: play, pause, progress (25/50/75/100), complete, dropout
|
|
610
|
+
*/
|
|
611
|
+
function initVimeoTracking(iframe, videoId) {
|
|
612
|
+
const v = () => state.videoTracking[videoId];
|
|
613
|
+
let duration = 0;
|
|
614
|
+
|
|
615
|
+
const send = (method, value) => {
|
|
616
|
+
iframe.contentWindow.postMessage(JSON.stringify({ method, value }), 'https://player.vimeo.com');
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// Habilita listeners via postMessage
|
|
620
|
+
const enableListeners = () => {
|
|
621
|
+
['play', 'pause', 'ended', 'timeupdate', 'seeked'].forEach(evt => {
|
|
622
|
+
send('addEventListener', evt);
|
|
623
|
+
});
|
|
624
|
+
// Obtém duração
|
|
625
|
+
send('getDuration');
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
window.addEventListener('message', (event) => {
|
|
629
|
+
if (!event.origin.includes('vimeo.com')) return;
|
|
630
|
+
let data;
|
|
631
|
+
try { data = JSON.parse(event.data); } catch { return; }
|
|
632
|
+
if (!data || data.player_id !== iframe.id) return;
|
|
633
|
+
|
|
634
|
+
if (data.event === 'ready') {
|
|
635
|
+
enableListeners();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (data.method === 'getDuration') {
|
|
640
|
+
duration = data.value || 0;
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const currentTime = data.data?.seconds || 0;
|
|
645
|
+
const progress = duration > 0 ? Math.round((currentTime / duration) * 100) : 0;
|
|
646
|
+
const second = Math.round(currentTime);
|
|
647
|
+
|
|
648
|
+
if (data.event === 'play') {
|
|
649
|
+
v().started = true;
|
|
650
|
+
v().paused = false;
|
|
651
|
+
if (!v().playTime) v().playTime = Date.now();
|
|
652
|
+
v().dropoutPercent = null;
|
|
653
|
+
v().dropoutSecond = null;
|
|
654
|
+
_updateVideoSummary(videoId);
|
|
655
|
+
|
|
656
|
+
dispatchEvent('VideoPlay', {
|
|
657
|
+
video_id: videoId,
|
|
658
|
+
video_platform: 'vimeo',
|
|
659
|
+
progress_percent: progress,
|
|
660
|
+
engagement_score: VIDEO_CONFIG.signalStrength.play,
|
|
661
|
+
engagement_type: 'video'
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
} else if (data.event === 'pause') {
|
|
665
|
+
v().paused = true;
|
|
666
|
+
v().lastSecond = second;
|
|
667
|
+
|
|
668
|
+
dispatchEvent('VideoPause', {
|
|
669
|
+
video_id: videoId,
|
|
670
|
+
video_platform: 'vimeo',
|
|
671
|
+
progress_percent: progress,
|
|
672
|
+
pause_second: second,
|
|
673
|
+
engagement_score: VIDEO_CONFIG.signalStrength.pause,
|
|
674
|
+
engagement_type: 'video'
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
_recordDropout(videoId, progress, second);
|
|
678
|
+
|
|
679
|
+
} else if (data.event === 'timeupdate') {
|
|
680
|
+
v().lastSecond = second;
|
|
681
|
+
|
|
682
|
+
VIDEO_CONFIG.progressThresholds.forEach(threshold => {
|
|
683
|
+
if (progress >= threshold && v().lastProgress < threshold) {
|
|
684
|
+
v().lastProgress = threshold;
|
|
685
|
+
|
|
686
|
+
dispatchEvent('VideoProgress', {
|
|
687
|
+
video_id: videoId,
|
|
688
|
+
video_platform: 'vimeo',
|
|
689
|
+
progress_percent: threshold,
|
|
690
|
+
progress_second: second,
|
|
691
|
+
engagement_score: VIDEO_CONFIG.signalStrength[`progress${threshold}`] || 0,
|
|
692
|
+
engagement_type: 'video',
|
|
693
|
+
video_summary: { ...state.videoSummary }
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
_updateVideoSummary(videoId);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
} else if (data.event === 'ended') {
|
|
701
|
+
v().completed = true;
|
|
702
|
+
v().dropoutPercent = null;
|
|
703
|
+
v().dropoutSecond = null;
|
|
704
|
+
const totalTime = v().playTime ? Math.round((Date.now() - v().playTime) / 1000) : 0;
|
|
705
|
+
_updateVideoSummary(videoId);
|
|
706
|
+
|
|
707
|
+
dispatchEvent('VideoComplete', {
|
|
708
|
+
video_id: videoId,
|
|
709
|
+
video_platform: 'vimeo',
|
|
710
|
+
progress_percent: 100,
|
|
711
|
+
total_watch_time: totalTime,
|
|
712
|
+
engagement_score: VIDEO_CONFIG.signalStrength.complete,
|
|
713
|
+
engagement_type: 'video',
|
|
714
|
+
video_summary: { ...state.videoSummary }
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
} else if (data.event === 'seeked') {
|
|
718
|
+
dispatchEvent('VideoSeek', {
|
|
719
|
+
video_id: videoId,
|
|
720
|
+
video_platform: 'vimeo',
|
|
721
|
+
progress_percent: progress,
|
|
722
|
+
seek_to_second: second,
|
|
723
|
+
engagement_score: VIDEO_CONFIG.signalStrength.seek,
|
|
724
|
+
engagement_type: 'video'
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
// Inicializa o player Vimeo — postMessage após load
|
|
730
|
+
if (iframe.contentDocument) {
|
|
731
|
+
enableListeners();
|
|
732
|
+
} else {
|
|
733
|
+
iframe.addEventListener('load', enableListeners);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Track evento de vídeo manual (chamada externa)
|
|
739
|
+
*/
|
|
740
|
+
function trackVideo(event, additionalData = {}) {
|
|
741
|
+
if (!isBrowser) return;
|
|
742
|
+
|
|
743
|
+
dispatchEvent(event, {
|
|
744
|
+
...additionalData,
|
|
745
|
+
engagement_type: 'video'
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Retorna o resumo agregado de todos os vídeos na página
|
|
751
|
+
* Útil para páginas VSL com múltiplos vídeos
|
|
752
|
+
*/
|
|
753
|
+
function getVideoSummary() {
|
|
754
|
+
return { ...state.videoSummary };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ── Click Heatmap Tracking ─────────────────────────────────────────────────────
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Inicializa tracking de clique
|
|
761
|
+
*/
|
|
762
|
+
function initClickHeatmapTracking() {
|
|
763
|
+
if (!isBrowser) return;
|
|
764
|
+
|
|
765
|
+
document.addEventListener('click', (e) => {
|
|
766
|
+
const target = e.target;
|
|
767
|
+
const clickCategory = categorizeClick(target);
|
|
768
|
+
const position = calculateClickPosition(e);
|
|
769
|
+
const timeOnPage = Math.round((Date.now() - state.pageLoadTime) / 1000);
|
|
770
|
+
|
|
771
|
+
// Track rapid clicks
|
|
772
|
+
const now = Date.now();
|
|
773
|
+
const timeSinceLastClick = now - state.clickTracking.lastClickTime;
|
|
774
|
+
|
|
775
|
+
if (timeSinceLastClick < CLICK_CONFIG.rapidClickWindow) {
|
|
776
|
+
state.clickTracking.clicks.push({
|
|
777
|
+
time: now,
|
|
778
|
+
target: target
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Reset rapid clicks após window
|
|
783
|
+
setTimeout(() => {
|
|
784
|
+
const recentClicks = state.clickTracking.clicks.filter(
|
|
785
|
+
c => now - c.time < CLICK_CONFIG.rapidClickWindow
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
if (recentClicks.length >= CLICK_CONFIG.maxRapidClicks) {
|
|
789
|
+
state.clickTracking.rapidClickDetected = true;
|
|
790
|
+
|
|
791
|
+
dispatchEvent('RapidClicks', {
|
|
792
|
+
click_count: recentClicks.length,
|
|
793
|
+
time_window: CLICK_CONFIG.rapidClickWindow,
|
|
794
|
+
behavior: 'nervous',
|
|
795
|
+
engagement_score: 3.0,
|
|
796
|
+
engagement_type: 'clicks',
|
|
797
|
+
time_on_page: timeOnPage,
|
|
798
|
+
description: `Usuário clicou ${recentClicks.length} vezes em ${CLICK_CONFIG.rapidClickWindow / 1000} segundos`
|
|
799
|
+
});
|
|
800
|
+
} else {
|
|
801
|
+
state.clickTracking.rapidClickDetected = false;
|
|
802
|
+
state.clickTracking.clicks = [];
|
|
803
|
+
}
|
|
804
|
+
}, CLICK_CONFIG.rapidClickWindow);
|
|
805
|
+
|
|
806
|
+
state.clickTracking.lastClickTime = now;
|
|
807
|
+
|
|
808
|
+
dispatchEvent('Click', {
|
|
809
|
+
click_category: clickCategory,
|
|
810
|
+
target_info: extractTargetInfo(target),
|
|
811
|
+
position: position,
|
|
812
|
+
time_on_page: timeOnPage,
|
|
813
|
+
click_heatmap: true,
|
|
814
|
+
engagement_score: calculateClickEngagementScore(clickCategory, position),
|
|
815
|
+
engagement_type: 'clicks'
|
|
816
|
+
});
|
|
817
|
+
}, true); // Capture para elementos dinâmicos
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Categorizar o clique
|
|
822
|
+
*/
|
|
823
|
+
function categorizeClick(target) {
|
|
824
|
+
const closest = (selector) => target.closest(selector);
|
|
825
|
+
const className = target.className || '';
|
|
826
|
+
|
|
827
|
+
if (closest('button') || className.includes('btn')) return 'button';
|
|
828
|
+
if (closest('a')) return 'link';
|
|
829
|
+
if (closest('input')) return 'input';
|
|
830
|
+
if (closest('.cta') || className.includes('cta')) return 'cta';
|
|
831
|
+
if (closest('.price') || className.includes('price')) return 'price';
|
|
832
|
+
if (closest('.whatsapp') || className.includes('whatsapp')) return 'whatsapp';
|
|
833
|
+
if (closest('.email') || className.includes('email')) return 'email';
|
|
834
|
+
if (closest('.phone') || className.includes('phone')) return 'phone';
|
|
835
|
+
|
|
836
|
+
return 'generic';
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Calcular posição do clique
|
|
841
|
+
*/
|
|
842
|
+
function calculateClickPosition(e) {
|
|
843
|
+
return {
|
|
844
|
+
x: Math.round(e.clientX),
|
|
845
|
+
y: Math.round(e.clientY),
|
|
846
|
+
relativeX: Math.round(e.clientX / window.innerWidth * 100),
|
|
847
|
+
relativeY: Math.round(e.clientY / window.innerHeight * 100),
|
|
848
|
+
viewportWidth: window.innerWidth,
|
|
849
|
+
viewportHeight: window.innerHeight
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Extrair informações do elemento alvo
|
|
855
|
+
*/
|
|
856
|
+
function extractTargetInfo(target) {
|
|
857
|
+
return {
|
|
858
|
+
tag: target.tagName,
|
|
859
|
+
class: target.className,
|
|
860
|
+
id: target.id,
|
|
861
|
+
text: target.textContent?.substring(0, 30) || '',
|
|
862
|
+
attributes: Array.from(target.attributes).map(a => `${a.name}="${a.value}"`).join(' ')
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Calcular score de engajamento para clique
|
|
868
|
+
*/
|
|
869
|
+
function calculateClickEngagementScore(clickCategory, position) {
|
|
870
|
+
const baseScore = CLICK_CONFIG.signalStrength[clickCategory] || 0.5;
|
|
871
|
+
const timeScore = Math.min(position.timeOnPage / 60, 2.0); // Até 2 minutos aumenta score
|
|
872
|
+
const depthBonus = Math.min(position.scrollDepth / 100, 0.5); // Até 100% scroll aumenta score
|
|
873
|
+
|
|
874
|
+
return baseScore + timeScore + depthBonus;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// ── CTA Hover Tracking ─────────────────────────────────────────────────────────
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Inicializa tracking de hover no CTA
|
|
881
|
+
*/
|
|
882
|
+
function initCTAHoverTracking() {
|
|
883
|
+
if (!isBrowser) return;
|
|
884
|
+
|
|
885
|
+
const ctaSelectors = [
|
|
886
|
+
'a[href*="checkout"]',
|
|
887
|
+
'a[href*="comprar"]',
|
|
888
|
+
'button.cta',
|
|
889
|
+
'.btn-comprar',
|
|
890
|
+
'.btn-checkout',
|
|
891
|
+
'.cta-button',
|
|
892
|
+
'[data-cta="true"]',
|
|
893
|
+
'button[type="submit"]'
|
|
894
|
+
];
|
|
895
|
+
|
|
896
|
+
ctaSelectors.forEach(selector => {
|
|
897
|
+
document.querySelectorAll(selector).forEach(element => {
|
|
898
|
+
element.addEventListener('mouseenter', (e) => {
|
|
899
|
+
const ctaElement = e.target.closest(selector);
|
|
900
|
+
if (!ctaElement) return;
|
|
901
|
+
|
|
902
|
+
state.ctaHoverTracking.hoverStartTime = Date.now();
|
|
903
|
+
state.ctaHoverTracking.hoveredElement = selector;
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
element.addEventListener('mouseleave', () => {
|
|
907
|
+
if (state.ctaHoverTracking.hoverStartTime) {
|
|
908
|
+
const hoverTime = Date.now() - state.ctaHoverTracking.hoverStartTime;
|
|
909
|
+
|
|
910
|
+
if (hoverTime >= CTA_HOVER_CONFIG.minHoverTimeForSignal) {
|
|
911
|
+
dispatchEvent('CTAHover', {
|
|
912
|
+
cta_selector: selector,
|
|
913
|
+
hover_time_seconds: Math.round(hoverTime / 1000),
|
|
914
|
+
engagement_score: CTA_HOVER_CONFIG.signalStrength,
|
|
915
|
+
engagement_type: 'hover',
|
|
916
|
+
time_on_page: Math.round((Date.now() - state.pageLoadTime) / 1000)
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
state.ctaHoverTracking.hoverStartTime = null;
|
|
922
|
+
state.ctaHoverTracking.hoveredElement = null;
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Track evento de hover no CTA
|
|
930
|
+
*/
|
|
931
|
+
function trackCTAHover(selector, additionalData = {}) {
|
|
932
|
+
if (!isBrowser) return;
|
|
933
|
+
|
|
934
|
+
dispatchEvent('CTAHover', {
|
|
935
|
+
cta_selector: selector,
|
|
936
|
+
engagement_type: 'hover',
|
|
937
|
+
...additionalData
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// ── Dispatcher Geral ─────────────────────────────────────────────────────────────
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Dispatch evento para o tracking principal (cdpTrack)
|
|
945
|
+
*/
|
|
946
|
+
function dispatchEvent(eventName, data = {}) {
|
|
947
|
+
// Verifica se cdpTrack existe
|
|
948
|
+
if (typeof cdpTrack !== 'undefined' && cdpTrack.track) {
|
|
949
|
+
cdpTrack.track(eventName, data);
|
|
950
|
+
} else {
|
|
951
|
+
console.warn('cdpTrack não disponível - evento não enviado:', eventName, data);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// ── Inicialização Principal ─────────────────────────────────────────────────────
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Inicializa todos os micro-events
|
|
959
|
+
*/
|
|
960
|
+
function initMicroEvents() {
|
|
961
|
+
if (!isBrowser) return;
|
|
962
|
+
|
|
963
|
+
state.pageLoadTime = Date.now();
|
|
964
|
+
|
|
965
|
+
// Inicializar tracking de scroll
|
|
966
|
+
initScrollTracking();
|
|
967
|
+
|
|
968
|
+
// Inicializar tracking de tempo na página
|
|
969
|
+
initTimeOnPageTracking();
|
|
970
|
+
|
|
971
|
+
// Inicializar tracking de vídeo
|
|
972
|
+
initVideoTracking();
|
|
973
|
+
|
|
974
|
+
// Inicializar tracking de clique
|
|
975
|
+
initClickHeatmapTracking();
|
|
976
|
+
|
|
977
|
+
// Inicializar tracking de hover no CTA
|
|
978
|
+
initCTAHoverTracking();
|
|
979
|
+
|
|
980
|
+
console.log('✅ Micro-Events inicializados');
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// ── Exportações ─────────────────────────────────────────────────────────────────
|
|
984
|
+
|
|
985
|
+
export {
|
|
986
|
+
initMicroEvents,
|
|
987
|
+
trackScroll,
|
|
988
|
+
trackTimeOnPage,
|
|
989
|
+
trackVideo,
|
|
990
|
+
trackCTAHover,
|
|
991
|
+
getVideoSummary
|
|
992
|
+
};
|