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.
- package/README.md +308 -308
- package/bin/cdp-edge.js +61 -61
- package/dist/commands/analyze.js +52 -52
- package/dist/commands/infra.js +54 -54
- package/dist/commands/install.js +186 -0
- package/dist/commands/server.js +174 -174
- package/dist/commands/setup.js +18 -1
- package/dist/commands/validate.js +84 -84
- package/dist/index.js +12 -12
- package/extracted-skill/tracking-events-generator/advanced-matching.js +364 -364
- package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +172 -72
- package/extracted-skill/tracking-events-generator/agents/google-agent.md +118 -0
- package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +86 -0
- package/extracted-skill/tracking-events-generator/agents/intelligence-scheduling.md +8 -641
- package/extracted-skill/tracking-events-generator/agents/memory-agent.md +98 -0
- package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +42 -0
- package/extracted-skill/tracking-events-generator/anti-blocking.js +285 -285
- package/extracted-skill/tracking-events-generator/cdpTrack.js +641 -641
- package/extracted-skill/tracking-events-generator/engagement-scoring.js +226 -226
- package/extracted-skill/tracking-events-generator/evals/evals.json +235 -235
- package/extracted-skill/tracking-events-generator/integration-test.js +497 -497
- package/extracted-skill/tracking-events-generator/micro-events.js +992 -992
- package/extracted-skill/tracking-events-generator/models/pinterest/conversions-api-template.js +144 -144
- package/extracted-skill/tracking-events-generator/models/pinterest/event-mappings.json +48 -48
- package/extracted-skill/tracking-events-generator/models/pinterest/tag-template.js +28 -28
- package/extracted-skill/tracking-events-generator/models/reddit/conversions-api-template.js +205 -205
- package/extracted-skill/tracking-events-generator/models/reddit/event-mappings.json +56 -56
- package/extracted-skill/tracking-events-generator/models/reddit/pixel-template.js +19 -19
- package/extracted-skill/tracking-events-generator/models/scenarios/behavior-engine.js +425 -425
- package/package.json +76 -76
- package/server-edge-tracker/schema.sql +265 -265
- package/server-edge-tracker/worker.js +4160 -4160
- package/server-edge-tracker/wrangler.toml +103 -103
- package/templates/pinterest/conversions-api-template.js +144 -144
- package/templates/pinterest/event-mappings.json +48 -48
- package/templates/pinterest/tag-template.js +28 -28
- package/templates/reddit/conversions-api-template.js +205 -205
- package/templates/reddit/event-mappings.json +56 -56
- package/templates/reddit/pixel-template.js +19 -19
- 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;
|