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.
Files changed (128) hide show
  1. package/README.md +367 -0
  2. package/bin/cdp-edge.js +61 -0
  3. package/contracts/api-versions.json +368 -0
  4. package/dist/commands/analyze.js +52 -0
  5. package/dist/commands/infra.js +54 -0
  6. package/dist/commands/install.js +168 -0
  7. package/dist/commands/server.js +174 -0
  8. package/dist/commands/setup.js +123 -0
  9. package/dist/commands/validate.js +84 -0
  10. package/dist/index.js +12 -0
  11. package/docs/CI-CD-SETUP.md +217 -0
  12. package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
  13. package/docs/events-reference.md +359 -0
  14. package/docs/installation.md +155 -0
  15. package/docs/quick-start.md +185 -0
  16. package/docs/sdk-reference.md +371 -0
  17. package/docs/whatsapp-ctwa.md +209 -0
  18. package/extracted-skill/tracking-events-generator/INDEX.md +94 -0
  19. package/extracted-skill/tracking-events-generator/INSTALACAO-CDPEDGE.md +58 -0
  20. package/extracted-skill/tracking-events-generator/INTEGRACAO-COMPLETA.md +594 -0
  21. package/extracted-skill/tracking-events-generator/MELHORIAS-IMPLEMENTADAS.md +412 -0
  22. package/extracted-skill/tracking-events-generator/Premium-Tracking-Intelligence-Resumo.md +333 -0
  23. package/extracted-skill/tracking-events-generator/SKILL.md +257 -0
  24. package/extracted-skill/tracking-events-generator/advanced-matching.js +364 -0
  25. package/extracted-skill/tracking-events-generator/agents/ab-testing-agent.md +54 -0
  26. package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +1304 -0
  27. package/extracted-skill/tracking-events-generator/agents/bing-agent.md +76 -0
  28. package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +264 -0
  29. package/extracted-skill/tracking-events-generator/agents/code-guardian-agent.md +149 -0
  30. package/extracted-skill/tracking-events-generator/agents/compliance-agent.md +2077 -0
  31. package/extracted-skill/tracking-events-generator/agents/crm-integration-agent.md +1419 -0
  32. package/extracted-skill/tracking-events-generator/agents/dashboard-agent.md +456 -0
  33. package/extracted-skill/tracking-events-generator/agents/database-agent.md +667 -0
  34. package/extracted-skill/tracking-events-generator/agents/debug-agent.md +1455 -0
  35. package/extracted-skill/tracking-events-generator/agents/domain-setup-agent.md +224 -0
  36. package/extracted-skill/tracking-events-generator/agents/email-agent.md +61 -0
  37. package/extracted-skill/tracking-events-generator/agents/fingerprint-agent.md +52 -0
  38. package/extracted-skill/tracking-events-generator/agents/google-agent.md +109 -0
  39. package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +365 -0
  40. package/extracted-skill/tracking-events-generator/agents/intelligence-scheduling.md +643 -0
  41. package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +62 -0
  42. package/extracted-skill/tracking-events-generator/agents/localization-agent.md +55 -0
  43. package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +59 -0
  44. package/extracted-skill/tracking-events-generator/agents/master-feedback-loop.md +900 -0
  45. package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +1922 -0
  46. package/extracted-skill/tracking-events-generator/agents/memory-agent.json +109 -0
  47. package/extracted-skill/tracking-events-generator/agents/memory-agent.md +703 -0
  48. package/extracted-skill/tracking-events-generator/agents/meta-agent.md +110 -0
  49. package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +255 -0
  50. package/extracted-skill/tracking-events-generator/agents/performance-agent.md +1157 -0
  51. package/extracted-skill/tracking-events-generator/agents/performance-optimization-agent.md +1432 -0
  52. package/extracted-skill/tracking-events-generator/agents/pinterest-agent.md +310 -0
  53. package/extracted-skill/tracking-events-generator/agents/premium-tracking-intelligence-agent.md +849 -0
  54. package/extracted-skill/tracking-events-generator/agents/r2-setup-agent.md +250 -0
  55. package/extracted-skill/tracking-events-generator/agents/reddit-agent.md +313 -0
  56. package/extracted-skill/tracking-events-generator/agents/security-enterprise-agent.md +1752 -0
  57. package/extracted-skill/tracking-events-generator/agents/server-tracking.md +1188 -0
  58. package/extracted-skill/tracking-events-generator/agents/spotify-agent.md +383 -0
  59. package/extracted-skill/tracking-events-generator/agents/tiktok-agent.md +111 -0
  60. package/extracted-skill/tracking-events-generator/agents/tracking-plan-agent.md +364 -0
  61. package/extracted-skill/tracking-events-generator/agents/validator-agent.md +267 -0
  62. package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +69 -0
  63. package/extracted-skill/tracking-events-generator/agents/whatsapp-agent.md +76 -0
  64. package/extracted-skill/tracking-events-generator/agents/whatsapp-ctwa-setup-agent.md +699 -0
  65. package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +422 -0
  66. package/extracted-skill/tracking-events-generator/anti-blocking.js +285 -0
  67. package/extracted-skill/tracking-events-generator/cdpTrack.js +641 -0
  68. package/extracted-skill/tracking-events-generator/contracts/api-versions.json +368 -0
  69. package/extracted-skill/tracking-events-generator/docs/guia-cloudflare-iniciante.md +107 -0
  70. package/extracted-skill/tracking-events-generator/engagement-scoring.js +226 -0
  71. package/extracted-skill/tracking-events-generator/evals/evals.json +235 -0
  72. package/extracted-skill/tracking-events-generator/integration-test.js +497 -0
  73. package/extracted-skill/tracking-events-generator/knowledge-base.md +2894 -0
  74. package/extracted-skill/tracking-events-generator/micro-events.js +992 -0
  75. package/extracted-skill/tracking-events-generator/models/captura-de-lead.md +78 -0
  76. package/extracted-skill/tracking-events-generator/models/captura-lead-evento-externo.md +99 -0
  77. package/extracted-skill/tracking-events-generator/models/checkout-proprio.md +111 -0
  78. package/extracted-skill/tracking-events-generator/models/multi-step-checkout.md +672 -0
  79. package/extracted-skill/tracking-events-generator/models/pagina-obrigado.md +55 -0
  80. package/extracted-skill/tracking-events-generator/models/pinterest/conversions-api-template.js +144 -0
  81. package/extracted-skill/tracking-events-generator/models/pinterest/event-mappings.json +48 -0
  82. package/extracted-skill/tracking-events-generator/models/pinterest/tag-template.js +28 -0
  83. package/extracted-skill/tracking-events-generator/models/quiz-funnel.md +68 -0
  84. package/extracted-skill/tracking-events-generator/models/reddit/conversions-api-template.js +205 -0
  85. package/extracted-skill/tracking-events-generator/models/reddit/event-mappings.json +56 -0
  86. package/extracted-skill/tracking-events-generator/models/reddit/pixel-template.js +19 -0
  87. package/extracted-skill/tracking-events-generator/models/scenarios/behavior-engine.js +425 -0
  88. package/extracted-skill/tracking-events-generator/models/scenarios/real-estate-logic.md +50 -0
  89. package/extracted-skill/tracking-events-generator/models/scenarios/sales-page-logic.md +50 -0
  90. package/extracted-skill/tracking-events-generator/models/trafego-direto.md +582 -0
  91. package/extracted-skill/tracking-events-generator/models/webinar-registration.md +63 -0
  92. package/extracted-skill/tracking-events-generator/tracking.config.js +46 -0
  93. package/extracted-skill/tracking-events-generator/walkthrough.md +26 -0
  94. package/package.json +75 -0
  95. package/server-edge-tracker/INSTALAR.md +328 -0
  96. package/server-edge-tracker/migrate-new-db.sql +137 -0
  97. package/server-edge-tracker/migrate-v2.sql +16 -0
  98. package/server-edge-tracker/migrate-v3.sql +6 -0
  99. package/server-edge-tracker/migrate-v4.sql +18 -0
  100. package/server-edge-tracker/migrate-v5.sql +17 -0
  101. package/server-edge-tracker/migrate-v6.sql +24 -0
  102. package/server-edge-tracker/migrate.sql +111 -0
  103. package/server-edge-tracker/schema.sql +265 -0
  104. package/server-edge-tracker/worker.js +2574 -0
  105. package/server-edge-tracker/wrangler.toml +85 -0
  106. package/templates/afiliado-sem-landing.md +312 -0
  107. package/templates/captura-de-lead.md +78 -0
  108. package/templates/captura-lead-evento-externo.md +99 -0
  109. package/templates/checkout-proprio.md +111 -0
  110. package/templates/install/.claude/commands/cdp.md +1 -0
  111. package/templates/install/CLAUDE.md +65 -0
  112. package/templates/linkedin/tag-template.js +46 -0
  113. package/templates/multi-step-checkout.md +673 -0
  114. package/templates/pagina-obrigado.md +55 -0
  115. package/templates/pinterest/conversions-api-template.js +144 -0
  116. package/templates/pinterest/event-mappings.json +48 -0
  117. package/templates/pinterest/tag-template.js +28 -0
  118. package/templates/quiz-funnel.md +68 -0
  119. package/templates/reddit/conversions-api-template.js +205 -0
  120. package/templates/reddit/event-mappings.json +56 -0
  121. package/templates/reddit/pixel-template.js +46 -0
  122. package/templates/scenarios/behavior-engine.js +402 -0
  123. package/templates/scenarios/real-estate-logic.md +50 -0
  124. package/templates/scenarios/sales-page-logic.md +50 -0
  125. package/templates/spotify/pixel-template.js +46 -0
  126. package/templates/trafego-direto.md +582 -0
  127. package/templates/vsl-page.md +292 -0
  128. 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
+ };