cdp-edge 1.18.0 → 2.0.1

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 (40) hide show
  1. package/README.md +308 -308
  2. package/bin/cdp-edge.js +61 -61
  3. package/dist/commands/analyze.js +52 -52
  4. package/dist/commands/infra.js +54 -54
  5. package/dist/commands/install.js +186 -0
  6. package/dist/commands/server.js +174 -174
  7. package/dist/commands/setup.js +18 -1
  8. package/dist/commands/validate.js +84 -84
  9. package/dist/index.js +12 -12
  10. package/extracted-skill/tracking-events-generator/advanced-matching.js +364 -364
  11. package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +172 -72
  12. package/extracted-skill/tracking-events-generator/agents/google-agent.md +118 -0
  13. package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +86 -0
  14. package/extracted-skill/tracking-events-generator/agents/intelligence-scheduling.md +8 -641
  15. package/extracted-skill/tracking-events-generator/agents/memory-agent.md +98 -0
  16. package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +42 -0
  17. package/extracted-skill/tracking-events-generator/anti-blocking.js +285 -285
  18. package/extracted-skill/tracking-events-generator/cdpTrack.js +641 -641
  19. package/extracted-skill/tracking-events-generator/engagement-scoring.js +226 -226
  20. package/extracted-skill/tracking-events-generator/evals/evals.json +235 -235
  21. package/extracted-skill/tracking-events-generator/integration-test.js +497 -497
  22. package/extracted-skill/tracking-events-generator/micro-events.js +992 -992
  23. package/extracted-skill/tracking-events-generator/models/pinterest/conversions-api-template.js +144 -144
  24. package/extracted-skill/tracking-events-generator/models/pinterest/event-mappings.json +48 -48
  25. package/extracted-skill/tracking-events-generator/models/pinterest/tag-template.js +28 -28
  26. package/extracted-skill/tracking-events-generator/models/reddit/conversions-api-template.js +205 -205
  27. package/extracted-skill/tracking-events-generator/models/reddit/event-mappings.json +56 -56
  28. package/extracted-skill/tracking-events-generator/models/reddit/pixel-template.js +19 -19
  29. package/extracted-skill/tracking-events-generator/models/scenarios/behavior-engine.js +425 -425
  30. package/package.json +76 -76
  31. package/server-edge-tracker/schema.sql +265 -265
  32. package/server-edge-tracker/worker.js +4160 -4160
  33. package/server-edge-tracker/wrangler.toml +103 -103
  34. package/templates/pinterest/conversions-api-template.js +144 -144
  35. package/templates/pinterest/event-mappings.json +48 -48
  36. package/templates/pinterest/tag-template.js +28 -28
  37. package/templates/reddit/conversions-api-template.js +205 -205
  38. package/templates/reddit/event-mappings.json +56 -56
  39. package/templates/reddit/pixel-template.js +19 -19
  40. package/templates/scenarios/behavior-engine.js +425 -425
@@ -1,425 +1,425 @@
1
- /**
2
- * CDP Edge Human-Behavior Engine (Quantum Tier)
3
- * Responsável por capturar micro-interações e intenção real.
4
- */
5
-
6
- const BehaviorEngine = {
7
- config: {
8
- rageClickThreshold: 3,
9
- rageClickTime: 700, // ms
10
- idleThreshold: 60000, // 60s
11
- scoreThresholds: {
12
- engaged: 40,
13
- highIntent: 80
14
- }
15
- },
16
-
17
- state: {
18
- clickHistory: [],
19
- startTime: Date.now(),
20
- lastActivity: Date.now(),
21
- lastPulse: Date.now(),
22
- isIdle: false,
23
- formStartTime: {},
24
- userScore: 0,
25
- firedScoreEvents: new Set(),
26
- abVariant: null,
27
- },
28
-
29
- init() {
30
- this.setupRageClicks();
31
- this.setupVisibility();
32
- this.setupHeartbeat();
33
- this.setupScroll();
34
- this.setupVideoTracking();
35
- this.setupFormAnalytics();
36
- this.setupFormAbandonment();
37
- this.setupCopyPaste();
38
- this.setupExitIntent();
39
- this.setupOutboundLinks();
40
- this.setupErrorTracking();
41
- this.setupIdleDetection();
42
- this.setupScoring();
43
- this.setupABTesting();
44
- console.log('[CDP Edge] Enterprise Behavior Engine Initialized');
45
- },
46
-
47
- // 0.1 A/B Testing (Mode 1 - Edge Sync)
48
- setupABTesting() {
49
- // Busca variante no Cookie ou URL (Modo 1)
50
- const getCookie = (name) => {
51
- const value = `; ${document.cookie}`;
52
- const parts = value.split(`; ${name}=`);
53
- if (parts.length === 2) return parts.pop().split(';').shift();
54
- };
55
-
56
- const urlParams = new URLSearchParams(window.location.search);
57
- this.state.abVariant = urlParams.get('cdp_variant') || getCookie('cdp_ab_variant') || 'original';
58
-
59
- console.log(`[CDP Edge] A/B Variant Detected: ${this.state.abVariant}`);
60
-
61
- // Intercepta o cdpTrack.track para injetar a variante globalmente
62
- const originalTrack = cdpTrack.track;
63
- cdpTrack.track = (eventName, eventParams = {}) => {
64
- const enrichedParams = {
65
- ...eventParams,
66
- ab_test_variant: this.state.abVariant,
67
- user_score: this.state.userScore
68
- };
69
- return originalTrack.apply(cdpTrack, [eventName, enrichedParams]);
70
- };
71
- },
72
-
73
- // 0. Scoring Engine (Internal)
74
- setupScoring() {
75
- console.log('[CDP Edge] Scoring Engine Online');
76
- },
77
-
78
- addScore(points, reason) {
79
- this.state.userScore = Math.min(100, Math.max(0, this.state.userScore + points));
80
- console.log(`[CDP Edge] Score Update: +${points} (${reason}) | Total: ${this.state.userScore}`);
81
-
82
- // Gatilhos de Plataforma
83
- if (this.state.userScore >= this.config.scoreThresholds.highIntent && !this.state.firedScoreEvents.has('highIntent')) {
84
- this.state.firedScoreEvents.add('highIntent');
85
- cdpTrack.track('High_Intent_Lead', { score: this.state.userScore, meta_intensity: 'high' });
86
- } else if (this.state.userScore >= this.config.scoreThresholds.engaged && !this.state.firedScoreEvents.has('engaged')) {
87
- this.state.firedScoreEvents.add('engaged');
88
- cdpTrack.track('Engaged_User', { score: this.state.userScore, meta_intensity: 'medium' });
89
- }
90
- },
91
-
92
- // 1. Rage Click Detector
93
- setupRageClicks() {
94
- document.addEventListener('click', (e) => {
95
- const now = Date.now();
96
- this.state.clickHistory.push(now);
97
-
98
- // Limpa histórico antigo
99
- this.state.clickHistory = this.state.clickHistory.filter(t => now - t < this.config.rageClickTime);
100
-
101
- if (this.state.clickHistory.length >= this.config.rageClickThreshold) {
102
- cdpTrack.track('rage_click', {
103
- element_id: e.target.id || '',
104
- element_class: e.target.className || '',
105
- x: e.pageX,
106
- y: e.pageY,
107
- meta_intensity: 'low' // Envia sinal leve para Meta
108
- });
109
- this.addScore(-10, 'rage_click');
110
- this.state.clickHistory = []; // Reseta após detecção
111
- }
112
- });
113
- },
114
-
115
- // 1.2 Scroll Depth (Quantum Tier)
116
- setupScroll() {
117
- const markers = [25, 50, 75, 90];
118
- const fired = new Set();
119
-
120
- window.addEventListener('scroll', () => {
121
- const scrollPct = Math.round((window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100);
122
-
123
- markers.forEach(m => {
124
- if (scrollPct >= m && !fired.has(m)) {
125
- fired.add(m);
126
- this.addScore(m === 25 ? 5 : (m === 50 ? 5 : 10), `scroll_${m}%`);
127
- cdpTrack.track('scroll_depth', {
128
- percent: m,
129
- meta_intensity: m >= 50 ? 'medium' : 'low' // Envia como sinal mais forte após 50%
130
- });
131
- }
132
- });
133
- }, { passive: true });
134
- },
135
-
136
- // 2. Tab Visibility (VSL Focus)
137
- setupVisibility() {
138
- document.addEventListener('visibilitychange', () => {
139
- const status = document.visibilityState; // 'visible' ou 'hidden'
140
- cdpTrack.track('tab_visibility_change', {
141
- status: status,
142
- time_since_start: Math.floor((Date.now() - this.state.startTime) / 1000),
143
- meta_intensity: status === 'visible' ? 'high' : 'low'
144
- });
145
- });
146
- },
147
-
148
- // 3. Click Heatmap (D1 Only)
149
- setupHeatmap() {
150
- document.addEventListener('click', (e) => {
151
- // Enviamos apenas para o servidor/D1 para não poluir o pixel do cliente
152
- // sendServerEvent é o método interno que pula o despacho para Meta/TikTok
153
- if (typeof cdpTrack.sendServerEvent === 'function') {
154
- cdpTrack.sendServerEvent('click_heatmap', null, {
155
- x: e.pageX,
156
- y: e.pageY,
157
- element: e.target.tagName.toLowerCase(),
158
- id: e.target.id || '',
159
- classes: e.target.className || ''
160
- });
161
- }
162
- });
163
- },
164
-
165
- // 4. Retention Pulse (Heartbeat)
166
- setupHeartbeat() {
167
- setInterval(() => {
168
- const activeTime = Math.floor((Date.now() - this.state.startTime) / 1000);
169
- cdpTrack.track('pulse_heartbeat', {
170
- duration_seconds: activeTime,
171
- is_visible: document.visibilityState === 'visible'
172
- });
173
- }, this.config.heartbeatInterval);
174
- },
175
-
176
- // 5. VSL / Video Tracking (YouTube & Vimeo)
177
- setupVideoTracking() {
178
- const iframes = document.querySelectorAll('iframe');
179
- const fired = new Set();
180
-
181
- iframes.forEach(iframe => {
182
- const src = iframe.src || '';
183
- const isYT = src.includes('youtube.com/embed');
184
- const isVimeo = src.includes('vimeo.com/video');
185
-
186
- if (isYT || isVimeo) {
187
- // Observer de progresso (via postMessage para evitar dependência de SDK pesado)
188
- window.addEventListener('message', (event) => {
189
- try {
190
- const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
191
-
192
- // Lógica YouTube
193
- if (isYT && data.event === 'infoDelivery' && data.info && data.info.currentTime) {
194
- this.handleVideoProgress(iframe, data.info.currentTime, data.info.duration, fired, 'YouTube');
195
- }
196
-
197
- // Lógica Vimeo
198
- if (isVimeo && data.event === 'timeupdate') {
199
- this.handleVideoProgress(iframe, data.data.seconds, data.data.duration, fired, 'Vimeo');
200
- }
201
- } catch (e) { /* ignore non-json messages */ }
202
- });
203
-
204
- // Ativa o JS API se for YouTube
205
- if (isYT && !src.includes('enablejsapi=1')) {
206
- const url = new URL(src);
207
- url.searchParams.set('enablejsapi', '1');
208
- iframe.src = url.toString();
209
- }
210
- }
211
- });
212
- },
213
-
214
- handleVideoProgress(iframe, current, total, firedSet, platform) {
215
- if (!total) return;
216
- const pct = Math.floor((current / total) * 100);
217
- const milestones = [25, 50, 75, 100];
218
- const videoId = iframe.id || iframe.src.split('/').pop().split('?')[0];
219
-
220
- milestones.forEach(m => {
221
- const key = `${videoId}_${m}`;
222
- if (pct >= m && !firedSet.has(key)) {
223
- firedSet.add(key);
224
- this.addScore(m === 25 ? 10 : (m === 50 ? 15 : 25), `vsl_${m}%`);
225
- cdpTrack.track('video_milestone', {
226
- video_id: videoId,
227
- platform: platform,
228
- percent: m,
229
- meta_intensity: m >= 50 ? 'high' : 'medium'
230
- });
231
- }
232
- });
233
- },
234
-
235
- // 6. Form Analytics (Friction & Abandonment)
236
- setupFormAnalytics() {
237
- const inputs = document.querySelectorAll('input, select, textarea');
238
- inputs.forEach(input => {
239
- input.addEventListener('focus', () => {
240
- const name = input.name || input.id;
241
- if (!this.state.formStartTime[name]) {
242
- this.state.formStartTime[name] = Date.now();
243
- this.addScore(10, `form_interaction_${name}`);
244
- cdpTrack.track('form_field_focus', { field: name });
245
- }
246
- });
247
-
248
- input.addEventListener('blur', () => {
249
- const name = input.name || input.id;
250
- const duration = Math.round((Date.now() - this.state.formStartTime[name]) / 1000);
251
- if (duration > 0) {
252
- cdpTrack.track('form_field_blur', { field: name, duration_sec: duration });
253
- }
254
- });
255
- });
256
- },
257
-
258
- // 6.1 Form Abandonment — saiu da página após interagir com formulário sem submeter
259
- setupFormAbandonment() {
260
- const formInteracted = new Set(); // campos que receberam foco
261
- let formSubmitted = false;
262
-
263
- // Marcar campo como interagido no focus
264
- document.addEventListener('focusin', (e) => {
265
- const el = e.target;
266
- if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT') {
267
- const name = el.name || el.id || el.type;
268
- if (name) formInteracted.add(name);
269
- }
270
- }, true);
271
-
272
- // Marcar submit realizado (qualquer formulário)
273
- document.addEventListener('submit', () => {
274
- formSubmitted = true;
275
- }, true);
276
-
277
- // Detectar abandono via visibilitychange (tab hidden / fechando)
278
- document.addEventListener('visibilitychange', () => {
279
- if (document.visibilityState === 'hidden' && formInteracted.size > 0 && !formSubmitted) {
280
- this.addScore(-5, 'form_abandonment');
281
- cdpTrack.track('form_abandonment', {
282
- fields_interacted: Array.from(formInteracted),
283
- fields_count: formInteracted.size,
284
- meta_intensity: 'medium',
285
- time_on_page: Math.floor((Date.now() - this.state.startTime) / 1000),
286
- });
287
- }
288
- });
289
-
290
- // Fallback: beforeunload
291
- window.addEventListener('beforeunload', () => {
292
- if (formInteracted.size > 0 && !formSubmitted) {
293
- // Usar sendBeacon para garantir entrega mesmo na saída
294
- if (navigator.sendBeacon && typeof cdpTrack !== 'undefined') {
295
- const data = JSON.stringify({
296
- eventName: 'form_abandonment',
297
- behavioral_data: { user_score: this.state.userScore },
298
- fields_interacted: Array.from(formInteracted),
299
- fields_count: formInteracted.size,
300
- meta_intensity: 'medium',
301
- });
302
- navigator.sendBeacon('/api/tracking', new Blob([data], { type: 'application/json' }));
303
- }
304
- }
305
- });
306
- },
307
-
308
- // 6.2 Exit Intent — mouse saindo pelo topo da viewport
309
- setupExitIntent() {
310
- let exitFired = false; // disparar apenas uma vez por sessão
311
-
312
- document.addEventListener('mousemove', (e) => {
313
- // Threshold: mouse a menos de 20px do topo da viewport
314
- if (e.clientY < 20 && !exitFired) {
315
- exitFired = true;
316
- this.addScore(15, 'exit_intent'); // sinal positivo: ainda está aqui
317
- cdpTrack.track('exit_intent', {
318
- mouse_y: e.clientY,
319
- time_on_page: Math.floor((Date.now() - this.state.startTime) / 1000),
320
- user_score: this.state.userScore,
321
- meta_intensity: 'high', // Alta intenção — usuário está prestes a sair
322
- });
323
-
324
- // Resetar após 30s para redetectar se usuário continuar
325
- setTimeout(() => { exitFired = false; }, 30000);
326
- }
327
- }, { passive: true });
328
-
329
- // Mobile: detectar via visibilitychange (sem mouse)
330
- let mobileExitFired = false;
331
- document.addEventListener('visibilitychange', () => {
332
- if (document.visibilityState === 'hidden' && !mobileExitFired) {
333
- const timeOnPage = Math.floor((Date.now() - this.state.startTime) / 1000);
334
- // Só dispara se usuário ficou tempo suficiente (não é bounce imediato)
335
- if (timeOnPage > 5) {
336
- mobileExitFired = true;
337
- cdpTrack.track('exit_intent_mobile', {
338
- time_on_page: timeOnPage,
339
- user_score: this.state.userScore,
340
- meta_intensity: 'medium',
341
- });
342
- }
343
- }
344
- });
345
- },
346
-
347
- // 7. Copy-Paste Intent (High Conv.)
348
- setupCopyPaste() {
349
- document.addEventListener('copy', () => {
350
- const selectedText = window.getSelection().toString();
351
- if (selectedText.length > 0) {
352
- this.addScore(20, 'text_copy');
353
- cdpTrack.track('content_copy', {
354
- text_length: selectedText.length,
355
- meta_intensity: 'medium'
356
- });
357
- }
358
- });
359
- },
360
-
361
- // 8. Outbound Link Tracking (Whitelist: WhatsApp/Checkouts)
362
- setupOutboundLinks() {
363
- document.addEventListener('click', (e) => {
364
- const link = e.target.closest('a');
365
- if (link && link.href) {
366
- const url = link.href;
367
- const isExternal = !url.includes(window.location.hostname);
368
- const isWhitelisted = url.includes('wa.me') ||
369
- url.includes('hotmart.com') ||
370
- url.includes('kiwify.com') ||
371
- url.includes('checkout');
372
-
373
- if (isExternal && isWhitelisted) {
374
- this.addScore(100, 'outbound_conversion_intent');
375
- cdpTrack.track('outbound_click', {
376
- destination: url,
377
- meta_intensity: 'high'
378
- });
379
- }
380
- }
381
- });
382
- },
383
-
384
- // 9. JS Error Tracking (D1 Only)
385
- setupErrorTracking() {
386
- window.addEventListener('error', (event) => {
387
- if (typeof cdpTrack.sendServerEvent === 'function') {
388
- cdpTrack.sendServerEvent('js_error', null, {
389
- message: event.message,
390
- source: event.filename,
391
- lineno: event.lineno,
392
- colno: event.colno
393
- });
394
- }
395
- });
396
- },
397
-
398
- // 10. Idle Detection (Active Time)
399
- setupIdleDetection() {
400
- const activityEvents = ['mousedown', 'mousemove', 'keydown', 'scroll', 'touchstart'];
401
- activityEvents.forEach(evt => {
402
- document.addEventListener(evt, () => {
403
- this.state.lastActivity = Date.now();
404
- if (this.state.isIdle) {
405
- this.state.isIdle = false;
406
- cdpTrack.track('user_active', { status: 'back_online' });
407
- }
408
- }, { passive: true });
409
- });
410
-
411
- setInterval(() => {
412
- if (Date.now() - this.state.lastActivity > this.config.idleThreshold && !this.state.isIdle) {
413
- this.state.isIdle = true;
414
- cdpTrack.track('user_idle', { idle_duration: this.config.idleThreshold / 1000 });
415
- }
416
- }, 10000);
417
- }
418
- };
419
-
420
- // Auto-inicialização se o cdpTrack estiver presente
421
- if (typeof cdpTrack !== 'undefined') {
422
- BehaviorEngine.init();
423
- }
424
-
425
- export default BehaviorEngine;
1
+ /**
2
+ * CDP Edge Human-Behavior Engine (Quantum Tier)
3
+ * Responsável por capturar micro-interações e intenção real.
4
+ */
5
+
6
+ const BehaviorEngine = {
7
+ config: {
8
+ rageClickThreshold: 3,
9
+ rageClickTime: 700, // ms
10
+ idleThreshold: 60000, // 60s
11
+ scoreThresholds: {
12
+ engaged: 40,
13
+ highIntent: 80
14
+ }
15
+ },
16
+
17
+ state: {
18
+ clickHistory: [],
19
+ startTime: Date.now(),
20
+ lastActivity: Date.now(),
21
+ lastPulse: Date.now(),
22
+ isIdle: false,
23
+ formStartTime: {},
24
+ userScore: 0,
25
+ firedScoreEvents: new Set(),
26
+ abVariant: null,
27
+ },
28
+
29
+ init() {
30
+ this.setupRageClicks();
31
+ this.setupVisibility();
32
+ this.setupHeartbeat();
33
+ this.setupScroll();
34
+ this.setupVideoTracking();
35
+ this.setupFormAnalytics();
36
+ this.setupFormAbandonment();
37
+ this.setupCopyPaste();
38
+ this.setupExitIntent();
39
+ this.setupOutboundLinks();
40
+ this.setupErrorTracking();
41
+ this.setupIdleDetection();
42
+ this.setupScoring();
43
+ this.setupABTesting();
44
+ console.log('[CDP Edge] Enterprise Behavior Engine Initialized');
45
+ },
46
+
47
+ // 0.1 A/B Testing (Mode 1 - Edge Sync)
48
+ setupABTesting() {
49
+ // Busca variante no Cookie ou URL (Modo 1)
50
+ const getCookie = (name) => {
51
+ const value = `; ${document.cookie}`;
52
+ const parts = value.split(`; ${name}=`);
53
+ if (parts.length === 2) return parts.pop().split(';').shift();
54
+ };
55
+
56
+ const urlParams = new URLSearchParams(window.location.search);
57
+ this.state.abVariant = urlParams.get('cdp_variant') || getCookie('cdp_ab_variant') || 'original';
58
+
59
+ console.log(`[CDP Edge] A/B Variant Detected: ${this.state.abVariant}`);
60
+
61
+ // Intercepta o cdpTrack.track para injetar a variante globalmente
62
+ const originalTrack = cdpTrack.track;
63
+ cdpTrack.track = (eventName, eventParams = {}) => {
64
+ const enrichedParams = {
65
+ ...eventParams,
66
+ ab_test_variant: this.state.abVariant,
67
+ user_score: this.state.userScore
68
+ };
69
+ return originalTrack.apply(cdpTrack, [eventName, enrichedParams]);
70
+ };
71
+ },
72
+
73
+ // 0. Scoring Engine (Internal)
74
+ setupScoring() {
75
+ console.log('[CDP Edge] Scoring Engine Online');
76
+ },
77
+
78
+ addScore(points, reason) {
79
+ this.state.userScore = Math.min(100, Math.max(0, this.state.userScore + points));
80
+ console.log(`[CDP Edge] Score Update: +${points} (${reason}) | Total: ${this.state.userScore}`);
81
+
82
+ // Gatilhos de Plataforma
83
+ if (this.state.userScore >= this.config.scoreThresholds.highIntent && !this.state.firedScoreEvents.has('highIntent')) {
84
+ this.state.firedScoreEvents.add('highIntent');
85
+ cdpTrack.track('High_Intent_Lead', { score: this.state.userScore, meta_intensity: 'high' });
86
+ } else if (this.state.userScore >= this.config.scoreThresholds.engaged && !this.state.firedScoreEvents.has('engaged')) {
87
+ this.state.firedScoreEvents.add('engaged');
88
+ cdpTrack.track('Engaged_User', { score: this.state.userScore, meta_intensity: 'medium' });
89
+ }
90
+ },
91
+
92
+ // 1. Rage Click Detector
93
+ setupRageClicks() {
94
+ document.addEventListener('click', (e) => {
95
+ const now = Date.now();
96
+ this.state.clickHistory.push(now);
97
+
98
+ // Limpa histórico antigo
99
+ this.state.clickHistory = this.state.clickHistory.filter(t => now - t < this.config.rageClickTime);
100
+
101
+ if (this.state.clickHistory.length >= this.config.rageClickThreshold) {
102
+ cdpTrack.track('rage_click', {
103
+ element_id: e.target.id || '',
104
+ element_class: e.target.className || '',
105
+ x: e.pageX,
106
+ y: e.pageY,
107
+ meta_intensity: 'low' // Envia sinal leve para Meta
108
+ });
109
+ this.addScore(-10, 'rage_click');
110
+ this.state.clickHistory = []; // Reseta após detecção
111
+ }
112
+ });
113
+ },
114
+
115
+ // 1.2 Scroll Depth (Quantum Tier)
116
+ setupScroll() {
117
+ const markers = [25, 50, 75, 90];
118
+ const fired = new Set();
119
+
120
+ window.addEventListener('scroll', () => {
121
+ const scrollPct = Math.round((window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100);
122
+
123
+ markers.forEach(m => {
124
+ if (scrollPct >= m && !fired.has(m)) {
125
+ fired.add(m);
126
+ this.addScore(m === 25 ? 5 : (m === 50 ? 5 : 10), `scroll_${m}%`);
127
+ cdpTrack.track('scroll_depth', {
128
+ percent: m,
129
+ meta_intensity: m >= 50 ? 'medium' : 'low' // Envia como sinal mais forte após 50%
130
+ });
131
+ }
132
+ });
133
+ }, { passive: true });
134
+ },
135
+
136
+ // 2. Tab Visibility (VSL Focus)
137
+ setupVisibility() {
138
+ document.addEventListener('visibilitychange', () => {
139
+ const status = document.visibilityState; // 'visible' ou 'hidden'
140
+ cdpTrack.track('tab_visibility_change', {
141
+ status: status,
142
+ time_since_start: Math.floor((Date.now() - this.state.startTime) / 1000),
143
+ meta_intensity: status === 'visible' ? 'high' : 'low'
144
+ });
145
+ });
146
+ },
147
+
148
+ // 3. Click Heatmap (D1 Only)
149
+ setupHeatmap() {
150
+ document.addEventListener('click', (e) => {
151
+ // Enviamos apenas para o servidor/D1 para não poluir o pixel do cliente
152
+ // sendServerEvent é o método interno que pula o despacho para Meta/TikTok
153
+ if (typeof cdpTrack.sendServerEvent === 'function') {
154
+ cdpTrack.sendServerEvent('click_heatmap', null, {
155
+ x: e.pageX,
156
+ y: e.pageY,
157
+ element: e.target.tagName.toLowerCase(),
158
+ id: e.target.id || '',
159
+ classes: e.target.className || ''
160
+ });
161
+ }
162
+ });
163
+ },
164
+
165
+ // 4. Retention Pulse (Heartbeat)
166
+ setupHeartbeat() {
167
+ setInterval(() => {
168
+ const activeTime = Math.floor((Date.now() - this.state.startTime) / 1000);
169
+ cdpTrack.track('pulse_heartbeat', {
170
+ duration_seconds: activeTime,
171
+ is_visible: document.visibilityState === 'visible'
172
+ });
173
+ }, this.config.heartbeatInterval);
174
+ },
175
+
176
+ // 5. VSL / Video Tracking (YouTube & Vimeo)
177
+ setupVideoTracking() {
178
+ const iframes = document.querySelectorAll('iframe');
179
+ const fired = new Set();
180
+
181
+ iframes.forEach(iframe => {
182
+ const src = iframe.src || '';
183
+ const isYT = src.includes('youtube.com/embed');
184
+ const isVimeo = src.includes('vimeo.com/video');
185
+
186
+ if (isYT || isVimeo) {
187
+ // Observer de progresso (via postMessage para evitar dependência de SDK pesado)
188
+ window.addEventListener('message', (event) => {
189
+ try {
190
+ const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
191
+
192
+ // Lógica YouTube
193
+ if (isYT && data.event === 'infoDelivery' && data.info && data.info.currentTime) {
194
+ this.handleVideoProgress(iframe, data.info.currentTime, data.info.duration, fired, 'YouTube');
195
+ }
196
+
197
+ // Lógica Vimeo
198
+ if (isVimeo && data.event === 'timeupdate') {
199
+ this.handleVideoProgress(iframe, data.data.seconds, data.data.duration, fired, 'Vimeo');
200
+ }
201
+ } catch (e) { /* ignore non-json messages */ }
202
+ });
203
+
204
+ // Ativa o JS API se for YouTube
205
+ if (isYT && !src.includes('enablejsapi=1')) {
206
+ const url = new URL(src);
207
+ url.searchParams.set('enablejsapi', '1');
208
+ iframe.src = url.toString();
209
+ }
210
+ }
211
+ });
212
+ },
213
+
214
+ handleVideoProgress(iframe, current, total, firedSet, platform) {
215
+ if (!total) return;
216
+ const pct = Math.floor((current / total) * 100);
217
+ const milestones = [25, 50, 75, 100];
218
+ const videoId = iframe.id || iframe.src.split('/').pop().split('?')[0];
219
+
220
+ milestones.forEach(m => {
221
+ const key = `${videoId}_${m}`;
222
+ if (pct >= m && !firedSet.has(key)) {
223
+ firedSet.add(key);
224
+ this.addScore(m === 25 ? 10 : (m === 50 ? 15 : 25), `vsl_${m}%`);
225
+ cdpTrack.track('video_milestone', {
226
+ video_id: videoId,
227
+ platform: platform,
228
+ percent: m,
229
+ meta_intensity: m >= 50 ? 'high' : 'medium'
230
+ });
231
+ }
232
+ });
233
+ },
234
+
235
+ // 6. Form Analytics (Friction & Abandonment)
236
+ setupFormAnalytics() {
237
+ const inputs = document.querySelectorAll('input, select, textarea');
238
+ inputs.forEach(input => {
239
+ input.addEventListener('focus', () => {
240
+ const name = input.name || input.id;
241
+ if (!this.state.formStartTime[name]) {
242
+ this.state.formStartTime[name] = Date.now();
243
+ this.addScore(10, `form_interaction_${name}`);
244
+ cdpTrack.track('form_field_focus', { field: name });
245
+ }
246
+ });
247
+
248
+ input.addEventListener('blur', () => {
249
+ const name = input.name || input.id;
250
+ const duration = Math.round((Date.now() - this.state.formStartTime[name]) / 1000);
251
+ if (duration > 0) {
252
+ cdpTrack.track('form_field_blur', { field: name, duration_sec: duration });
253
+ }
254
+ });
255
+ });
256
+ },
257
+
258
+ // 6.1 Form Abandonment — saiu da página após interagir com formulário sem submeter
259
+ setupFormAbandonment() {
260
+ const formInteracted = new Set(); // campos que receberam foco
261
+ let formSubmitted = false;
262
+
263
+ // Marcar campo como interagido no focus
264
+ document.addEventListener('focusin', (e) => {
265
+ const el = e.target;
266
+ if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT') {
267
+ const name = el.name || el.id || el.type;
268
+ if (name) formInteracted.add(name);
269
+ }
270
+ }, true);
271
+
272
+ // Marcar submit realizado (qualquer formulário)
273
+ document.addEventListener('submit', () => {
274
+ formSubmitted = true;
275
+ }, true);
276
+
277
+ // Detectar abandono via visibilitychange (tab hidden / fechando)
278
+ document.addEventListener('visibilitychange', () => {
279
+ if (document.visibilityState === 'hidden' && formInteracted.size > 0 && !formSubmitted) {
280
+ this.addScore(-5, 'form_abandonment');
281
+ cdpTrack.track('form_abandonment', {
282
+ fields_interacted: Array.from(formInteracted),
283
+ fields_count: formInteracted.size,
284
+ meta_intensity: 'medium',
285
+ time_on_page: Math.floor((Date.now() - this.state.startTime) / 1000),
286
+ });
287
+ }
288
+ });
289
+
290
+ // Fallback: beforeunload
291
+ window.addEventListener('beforeunload', () => {
292
+ if (formInteracted.size > 0 && !formSubmitted) {
293
+ // Usar sendBeacon para garantir entrega mesmo na saída
294
+ if (navigator.sendBeacon && typeof cdpTrack !== 'undefined') {
295
+ const data = JSON.stringify({
296
+ eventName: 'form_abandonment',
297
+ behavioral_data: { user_score: this.state.userScore },
298
+ fields_interacted: Array.from(formInteracted),
299
+ fields_count: formInteracted.size,
300
+ meta_intensity: 'medium',
301
+ });
302
+ navigator.sendBeacon('/api/tracking', new Blob([data], { type: 'application/json' }));
303
+ }
304
+ }
305
+ });
306
+ },
307
+
308
+ // 6.2 Exit Intent — mouse saindo pelo topo da viewport
309
+ setupExitIntent() {
310
+ let exitFired = false; // disparar apenas uma vez por sessão
311
+
312
+ document.addEventListener('mousemove', (e) => {
313
+ // Threshold: mouse a menos de 20px do topo da viewport
314
+ if (e.clientY < 20 && !exitFired) {
315
+ exitFired = true;
316
+ this.addScore(15, 'exit_intent'); // sinal positivo: ainda está aqui
317
+ cdpTrack.track('exit_intent', {
318
+ mouse_y: e.clientY,
319
+ time_on_page: Math.floor((Date.now() - this.state.startTime) / 1000),
320
+ user_score: this.state.userScore,
321
+ meta_intensity: 'high', // Alta intenção — usuário está prestes a sair
322
+ });
323
+
324
+ // Resetar após 30s para redetectar se usuário continuar
325
+ setTimeout(() => { exitFired = false; }, 30000);
326
+ }
327
+ }, { passive: true });
328
+
329
+ // Mobile: detectar via visibilitychange (sem mouse)
330
+ let mobileExitFired = false;
331
+ document.addEventListener('visibilitychange', () => {
332
+ if (document.visibilityState === 'hidden' && !mobileExitFired) {
333
+ const timeOnPage = Math.floor((Date.now() - this.state.startTime) / 1000);
334
+ // Só dispara se usuário ficou tempo suficiente (não é bounce imediato)
335
+ if (timeOnPage > 5) {
336
+ mobileExitFired = true;
337
+ cdpTrack.track('exit_intent_mobile', {
338
+ time_on_page: timeOnPage,
339
+ user_score: this.state.userScore,
340
+ meta_intensity: 'medium',
341
+ });
342
+ }
343
+ }
344
+ });
345
+ },
346
+
347
+ // 7. Copy-Paste Intent (High Conv.)
348
+ setupCopyPaste() {
349
+ document.addEventListener('copy', () => {
350
+ const selectedText = window.getSelection().toString();
351
+ if (selectedText.length > 0) {
352
+ this.addScore(20, 'text_copy');
353
+ cdpTrack.track('content_copy', {
354
+ text_length: selectedText.length,
355
+ meta_intensity: 'medium'
356
+ });
357
+ }
358
+ });
359
+ },
360
+
361
+ // 8. Outbound Link Tracking (Whitelist: WhatsApp/Checkouts)
362
+ setupOutboundLinks() {
363
+ document.addEventListener('click', (e) => {
364
+ const link = e.target.closest('a');
365
+ if (link && link.href) {
366
+ const url = link.href;
367
+ const isExternal = !url.includes(window.location.hostname);
368
+ const isWhitelisted = url.includes('wa.me') ||
369
+ url.includes('hotmart.com') ||
370
+ url.includes('kiwify.com') ||
371
+ url.includes('checkout');
372
+
373
+ if (isExternal && isWhitelisted) {
374
+ this.addScore(100, 'outbound_conversion_intent');
375
+ cdpTrack.track('outbound_click', {
376
+ destination: url,
377
+ meta_intensity: 'high'
378
+ });
379
+ }
380
+ }
381
+ });
382
+ },
383
+
384
+ // 9. JS Error Tracking (D1 Only)
385
+ setupErrorTracking() {
386
+ window.addEventListener('error', (event) => {
387
+ if (typeof cdpTrack.sendServerEvent === 'function') {
388
+ cdpTrack.sendServerEvent('js_error', null, {
389
+ message: event.message,
390
+ source: event.filename,
391
+ lineno: event.lineno,
392
+ colno: event.colno
393
+ });
394
+ }
395
+ });
396
+ },
397
+
398
+ // 10. Idle Detection (Active Time)
399
+ setupIdleDetection() {
400
+ const activityEvents = ['mousedown', 'mousemove', 'keydown', 'scroll', 'touchstart'];
401
+ activityEvents.forEach(evt => {
402
+ document.addEventListener(evt, () => {
403
+ this.state.lastActivity = Date.now();
404
+ if (this.state.isIdle) {
405
+ this.state.isIdle = false;
406
+ cdpTrack.track('user_active', { status: 'back_online' });
407
+ }
408
+ }, { passive: true });
409
+ });
410
+
411
+ setInterval(() => {
412
+ if (Date.now() - this.state.lastActivity > this.config.idleThreshold && !this.state.isIdle) {
413
+ this.state.isIdle = true;
414
+ cdpTrack.track('user_idle', { idle_duration: this.config.idleThreshold / 1000 });
415
+ }
416
+ }, 10000);
417
+ }
418
+ };
419
+
420
+ // Auto-inicialização se o cdpTrack estiver presente
421
+ if (typeof cdpTrack !== 'undefined') {
422
+ BehaviorEngine.init();
423
+ }
424
+
425
+ export default BehaviorEngine;