@metodokorexmk/tracking 1.0.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.
@@ -0,0 +1,862 @@
1
+ import { useRef, useEffect, useState } from 'react';
2
+ import ReactGA from 'react-ga4';
3
+
4
+ // src/react/use-landing-tracking.ts
5
+ var currentConfig = {};
6
+ var cachedUserId = null;
7
+ var cachedUserName = null;
8
+ var captureUTMParams = () => {
9
+ if (typeof window === "undefined") return null;
10
+ const params = new URLSearchParams(window.location.search);
11
+ const utmParams = {};
12
+ let hasParams = false;
13
+ const utmKeys = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
14
+ for (const key of utmKeys) {
15
+ const value = params.get(key);
16
+ if (value) {
17
+ utmParams[key] = value;
18
+ hasParams = true;
19
+ }
20
+ }
21
+ return hasParams ? utmParams : null;
22
+ };
23
+ var captureUserIdFromURL = () => {
24
+ if (typeof window === "undefined") return null;
25
+ const params = new URLSearchParams(window.location.search);
26
+ const userId = params.get("user_id") || params.get("userId");
27
+ if (userId) {
28
+ cachedUserId = userId;
29
+ try {
30
+ localStorage.setItem("tracking_user_id", userId);
31
+ } catch {
32
+ }
33
+ }
34
+ return userId;
35
+ };
36
+ var getUserName = () => {
37
+ if (typeof window === "undefined") return null;
38
+ const params = new URLSearchParams(window.location.search);
39
+ const fromUrl = params.get("user_name") || params.get("userName");
40
+ if (fromUrl) {
41
+ cachedUserName = fromUrl;
42
+ return fromUrl;
43
+ }
44
+ if (cachedUserName) return cachedUserName;
45
+ try {
46
+ const fromStorage = localStorage.getItem("tracking_user_name");
47
+ if (fromStorage) {
48
+ cachedUserName = fromStorage;
49
+ return fromStorage;
50
+ }
51
+ const userInfo = localStorage.getItem("userInfo");
52
+ if (userInfo) {
53
+ const parsed = JSON.parse(userInfo);
54
+ const name = parsed?.name || parsed?.userName || parsed?.fullName;
55
+ if (name) {
56
+ cachedUserName = name;
57
+ return name;
58
+ }
59
+ }
60
+ } catch {
61
+ }
62
+ return null;
63
+ };
64
+ var getUserId = () => {
65
+ if (cachedUserId) return cachedUserId;
66
+ if (typeof window === "undefined") return null;
67
+ try {
68
+ return localStorage.getItem("tracking_user_id");
69
+ } catch {
70
+ return null;
71
+ }
72
+ };
73
+ var trackPageView = (path) => {
74
+ if (typeof window === "undefined") return;
75
+ const userId = getUserId();
76
+ const utmParams = captureUTMParams();
77
+ let pageWithParams = path;
78
+ if (utmParams) {
79
+ const searchParams = new URLSearchParams();
80
+ Object.entries(utmParams).forEach(([key, value]) => {
81
+ if (value) searchParams.set(key, value);
82
+ });
83
+ const paramString = searchParams.toString();
84
+ if (paramString) {
85
+ pageWithParams = `${path}${path.includes("?") ? "&" : "?"}${paramString}`;
86
+ }
87
+ }
88
+ try {
89
+ ReactGA.send({
90
+ hitType: "pageview",
91
+ page: pageWithParams,
92
+ ...userId && { user_id: userId }
93
+ });
94
+ } catch {
95
+ }
96
+ try {
97
+ window.gtag?.("event", "page_view", {
98
+ page_path: pageWithParams,
99
+ page_title: document.title,
100
+ ...userId && { user_id: userId }
101
+ });
102
+ } catch {
103
+ }
104
+ if (currentConfig.debug) {
105
+ console.log(`[KorexTracking] PageView: ${pageWithParams}`, { userId });
106
+ }
107
+ };
108
+ var trackEvent = (category, action, label, value, additionalData) => {
109
+ if (typeof window === "undefined") return;
110
+ const userId = getUserId();
111
+ const userName = getUserName();
112
+ const eventData = {
113
+ event_category: category,
114
+ event_label: label || "",
115
+ ...value !== void 0 && { value },
116
+ ...userId && { user_id: userId },
117
+ ...userName && { user_name: userName },
118
+ timestamp: Date.now(),
119
+ ...additionalData
120
+ };
121
+ try {
122
+ window.gtag?.("event", action, eventData);
123
+ } catch {
124
+ }
125
+ try {
126
+ ReactGA.event({
127
+ category,
128
+ action,
129
+ label: label || void 0,
130
+ value: value || void 0,
131
+ ...userId && { user_id: userId }
132
+ });
133
+ } catch {
134
+ }
135
+ if (currentConfig.debug) {
136
+ console.log(`[KorexTracking] Event: ${category}/${action}`, eventData);
137
+ }
138
+ };
139
+
140
+ // src/core/gtm.ts
141
+ var injectGTMScript = (gtmId) => {
142
+ if (typeof window === "undefined") return;
143
+ if (!gtmId) return;
144
+ const existingScript = document.querySelector(`script[data-gtm-id="${gtmId}"]`);
145
+ if (existingScript) return;
146
+ window.dataLayer = window.dataLayer || [];
147
+ window.dataLayer.push({
148
+ "gtm.start": (/* @__PURE__ */ new Date()).getTime(),
149
+ event: "gtm.js"
150
+ });
151
+ const script = document.createElement("script");
152
+ script.async = true;
153
+ script.src = `https://www.googletagmanager.com/gtm.js?id=${gtmId}`;
154
+ script.setAttribute("data-gtm-id", gtmId);
155
+ const firstScript = document.getElementsByTagName("script")[0];
156
+ if (firstScript?.parentNode) {
157
+ firstScript.parentNode.insertBefore(script, firstScript);
158
+ } else {
159
+ document.head.appendChild(script);
160
+ }
161
+ if (document.body) {
162
+ const noscript = document.createElement("noscript");
163
+ const iframe = document.createElement("iframe");
164
+ iframe.src = `https://www.googletagmanager.com/ns.html?id=${gtmId}`;
165
+ iframe.height = "0";
166
+ iframe.width = "0";
167
+ iframe.style.display = "none";
168
+ iframe.style.visibility = "hidden";
169
+ noscript.appendChild(iframe);
170
+ document.body.insertBefore(noscript, document.body.firstChild);
171
+ }
172
+ };
173
+ var pushToDataLayer = (eventName, data) => {
174
+ if (typeof window === "undefined") return;
175
+ window.dataLayer = window.dataLayer || [];
176
+ window.dataLayer.push({
177
+ event: eventName,
178
+ ...data
179
+ });
180
+ };
181
+
182
+ // src/trackers/events.ts
183
+ var trackCTAClick = (buttonName, section, additionalData) => {
184
+ trackEvent("CTA", "click", buttonName, void 0, {
185
+ button_name: buttonName,
186
+ section,
187
+ ...additionalData
188
+ });
189
+ };
190
+ var trackFormStart = (formName) => {
191
+ trackEvent("Form", "started", formName, void 0, {
192
+ form_name: formName,
193
+ timestamp: Date.now()
194
+ });
195
+ };
196
+ var trackFormSubmit = (formName, success, additionalData) => {
197
+ trackEvent("Form", "submit_success" , formName, void 0, {
198
+ form_name: formName,
199
+ success,
200
+ ...additionalData
201
+ });
202
+ };
203
+ var trackTimeInSection = (sectionName, seconds) => {
204
+ if (seconds < 3) return;
205
+ trackEvent("Engagement", "time_in_section", sectionName, Math.round(seconds), {
206
+ section_name: sectionName,
207
+ time_seconds: Math.round(seconds)
208
+ });
209
+ };
210
+
211
+ // src/orchestrator/landing-tracker.ts
212
+ var LandingTracker = class {
213
+ constructor() {
214
+ this.config = null;
215
+ this.isInitialized = false;
216
+ this.sessionStartTime = 0;
217
+ this.scrollMilestonesReached = /* @__PURE__ */ new Set();
218
+ this.clicks = [];
219
+ this.sectionTimers = /* @__PURE__ */ new Map();
220
+ this.observers = [];
221
+ this.listeners = [];
222
+ }
223
+ /**
224
+ * Inicializa el tracking de la landing con la configuración dada.
225
+ */
226
+ init(config) {
227
+ if (typeof window === "undefined") return;
228
+ if (this.isInitialized) return;
229
+ this.config = {
230
+ enableScrollTracking: true,
231
+ enableCTATracking: true,
232
+ enableTimeTracking: true,
233
+ enableSectionTracking: true,
234
+ enableFormTracking: true,
235
+ enableHeatmap: false,
236
+ eventSuffix: "",
237
+ ...config
238
+ };
239
+ this.isInitialized = true;
240
+ this.sessionStartTime = Date.now();
241
+ this.captureInitialData();
242
+ trackPageView(this.config.pagePath);
243
+ if (this.config.gtmId) {
244
+ injectGTMScript(this.config.gtmId);
245
+ pushToDataLayer(this.getEventName("page_load"), {
246
+ page_path: this.config.pagePath,
247
+ page_title: this.config.pageName,
248
+ timestamp: Date.now()
249
+ });
250
+ }
251
+ if (this.config.enableScrollTracking) this.initScrollTracking();
252
+ if (this.config.enableCTATracking) this.initCTATracking();
253
+ if (this.config.enableTimeTracking) this.initTimeTracking();
254
+ if (this.config.enableSectionTracking) this.initSectionTracking();
255
+ if (this.config.enableFormTracking) this.initFormTracking();
256
+ if (this.config.enableHeatmap) this.initClickHeatmap();
257
+ this.trackSessionStart();
258
+ }
259
+ /**
260
+ * Destruye el tracker y limpia todos los observers y listeners.
261
+ */
262
+ destroy() {
263
+ this.observers.forEach((obs) => obs.disconnect());
264
+ this.observers = [];
265
+ this.listeners.forEach(({ target, event, handler }) => {
266
+ target.removeEventListener(event, handler);
267
+ });
268
+ this.listeners = [];
269
+ this.isInitialized = false;
270
+ this.config = null;
271
+ this.scrollMilestonesReached.clear();
272
+ this.clicks = [];
273
+ this.sectionTimers.clear();
274
+ }
275
+ // ====================================
276
+ // MÉTODOS PÚBLICOS DE TRACKING
277
+ // ====================================
278
+ /** Trackear click en un CTA */
279
+ trackCTAClick(buttonName, section, additionalData) {
280
+ trackCTAClick(buttonName, section, additionalData);
281
+ this.pushGTMEvent("cta_click", { button_text: buttonName, section });
282
+ }
283
+ /** Trackear conversión */
284
+ trackConversion(type, value, additionalData) {
285
+ trackEvent("Conversion", type, void 0, value, additionalData);
286
+ this.pushGTMEvent("conversion", { conversion_type: type, value });
287
+ }
288
+ /** Trackear click en FAQs */
289
+ trackFAQExpand(question, index) {
290
+ trackEvent("FAQ", "expand", question, index);
291
+ }
292
+ /** Trackear click social */
293
+ trackSocialClick(platform, action) {
294
+ trackEvent("Social", "click", platform, void 0, { social_action: action });
295
+ }
296
+ /** Trackear click en imagen */
297
+ trackImageClick(imageName, section) {
298
+ trackEvent("Engagement", "image_click", imageName, void 0, { section });
299
+ }
300
+ /** Trackear scroll a sección */
301
+ trackScrollTo(section) {
302
+ trackEvent("Navigation", "scroll_to", section);
303
+ }
304
+ /** Trackear share */
305
+ trackShare(platform, content) {
306
+ trackEvent("Share", "click", platform, void 0, { share_content: content });
307
+ }
308
+ /** Trackear descarga */
309
+ trackDownload(fileName, fileType) {
310
+ trackEvent("Download", "click", fileName, void 0, { file_type: fileType });
311
+ }
312
+ /** Obtener datos de la sesión actual */
313
+ getSessionData() {
314
+ return {
315
+ duration: Math.round((Date.now() - this.sessionStartTime) / 1e3),
316
+ clicks: this.clicks.length,
317
+ scrollMilestones: Array.from(this.scrollMilestonesReached)
318
+ };
319
+ }
320
+ // ====================================
321
+ // MÉTODOS PRIVADOS
322
+ // ====================================
323
+ captureInitialData() {
324
+ const utmParams = captureUTMParams();
325
+ const userId = captureUserIdFromURL();
326
+ const userName = getUserName();
327
+ const prefix = this.config.pagePath.replace("/", "");
328
+ try {
329
+ localStorage.setItem(`${prefix}_entry_time`, (/* @__PURE__ */ new Date()).toISOString());
330
+ if (utmParams) {
331
+ localStorage.setItem(`${prefix}_utm_params`, JSON.stringify(utmParams));
332
+ }
333
+ if (userId) {
334
+ localStorage.setItem(`${prefix}_user_id`, userId);
335
+ }
336
+ } catch {
337
+ }
338
+ trackEvent("Landing", "page_load", this.config.pageName, void 0, {
339
+ page_path: this.config.pagePath,
340
+ user_id: userId,
341
+ user_name: userName,
342
+ utm_params: utmParams,
343
+ timestamp: Date.now()
344
+ });
345
+ }
346
+ trackSessionStart() {
347
+ trackEvent("Session", "start", this.config.pageName, void 0, {
348
+ page_path: this.config.pagePath,
349
+ timestamp: Date.now()
350
+ });
351
+ }
352
+ getEventName(baseName) {
353
+ const suffix = this.config?.eventSuffix || "";
354
+ return suffix ? `${baseName}${suffix}` : baseName;
355
+ }
356
+ pushGTMEvent(eventName, data) {
357
+ if (this.config?.gtmId) {
358
+ pushToDataLayer(this.getEventName(eventName), {
359
+ page_path: this.config.pagePath,
360
+ ...data
361
+ });
362
+ }
363
+ }
364
+ addListener(target, event, handler) {
365
+ target.addEventListener(event, handler);
366
+ this.listeners.push({ target, event, handler });
367
+ }
368
+ // --- Scroll Tracking ---
369
+ initScrollTracking() {
370
+ let scrollTimeout;
371
+ const handler = () => {
372
+ clearTimeout(scrollTimeout);
373
+ scrollTimeout = setTimeout(() => {
374
+ const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
375
+ if (scrollHeight <= 0) return;
376
+ const pct = Math.round(window.scrollY / scrollHeight * 100);
377
+ for (const milestone of [25, 50, 75, 100]) {
378
+ if (pct >= milestone && !this.scrollMilestonesReached.has(milestone)) {
379
+ this.scrollMilestonesReached.add(milestone);
380
+ trackEvent("Scroll", "milestone", `${milestone}%`, milestone, {
381
+ page_path: this.config.pagePath,
382
+ scroll_percentage: milestone
383
+ });
384
+ this.pushGTMEvent("scroll_milestone", { scroll_percentage: milestone });
385
+ }
386
+ }
387
+ }, 100);
388
+ };
389
+ this.addListener(window, "scroll", handler);
390
+ }
391
+ // --- CTA Tracking ---
392
+ initCTATracking() {
393
+ setTimeout(() => {
394
+ const ctaButtons = document.querySelectorAll('button, a[href*="#"], [data-cta]');
395
+ ctaButtons.forEach((button) => {
396
+ const handler = () => {
397
+ const el = button;
398
+ const text = el.textContent?.trim() || el.getAttribute("data-cta") || "Unknown CTA";
399
+ const section = el.closest("section")?.id || el.closest("[data-section]")?.getAttribute("data-section") || "unknown";
400
+ this.trackCTAClick(text, section);
401
+ };
402
+ this.addListener(button, "click", handler);
403
+ });
404
+ }, 2e3);
405
+ }
406
+ // --- Time Tracking (page exit) ---
407
+ initTimeTracking() {
408
+ const handler = () => {
409
+ const duration = Math.round((Date.now() - this.sessionStartTime) / 1e3);
410
+ trackEvent("Session", "end", this.config.pageName, duration, {
411
+ session_duration: duration,
412
+ page_path: this.config.pagePath,
413
+ exit_timestamp: Date.now()
414
+ });
415
+ this.pushGTMEvent("page_exit", {
416
+ session_duration: duration,
417
+ exit_timestamp: Date.now()
418
+ });
419
+ };
420
+ this.addListener(window, "beforeunload", handler);
421
+ }
422
+ // --- Section Dwell Time (IntersectionObserver) ---
423
+ initSectionTracking() {
424
+ const sectionEntryTimes = /* @__PURE__ */ new Map();
425
+ const observer = new IntersectionObserver(
426
+ (entries) => {
427
+ entries.forEach((entry) => {
428
+ const sectionId = entry.target.id || entry.target.getAttribute("data-section") || "unknown";
429
+ if (entry.isIntersecting) {
430
+ sectionEntryTimes.set(sectionId, Date.now());
431
+ } else {
432
+ const entryTime = sectionEntryTimes.get(sectionId);
433
+ if (entryTime) {
434
+ const seconds = (Date.now() - entryTime) / 1e3;
435
+ trackTimeInSection(sectionId, seconds);
436
+ sectionEntryTimes.delete(sectionId);
437
+ }
438
+ }
439
+ });
440
+ },
441
+ { threshold: 0.5 }
442
+ );
443
+ setTimeout(() => {
444
+ const sections = document.querySelectorAll("section, [data-section]");
445
+ sections.forEach((section) => observer.observe(section));
446
+ }, 1e3);
447
+ this.observers.push(observer);
448
+ }
449
+ // --- Form Auto-Tracking ---
450
+ initFormTracking() {
451
+ setTimeout(() => {
452
+ const forms = document.querySelectorAll("form");
453
+ forms.forEach((form) => {
454
+ const formName = form.getAttribute("name") || form.id || "unknown-form";
455
+ let started = false;
456
+ const inputs = form.querySelectorAll("input, textarea, select");
457
+ inputs.forEach((input) => {
458
+ this.addListener(input, "focus", (() => {
459
+ if (!started) {
460
+ started = true;
461
+ trackFormStart(formName);
462
+ }
463
+ }));
464
+ });
465
+ this.addListener(form, "submit", ((_e) => {
466
+ trackFormSubmit(formName, true, {
467
+ page_path: this.config.pagePath
468
+ });
469
+ this.pushGTMEvent("form_submit", { form_name: formName });
470
+ }));
471
+ });
472
+ }, 2e3);
473
+ }
474
+ // --- Click Heatmap ---
475
+ initClickHeatmap() {
476
+ const handler = (e) => {
477
+ const mouseEvent = e;
478
+ const target = mouseEvent.target;
479
+ const clickData = {
480
+ x: mouseEvent.clientX,
481
+ y: mouseEvent.clientY,
482
+ element: target.tagName.toLowerCase(),
483
+ section: target.closest("section")?.id || "unknown",
484
+ timestamp: Date.now(),
485
+ viewportWidth: window.innerWidth,
486
+ viewportHeight: window.innerHeight
487
+ };
488
+ this.clicks.push(clickData);
489
+ };
490
+ this.addListener(document, "click", handler);
491
+ }
492
+ };
493
+
494
+ // src/react/use-landing-tracking.ts
495
+ function useLandingPageTracking(config) {
496
+ const trackerRef = useRef(null);
497
+ const initializedRef = useRef(false);
498
+ useEffect(() => {
499
+ if (initializedRef.current) return;
500
+ initializedRef.current = true;
501
+ const tracker = new LandingTracker();
502
+ tracker.init(config);
503
+ trackerRef.current = tracker;
504
+ return () => {
505
+ tracker.destroy();
506
+ trackerRef.current = null;
507
+ };
508
+ }, [config.pagePath]);
509
+ return trackerRef.current;
510
+ }
511
+
512
+ // src/trackers/video-tracker.ts
513
+ var VideoTracker = class {
514
+ constructor() {
515
+ this.videos = /* @__PURE__ */ new Map();
516
+ this.watchTimeIntervals = /* @__PURE__ */ new Map();
517
+ }
518
+ /**
519
+ * Inicializa el tracking para un video.
520
+ */
521
+ initVideo(videoId) {
522
+ if (this.videos.has(videoId)) return;
523
+ const state = {
524
+ videoId,
525
+ startTime: Date.now(),
526
+ totalWatchTime: 0,
527
+ playCount: 0,
528
+ pauseCount: 0,
529
+ seekCount: 0,
530
+ completionPercentage: 0,
531
+ lastPlayTimestamp: 0,
532
+ isPlaying: false,
533
+ currentTime: 0,
534
+ duration: 0,
535
+ pauseTimes: [],
536
+ seekEvents: [],
537
+ progressMilestones: /* @__PURE__ */ new Set(),
538
+ playbackSpeed: 1
539
+ };
540
+ this.videos.set(videoId, state);
541
+ trackEvent("Video", "init", videoId, void 0, {
542
+ video_id: videoId,
543
+ timestamp: Date.now()
544
+ });
545
+ }
546
+ /**
547
+ * Tracking de evento play.
548
+ */
549
+ trackPlay(videoId, currentTime = 0) {
550
+ const state = this.getOrCreateState(videoId);
551
+ state.playCount++;
552
+ state.isPlaying = true;
553
+ state.lastPlayTimestamp = Date.now();
554
+ state.currentTime = currentTime;
555
+ this.startWatchTimeTracking(videoId);
556
+ trackEvent("Video", "play", videoId, void 0, {
557
+ video_id: videoId,
558
+ play_count: state.playCount,
559
+ current_time: Math.round(currentTime)
560
+ });
561
+ }
562
+ /**
563
+ * Tracking de evento pause.
564
+ */
565
+ trackPause(videoId, currentTime, duration) {
566
+ const state = this.getOrCreateState(videoId);
567
+ state.pauseCount++;
568
+ state.isPlaying = false;
569
+ state.currentTime = currentTime;
570
+ state.pauseTimes.push(currentTime);
571
+ if (duration) {
572
+ state.duration = duration;
573
+ state.completionPercentage = Math.round(currentTime / duration * 100);
574
+ }
575
+ this.stopWatchTimeTracking(videoId);
576
+ trackEvent("Video", "pause", videoId, void 0, {
577
+ video_id: videoId,
578
+ pause_count: state.pauseCount,
579
+ current_time: Math.round(currentTime),
580
+ completion_percentage: state.completionPercentage,
581
+ total_watch_time: Math.round(state.totalWatchTime)
582
+ });
583
+ }
584
+ /**
585
+ * Tracking de evento seek (saltar en el timeline).
586
+ */
587
+ trackSeek(videoId, fromTime, toTime) {
588
+ const state = this.getOrCreateState(videoId);
589
+ state.seekCount++;
590
+ state.seekEvents.push({ from: fromTime, to: toTime });
591
+ trackEvent("Video", "seek", videoId, void 0, {
592
+ video_id: videoId,
593
+ seek_count: state.seekCount,
594
+ from_time: Math.round(fromTime),
595
+ to_time: Math.round(toTime),
596
+ skip_duration: Math.round(toTime - fromTime)
597
+ });
598
+ }
599
+ /**
600
+ * Tracking de progreso (milestones: 25%, 50%, 75%).
601
+ */
602
+ trackProgress(videoId, percentage, currentTime) {
603
+ const state = this.getOrCreateState(videoId);
604
+ const milestone = Math.floor(percentage / 25) * 25;
605
+ if (milestone > 0 && milestone < 100 && !state.progressMilestones.has(milestone)) {
606
+ state.progressMilestones.add(milestone);
607
+ trackEvent("Video", "progress", videoId, milestone, {
608
+ video_id: videoId,
609
+ progress_percentage: milestone,
610
+ current_time: Math.round(currentTime),
611
+ total_watch_time: Math.round(state.totalWatchTime)
612
+ });
613
+ }
614
+ }
615
+ /**
616
+ * Tracking de completación del video (≥95%).
617
+ */
618
+ trackComplete(videoId, totalDuration) {
619
+ const state = this.getOrCreateState(videoId);
620
+ state.completionPercentage = 100;
621
+ state.duration = totalDuration;
622
+ this.stopWatchTimeTracking(videoId);
623
+ trackEvent("Video", "complete", videoId, void 0, {
624
+ video_id: videoId,
625
+ total_duration: Math.round(totalDuration),
626
+ total_watch_time: Math.round(state.totalWatchTime),
627
+ play_count: state.playCount,
628
+ pause_count: state.pauseCount,
629
+ seek_count: state.seekCount
630
+ });
631
+ }
632
+ /**
633
+ * Tracking de fin del video (evento ended nativo).
634
+ */
635
+ trackEnd(videoId) {
636
+ const state = this.getOrCreateState(videoId);
637
+ state.isPlaying = false;
638
+ this.stopWatchTimeTracking(videoId);
639
+ trackEvent("Video", "end", videoId, void 0, {
640
+ video_id: videoId,
641
+ total_watch_time: Math.round(state.totalWatchTime),
642
+ play_count: state.playCount
643
+ });
644
+ }
645
+ /**
646
+ * Tracking de cambio de velocidad de reproducción.
647
+ */
648
+ trackSpeedChange(videoId, speed) {
649
+ const state = this.getOrCreateState(videoId);
650
+ state.playbackSpeed = speed;
651
+ trackEvent("Video", "speed_change", videoId, void 0, {
652
+ video_id: videoId,
653
+ playback_speed: speed
654
+ });
655
+ }
656
+ /**
657
+ * Tracking de pantalla completa.
658
+ */
659
+ trackFullscreen(videoId, isFullscreen) {
660
+ trackEvent("Video", isFullscreen ? "fullscreen_enter" : "fullscreen_exit", videoId, void 0, {
661
+ video_id: videoId,
662
+ is_fullscreen: isFullscreen
663
+ });
664
+ }
665
+ /**
666
+ * Tracking de no interacción (el usuario está en la página pero no interactúa con el video).
667
+ */
668
+ trackNoInteraction(videoId, timeOnPage) {
669
+ trackEvent("Video", "no_interaction", videoId, void 0, {
670
+ video_id: videoId,
671
+ time_on_page: Math.round(timeOnPage)
672
+ });
673
+ }
674
+ /**
675
+ * Obtiene el estado actual de un video.
676
+ */
677
+ getState(videoId) {
678
+ return this.videos.get(videoId);
679
+ }
680
+ /**
681
+ * Limpia el tracking de un video específico.
682
+ */
683
+ cleanup(videoId) {
684
+ this.stopWatchTimeTracking(videoId);
685
+ this.videos.delete(videoId);
686
+ }
687
+ /**
688
+ * Limpia todo el tracking.
689
+ */
690
+ cleanupAll() {
691
+ for (const videoId of this.videos.keys()) {
692
+ this.stopWatchTimeTracking(videoId);
693
+ }
694
+ this.videos.clear();
695
+ }
696
+ // --- Métodos privados ---
697
+ getOrCreateState(videoId) {
698
+ if (!this.videos.has(videoId)) {
699
+ this.initVideo(videoId);
700
+ }
701
+ return this.videos.get(videoId);
702
+ }
703
+ startWatchTimeTracking(videoId) {
704
+ this.stopWatchTimeTracking(videoId);
705
+ const interval = setInterval(() => {
706
+ const state = this.videos.get(videoId);
707
+ if (state?.isPlaying) {
708
+ state.totalWatchTime += 1;
709
+ }
710
+ }, 1e3);
711
+ this.watchTimeIntervals.set(videoId, interval);
712
+ }
713
+ stopWatchTimeTracking(videoId) {
714
+ const interval = this.watchTimeIntervals.get(videoId);
715
+ if (interval) {
716
+ clearInterval(interval);
717
+ this.watchTimeIntervals.delete(videoId);
718
+ }
719
+ }
720
+ };
721
+ var videoTracker = new VideoTracker();
722
+
723
+ // src/react/use-video-tracking.ts
724
+ function useVideoTracking({ videoId, videoElement, onComplete, onProgress }) {
725
+ const [progress, setProgress] = useState(0);
726
+ const [isPlaying, setIsPlaying] = useState(false);
727
+ const [currentTime, setCurrentTime] = useState(0);
728
+ const [duration, setDuration] = useState(0);
729
+ const milestonesRef = useRef(/* @__PURE__ */ new Set());
730
+ const noInteractionTimerRef = useRef(null);
731
+ useEffect(() => {
732
+ if (!videoElement) return;
733
+ videoTracker.initVideo(videoId);
734
+ const handlePlay = () => {
735
+ setIsPlaying(true);
736
+ videoTracker.trackPlay(videoId, videoElement.currentTime);
737
+ if (noInteractionTimerRef.current) {
738
+ clearTimeout(noInteractionTimerRef.current);
739
+ }
740
+ };
741
+ const handlePause = () => {
742
+ setIsPlaying(false);
743
+ videoTracker.trackPause(videoId, videoElement.currentTime, videoElement.duration);
744
+ };
745
+ const handleTimeUpdate = () => {
746
+ const ct = videoElement.currentTime;
747
+ const dur = videoElement.duration || 0;
748
+ const pct = dur > 0 ? Math.round(ct / dur * 100) : 0;
749
+ setCurrentTime(ct);
750
+ setDuration(dur);
751
+ setProgress(pct);
752
+ for (const milestone of [25, 50, 75]) {
753
+ if (pct >= milestone && !milestonesRef.current.has(milestone)) {
754
+ milestonesRef.current.add(milestone);
755
+ videoTracker.trackProgress(videoId, pct, ct);
756
+ onProgress?.(milestone);
757
+ }
758
+ }
759
+ if (pct >= 95 && !milestonesRef.current.has(100)) {
760
+ milestonesRef.current.add(100);
761
+ videoTracker.trackComplete(videoId, dur);
762
+ onComplete?.();
763
+ }
764
+ };
765
+ const handleSeeking = () => {
766
+ const state = videoTracker.getState(videoId);
767
+ if (state) {
768
+ videoTracker.trackSeek(videoId, state.currentTime, videoElement.currentTime);
769
+ }
770
+ };
771
+ const handleEnded = () => {
772
+ setIsPlaying(false);
773
+ videoTracker.trackEnd(videoId);
774
+ };
775
+ const handleRateChange = () => {
776
+ videoTracker.trackSpeedChange(videoId, videoElement.playbackRate);
777
+ };
778
+ videoElement.addEventListener("play", handlePlay);
779
+ videoElement.addEventListener("pause", handlePause);
780
+ videoElement.addEventListener("timeupdate", handleTimeUpdate);
781
+ videoElement.addEventListener("seeking", handleSeeking);
782
+ videoElement.addEventListener("ended", handleEnded);
783
+ videoElement.addEventListener("ratechange", handleRateChange);
784
+ noInteractionTimerRef.current = setTimeout(() => {
785
+ if (!videoElement.paused) return;
786
+ videoTracker.trackNoInteraction(videoId, 30);
787
+ }, 3e4);
788
+ return () => {
789
+ videoElement.removeEventListener("play", handlePlay);
790
+ videoElement.removeEventListener("pause", handlePause);
791
+ videoElement.removeEventListener("timeupdate", handleTimeUpdate);
792
+ videoElement.removeEventListener("seeking", handleSeeking);
793
+ videoElement.removeEventListener("ended", handleEnded);
794
+ videoElement.removeEventListener("ratechange", handleRateChange);
795
+ if (noInteractionTimerRef.current) {
796
+ clearTimeout(noInteractionTimerRef.current);
797
+ }
798
+ videoTracker.cleanup(videoId);
799
+ };
800
+ }, [videoId, videoElement, onComplete, onProgress]);
801
+ return { progress, isPlaying, currentTime, duration };
802
+ }
803
+ function useWistiaVideoTracking(mediaId) {
804
+ const [stats, setStats] = useState({
805
+ timeWatched: 0,
806
+ completed: false,
807
+ lastUpdate: Date.now()
808
+ });
809
+ const boundRef = useRef(false);
810
+ useEffect(() => {
811
+ if (!mediaId || boundRef.current) return;
812
+ const bindWistia = () => {
813
+ if (!window._wq) return false;
814
+ boundRef.current = true;
815
+ window._wq.push({
816
+ id: mediaId,
817
+ onReady: (video) => {
818
+ const v = video;
819
+ const update = () => {
820
+ const t = v.time();
821
+ const d = v.duration();
822
+ const pct = d > 0 ? Math.round(t / d * 100) : 0;
823
+ setStats({
824
+ timeWatched: Math.round(t),
825
+ completed: pct >= 95,
826
+ lastUpdate: Date.now()
827
+ });
828
+ };
829
+ v.bind("play", update);
830
+ v.bind("pause", update);
831
+ v.bind("secondchange", update);
832
+ v.bind("end", () => {
833
+ setStats({
834
+ timeWatched: Math.round(v.duration()),
835
+ completed: true,
836
+ lastUpdate: Date.now()
837
+ });
838
+ });
839
+ }
840
+ });
841
+ return true;
842
+ };
843
+ const interval = setInterval(() => {
844
+ if (bindWistia()) {
845
+ clearInterval(interval);
846
+ }
847
+ }, 100);
848
+ const timeout = setTimeout(() => clearInterval(interval), 15e3);
849
+ return () => {
850
+ clearInterval(interval);
851
+ clearTimeout(timeout);
852
+ };
853
+ }, [mediaId]);
854
+ return {
855
+ stats,
856
+ isCompleted: stats.completed
857
+ };
858
+ }
859
+
860
+ export { useLandingPageTracking, useVideoTracking, useWistiaVideoTracking };
861
+ //# sourceMappingURL=index.js.map
862
+ //# sourceMappingURL=index.js.map