@clianta/sdk 1.0.0 → 1.1.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.
@@ -0,0 +1,2657 @@
1
+ /*!
2
+ * Clianta SDK v1.1.1
3
+ * (c) 2026 Clianta
4
+ * Released under the MIT License.
5
+ */
6
+ import { jsx } from 'react/jsx-runtime';
7
+ import { createContext, useRef, useEffect, useContext } from 'react';
8
+
9
+ /**
10
+ * Clianta SDK - Configuration
11
+ * @see SDK_VERSION in core/config.ts
12
+ */
13
+ /** SDK Version */
14
+ const SDK_VERSION = '1.1.0';
15
+ /** Default API endpoint based on environment */
16
+ const getDefaultApiEndpoint = () => {
17
+ if (typeof window === 'undefined')
18
+ return 'https://api.clianta.online';
19
+ const hostname = window.location.hostname;
20
+ if (hostname.includes('localhost') || hostname.includes('127.0.0.1')) {
21
+ return 'http://localhost:5000';
22
+ }
23
+ return 'https://api.clianta.online';
24
+ };
25
+ /** Core plugins enabled by default */
26
+ const DEFAULT_PLUGINS = [
27
+ 'pageView',
28
+ 'forms',
29
+ 'scroll',
30
+ 'clicks',
31
+ 'engagement',
32
+ 'downloads',
33
+ 'exitIntent',
34
+ 'popupForms',
35
+ ];
36
+ /** Default configuration values */
37
+ const DEFAULT_CONFIG = {
38
+ projectId: '',
39
+ apiEndpoint: getDefaultApiEndpoint(),
40
+ authToken: '',
41
+ debug: false,
42
+ autoPageView: true,
43
+ plugins: DEFAULT_PLUGINS,
44
+ sessionTimeout: 30 * 60 * 1000, // 30 minutes
45
+ batchSize: 10,
46
+ flushInterval: 5000, // 5 seconds
47
+ consent: {
48
+ defaultConsent: { analytics: true, marketing: false, personalization: false },
49
+ waitForConsent: false,
50
+ storageKey: 'mb_consent',
51
+ anonymousMode: false,
52
+ },
53
+ cookieDomain: '',
54
+ useCookies: false,
55
+ cookielessMode: false,
56
+ };
57
+ /** Storage keys */
58
+ const STORAGE_KEYS = {
59
+ VISITOR_ID: 'mb_vid',
60
+ SESSION_ID: 'mb_sid',
61
+ SESSION_TIMESTAMP: 'mb_st',
62
+ CONSENT: 'mb_consent',
63
+ EVENT_QUEUE: 'mb_queue',
64
+ };
65
+ /** Scroll depth milestones to track */
66
+ const SCROLL_MILESTONES = [25, 50, 75, 100];
67
+ /** File extensions to track as downloads */
68
+ const DOWNLOAD_EXTENSIONS = [
69
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
70
+ '.zip', '.rar', '.tar', '.gz', '.7z',
71
+ '.csv', '.txt', '.json', '.xml',
72
+ '.mp3', '.mp4', '.wav', '.avi', '.mov',
73
+ ];
74
+ /**
75
+ * Merge user config with defaults
76
+ */
77
+ function mergeConfig(userConfig = {}) {
78
+ return {
79
+ ...DEFAULT_CONFIG,
80
+ ...userConfig,
81
+ consent: {
82
+ ...DEFAULT_CONFIG.consent,
83
+ ...userConfig.consent,
84
+ },
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Clianta SDK - Debug Logger
90
+ * @see SDK_VERSION in core/config.ts
91
+ */
92
+ const LOG_PREFIX = '[Clianta]';
93
+ const LOG_STYLES = {
94
+ debug: 'color: #6b7280; font-weight: normal;',
95
+ info: 'color: #3b82f6; font-weight: normal;',
96
+ warn: 'color: #f59e0b; font-weight: bold;',
97
+ error: 'color: #ef4444; font-weight: bold;',
98
+ };
99
+ const LOG_LEVELS = {
100
+ debug: 0,
101
+ info: 1,
102
+ warn: 2,
103
+ error: 3,
104
+ };
105
+ /**
106
+ * Create a logger instance
107
+ */
108
+ function createLogger(enabled = false) {
109
+ let currentLevel = 'debug';
110
+ let isEnabled = enabled;
111
+ const shouldLog = (level) => {
112
+ if (!isEnabled)
113
+ return false;
114
+ return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
115
+ };
116
+ const formatArgs = (level, args) => {
117
+ if (typeof console !== 'undefined' && typeof window !== 'undefined') {
118
+ // Browser with styled console
119
+ return [`%c${LOG_PREFIX}`, LOG_STYLES[level], ...args];
120
+ }
121
+ // Node.js or basic console
122
+ return [`${LOG_PREFIX} [${level.toUpperCase()}]`, ...args];
123
+ };
124
+ return {
125
+ get enabled() {
126
+ return isEnabled;
127
+ },
128
+ set enabled(value) {
129
+ isEnabled = value;
130
+ },
131
+ debug(...args) {
132
+ if (shouldLog('debug') && typeof console !== 'undefined') {
133
+ console.log(...formatArgs('debug', args));
134
+ }
135
+ },
136
+ info(...args) {
137
+ if (shouldLog('info') && typeof console !== 'undefined') {
138
+ console.info(...formatArgs('info', args));
139
+ }
140
+ },
141
+ warn(...args) {
142
+ if (shouldLog('warn') && typeof console !== 'undefined') {
143
+ console.warn(...formatArgs('warn', args));
144
+ }
145
+ },
146
+ error(...args) {
147
+ if (shouldLog('error') && typeof console !== 'undefined') {
148
+ console.error(...formatArgs('error', args));
149
+ }
150
+ },
151
+ setLevel(level) {
152
+ currentLevel = level;
153
+ },
154
+ };
155
+ }
156
+ /** Global logger instance */
157
+ const logger = createLogger(false);
158
+
159
+ /**
160
+ * Clianta SDK - Transport Layer
161
+ * Handles sending events to the backend with retry logic
162
+ * @see SDK_VERSION in core/config.ts
163
+ */
164
+ const DEFAULT_TIMEOUT = 10000; // 10 seconds
165
+ const DEFAULT_MAX_RETRIES = 3;
166
+ const DEFAULT_RETRY_DELAY = 1000; // 1 second
167
+ /**
168
+ * Transport class for sending data to the backend
169
+ */
170
+ class Transport {
171
+ constructor(config) {
172
+ this.config = {
173
+ apiEndpoint: config.apiEndpoint,
174
+ maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
175
+ retryDelay: config.retryDelay ?? DEFAULT_RETRY_DELAY,
176
+ timeout: config.timeout ?? DEFAULT_TIMEOUT,
177
+ };
178
+ }
179
+ /**
180
+ * Send events to the tracking endpoint
181
+ */
182
+ async sendEvents(events) {
183
+ const url = `${this.config.apiEndpoint}/api/public/track/event`;
184
+ const payload = JSON.stringify({ events });
185
+ return this.send(url, payload);
186
+ }
187
+ /**
188
+ * Send identify request
189
+ */
190
+ async sendIdentify(data) {
191
+ const url = `${this.config.apiEndpoint}/api/public/track/identify`;
192
+ const payload = JSON.stringify(data);
193
+ return this.send(url, payload);
194
+ }
195
+ /**
196
+ * Send events synchronously (for page unload)
197
+ * Uses navigator.sendBeacon for reliability
198
+ */
199
+ sendBeacon(events) {
200
+ if (typeof navigator === 'undefined' || !navigator.sendBeacon) {
201
+ logger.warn('sendBeacon not available, events may be lost');
202
+ return false;
203
+ }
204
+ const url = `${this.config.apiEndpoint}/api/public/track/event`;
205
+ const payload = JSON.stringify({ events });
206
+ const blob = new Blob([payload], { type: 'application/json' });
207
+ try {
208
+ const success = navigator.sendBeacon(url, blob);
209
+ if (success) {
210
+ logger.debug(`Beacon sent ${events.length} events`);
211
+ }
212
+ else {
213
+ logger.warn('sendBeacon returned false');
214
+ }
215
+ return success;
216
+ }
217
+ catch (error) {
218
+ logger.error('sendBeacon error:', error);
219
+ return false;
220
+ }
221
+ }
222
+ /**
223
+ * Internal send with retry logic
224
+ */
225
+ async send(url, payload, attempt = 1) {
226
+ try {
227
+ const response = await this.fetchWithTimeout(url, {
228
+ method: 'POST',
229
+ headers: {
230
+ 'Content-Type': 'application/json',
231
+ },
232
+ body: payload,
233
+ keepalive: true,
234
+ });
235
+ if (response.ok) {
236
+ logger.debug('Request successful:', url);
237
+ return { success: true, status: response.status };
238
+ }
239
+ // Server error - may retry
240
+ if (response.status >= 500 && attempt < this.config.maxRetries) {
241
+ logger.warn(`Server error (${response.status}), retrying...`);
242
+ await this.delay(this.config.retryDelay * attempt);
243
+ return this.send(url, payload, attempt + 1);
244
+ }
245
+ // Client error - don't retry
246
+ logger.error(`Request failed with status ${response.status}`);
247
+ return { success: false, status: response.status };
248
+ }
249
+ catch (error) {
250
+ // Network error - retry if possible
251
+ if (attempt < this.config.maxRetries) {
252
+ logger.warn(`Network error, retrying (${attempt}/${this.config.maxRetries})...`);
253
+ await this.delay(this.config.retryDelay * attempt);
254
+ return this.send(url, payload, attempt + 1);
255
+ }
256
+ logger.error('Request failed after retries:', error);
257
+ return { success: false, error: error };
258
+ }
259
+ }
260
+ /**
261
+ * Fetch with timeout
262
+ */
263
+ async fetchWithTimeout(url, options) {
264
+ const controller = new AbortController();
265
+ const timeout = setTimeout(() => controller.abort(), this.config.timeout);
266
+ try {
267
+ const response = await fetch(url, {
268
+ ...options,
269
+ signal: controller.signal,
270
+ });
271
+ return response;
272
+ }
273
+ finally {
274
+ clearTimeout(timeout);
275
+ }
276
+ }
277
+ /**
278
+ * Delay helper
279
+ */
280
+ delay(ms) {
281
+ return new Promise((resolve) => setTimeout(resolve, ms));
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Clianta SDK - Utility Functions
287
+ * @see SDK_VERSION in core/config.ts
288
+ */
289
+ // ============================================
290
+ // UUID GENERATION
291
+ // ============================================
292
+ /**
293
+ * Generate a UUID v4
294
+ */
295
+ function generateUUID() {
296
+ // Use crypto.randomUUID if available (modern browsers)
297
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
298
+ return crypto.randomUUID();
299
+ }
300
+ // Fallback to manual generation
301
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
302
+ const r = (Math.random() * 16) | 0;
303
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
304
+ return v.toString(16);
305
+ });
306
+ }
307
+ // ============================================
308
+ // STORAGE UTILITIES
309
+ // ============================================
310
+ /**
311
+ * Safely get from localStorage
312
+ */
313
+ function getLocalStorage(key) {
314
+ try {
315
+ if (typeof localStorage !== 'undefined') {
316
+ return localStorage.getItem(key);
317
+ }
318
+ }
319
+ catch {
320
+ // localStorage not available or blocked
321
+ }
322
+ return null;
323
+ }
324
+ /**
325
+ * Safely set to localStorage
326
+ */
327
+ function setLocalStorage(key, value) {
328
+ try {
329
+ if (typeof localStorage !== 'undefined') {
330
+ localStorage.setItem(key, value);
331
+ return true;
332
+ }
333
+ }
334
+ catch {
335
+ // localStorage not available or blocked
336
+ }
337
+ return false;
338
+ }
339
+ /**
340
+ * Safely get from sessionStorage
341
+ */
342
+ function getSessionStorage(key) {
343
+ try {
344
+ if (typeof sessionStorage !== 'undefined') {
345
+ return sessionStorage.getItem(key);
346
+ }
347
+ }
348
+ catch {
349
+ // sessionStorage not available or blocked
350
+ }
351
+ return null;
352
+ }
353
+ /**
354
+ * Safely set to sessionStorage
355
+ */
356
+ function setSessionStorage(key, value) {
357
+ try {
358
+ if (typeof sessionStorage !== 'undefined') {
359
+ sessionStorage.setItem(key, value);
360
+ return true;
361
+ }
362
+ }
363
+ catch {
364
+ // sessionStorage not available or blocked
365
+ }
366
+ return false;
367
+ }
368
+ /**
369
+ * Get or set a cookie
370
+ */
371
+ function cookie(name, value, days) {
372
+ if (typeof document === 'undefined')
373
+ return null;
374
+ // Get cookie
375
+ if (value === undefined) {
376
+ const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
377
+ return match ? match[2] : null;
378
+ }
379
+ // Set cookie
380
+ let expires = '';
381
+ if (days) {
382
+ const date = new Date();
383
+ date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
384
+ expires = '; expires=' + date.toUTCString();
385
+ }
386
+ document.cookie = name + '=' + value + expires + '; path=/; SameSite=Lax';
387
+ return value;
388
+ }
389
+ // ============================================
390
+ // VISITOR & SESSION MANAGEMENT
391
+ // ============================================
392
+ /**
393
+ * Get or create a persistent visitor ID
394
+ */
395
+ function getOrCreateVisitorId(useCookies = false) {
396
+ const key = STORAGE_KEYS.VISITOR_ID;
397
+ // Try to get existing ID
398
+ let visitorId = null;
399
+ if (useCookies) {
400
+ visitorId = cookie(key);
401
+ }
402
+ else {
403
+ visitorId = getLocalStorage(key);
404
+ }
405
+ // Create new ID if not found
406
+ if (!visitorId) {
407
+ visitorId = generateUUID();
408
+ if (useCookies) {
409
+ cookie(key, visitorId, 365); // 1 year
410
+ }
411
+ else {
412
+ setLocalStorage(key, visitorId);
413
+ }
414
+ }
415
+ return visitorId;
416
+ }
417
+ /**
418
+ * Get or create a session ID (expires after timeout)
419
+ */
420
+ function getOrCreateSessionId(timeout) {
421
+ const sidKey = STORAGE_KEYS.SESSION_ID;
422
+ const tsKey = STORAGE_KEYS.SESSION_TIMESTAMP;
423
+ let sessionId = getSessionStorage(sidKey);
424
+ const lastActivity = parseInt(getSessionStorage(tsKey) || '0', 10);
425
+ const now = Date.now();
426
+ // Check if session expired
427
+ if (!sessionId || now - lastActivity > timeout) {
428
+ sessionId = generateUUID();
429
+ setSessionStorage(sidKey, sessionId);
430
+ }
431
+ // Update last activity
432
+ setSessionStorage(tsKey, now.toString());
433
+ return sessionId;
434
+ }
435
+ /**
436
+ * Reset visitor and session IDs
437
+ */
438
+ function resetIds(useCookies = false) {
439
+ const visitorKey = STORAGE_KEYS.VISITOR_ID;
440
+ if (useCookies) {
441
+ cookie(visitorKey, '', -1); // Delete cookie
442
+ }
443
+ else {
444
+ try {
445
+ localStorage.removeItem(visitorKey);
446
+ }
447
+ catch {
448
+ // Ignore
449
+ }
450
+ }
451
+ try {
452
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_ID);
453
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_TIMESTAMP);
454
+ }
455
+ catch {
456
+ // Ignore
457
+ }
458
+ }
459
+ // ============================================
460
+ // URL UTILITIES
461
+ // ============================================
462
+ /**
463
+ * Extract UTM parameters from URL
464
+ */
465
+ function getUTMParams() {
466
+ if (typeof window === 'undefined')
467
+ return {};
468
+ try {
469
+ const params = new URLSearchParams(window.location.search);
470
+ return {
471
+ utmSource: params.get('utm_source') || undefined,
472
+ utmMedium: params.get('utm_medium') || undefined,
473
+ utmCampaign: params.get('utm_campaign') || undefined,
474
+ utmTerm: params.get('utm_term') || undefined,
475
+ utmContent: params.get('utm_content') || undefined,
476
+ };
477
+ }
478
+ catch {
479
+ return {};
480
+ }
481
+ }
482
+ /**
483
+ * Check if URL is a download link
484
+ */
485
+ function isDownloadUrl(url) {
486
+ const lowerUrl = url.toLowerCase();
487
+ return DOWNLOAD_EXTENSIONS.some((ext) => lowerUrl.includes(ext));
488
+ }
489
+ /**
490
+ * Extract filename from URL
491
+ */
492
+ function getFilenameFromUrl(url) {
493
+ try {
494
+ return url.split('/').pop()?.split('?')[0] || 'unknown';
495
+ }
496
+ catch {
497
+ return 'unknown';
498
+ }
499
+ }
500
+ /**
501
+ * Extract file extension from URL
502
+ */
503
+ function getFileExtension(url) {
504
+ const filename = getFilenameFromUrl(url);
505
+ const parts = filename.split('.');
506
+ return parts.length > 1 ? parts.pop() || 'unknown' : 'unknown';
507
+ }
508
+ // ============================================
509
+ // DOM UTILITIES
510
+ // ============================================
511
+ /**
512
+ * Get text content from element (truncated)
513
+ */
514
+ function getElementText(element, maxLength = 100) {
515
+ const text = element.innerText ||
516
+ element.textContent ||
517
+ element.value ||
518
+ '';
519
+ return text.trim().substring(0, maxLength);
520
+ }
521
+ /**
522
+ * Get element identification info
523
+ */
524
+ function getElementInfo(element) {
525
+ return {
526
+ tag: element.tagName?.toLowerCase() || 'unknown',
527
+ id: element.id || '',
528
+ className: element.className || '',
529
+ text: getElementText(element, 50),
530
+ };
531
+ }
532
+ /**
533
+ * Check if element is a trackable click target
534
+ */
535
+ function isTrackableClickElement(element) {
536
+ const trackableTags = ['BUTTON', 'A', 'INPUT'];
537
+ return (trackableTags.includes(element.tagName) ||
538
+ element.hasAttribute('data-track-click') ||
539
+ element.classList.contains('track-click'));
540
+ }
541
+ /**
542
+ * Check if device is mobile
543
+ */
544
+ function isMobile() {
545
+ if (typeof navigator === 'undefined')
546
+ return false;
547
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
548
+ }
549
+ // ============================================
550
+ // DEVICE INFO
551
+ // ============================================
552
+ /**
553
+ * Get current device information
554
+ */
555
+ function getDeviceInfo() {
556
+ if (typeof window === 'undefined' || typeof navigator === 'undefined') {
557
+ return {
558
+ userAgent: 'unknown',
559
+ screen: 'unknown',
560
+ language: 'unknown',
561
+ timezone: 'unknown',
562
+ };
563
+ }
564
+ return {
565
+ userAgent: navigator.userAgent,
566
+ screen: `${screen.width}x${screen.height}`,
567
+ language: navigator.language,
568
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'unknown',
569
+ };
570
+ }
571
+
572
+ /**
573
+ * Clianta SDK - Event Queue
574
+ * Handles batching and flushing of events
575
+ * @see SDK_VERSION in core/config.ts
576
+ */
577
+ const MAX_QUEUE_SIZE = 1000;
578
+ /**
579
+ * Event queue with batching, persistence, and auto-flush
580
+ */
581
+ class EventQueue {
582
+ constructor(transport, config = {}) {
583
+ this.queue = [];
584
+ this.flushTimer = null;
585
+ this.isFlushing = false;
586
+ this.transport = transport;
587
+ this.config = {
588
+ batchSize: config.batchSize ?? 10,
589
+ flushInterval: config.flushInterval ?? 5000,
590
+ maxQueueSize: config.maxQueueSize ?? MAX_QUEUE_SIZE,
591
+ storageKey: config.storageKey ?? STORAGE_KEYS.EVENT_QUEUE,
592
+ };
593
+ // Restore persisted queue
594
+ this.restoreQueue();
595
+ // Start auto-flush timer
596
+ this.startFlushTimer();
597
+ // Setup unload handlers
598
+ this.setupUnloadHandlers();
599
+ }
600
+ /**
601
+ * Add an event to the queue
602
+ */
603
+ push(event) {
604
+ // Don't exceed max queue size
605
+ if (this.queue.length >= this.config.maxQueueSize) {
606
+ logger.warn('Queue full, dropping oldest event');
607
+ this.queue.shift();
608
+ }
609
+ this.queue.push(event);
610
+ logger.debug('Event queued:', event.eventName, `(${this.queue.length} in queue)`);
611
+ // Flush if batch size reached
612
+ if (this.queue.length >= this.config.batchSize) {
613
+ this.flush();
614
+ }
615
+ }
616
+ /**
617
+ * Flush the queue (send all events)
618
+ */
619
+ async flush() {
620
+ if (this.isFlushing || this.queue.length === 0) {
621
+ return;
622
+ }
623
+ this.isFlushing = true;
624
+ try {
625
+ // Take all events from queue
626
+ const events = this.queue.splice(0, this.queue.length);
627
+ logger.debug(`Flushing ${events.length} events`);
628
+ // Clear persisted queue
629
+ this.persistQueue([]);
630
+ // Send to backend
631
+ const result = await this.transport.sendEvents(events);
632
+ if (!result.success) {
633
+ // Re-queue events on failure (at the front)
634
+ logger.warn('Flush failed, re-queuing events');
635
+ this.queue.unshift(...events);
636
+ this.persistQueue(this.queue);
637
+ }
638
+ else {
639
+ logger.debug('Flush successful');
640
+ }
641
+ }
642
+ catch (error) {
643
+ logger.error('Flush error:', error);
644
+ }
645
+ finally {
646
+ this.isFlushing = false;
647
+ }
648
+ }
649
+ /**
650
+ * Flush synchronously using sendBeacon (for page unload)
651
+ */
652
+ flushSync() {
653
+ if (this.queue.length === 0)
654
+ return;
655
+ const events = this.queue.splice(0, this.queue.length);
656
+ logger.debug(`Sync flushing ${events.length} events via beacon`);
657
+ const success = this.transport.sendBeacon(events);
658
+ if (!success) {
659
+ // Re-queue and persist for next page load
660
+ this.queue.unshift(...events);
661
+ this.persistQueue(this.queue);
662
+ }
663
+ }
664
+ /**
665
+ * Get current queue length
666
+ */
667
+ get length() {
668
+ return this.queue.length;
669
+ }
670
+ /**
671
+ * Clear the queue
672
+ */
673
+ clear() {
674
+ this.queue = [];
675
+ this.persistQueue([]);
676
+ }
677
+ /**
678
+ * Stop the flush timer
679
+ */
680
+ destroy() {
681
+ if (this.flushTimer) {
682
+ clearInterval(this.flushTimer);
683
+ this.flushTimer = null;
684
+ }
685
+ }
686
+ /**
687
+ * Start auto-flush timer
688
+ */
689
+ startFlushTimer() {
690
+ if (this.flushTimer) {
691
+ clearInterval(this.flushTimer);
692
+ }
693
+ this.flushTimer = setInterval(() => {
694
+ this.flush();
695
+ }, this.config.flushInterval);
696
+ }
697
+ /**
698
+ * Setup page unload handlers
699
+ */
700
+ setupUnloadHandlers() {
701
+ if (typeof window === 'undefined')
702
+ return;
703
+ // Flush on page unload
704
+ window.addEventListener('beforeunload', () => {
705
+ this.flushSync();
706
+ });
707
+ // Flush when page becomes hidden
708
+ window.addEventListener('visibilitychange', () => {
709
+ if (document.visibilityState === 'hidden') {
710
+ this.flushSync();
711
+ }
712
+ });
713
+ // Flush on page hide (iOS Safari)
714
+ window.addEventListener('pagehide', () => {
715
+ this.flushSync();
716
+ });
717
+ }
718
+ /**
719
+ * Persist queue to localStorage
720
+ */
721
+ persistQueue(events) {
722
+ try {
723
+ setLocalStorage(this.config.storageKey, JSON.stringify(events));
724
+ }
725
+ catch {
726
+ // Ignore storage errors
727
+ }
728
+ }
729
+ /**
730
+ * Restore queue from localStorage
731
+ */
732
+ restoreQueue() {
733
+ try {
734
+ const stored = getLocalStorage(this.config.storageKey);
735
+ if (stored) {
736
+ const events = JSON.parse(stored);
737
+ if (Array.isArray(events) && events.length > 0) {
738
+ this.queue = events;
739
+ logger.debug(`Restored ${events.length} events from storage`);
740
+ }
741
+ }
742
+ }
743
+ catch {
744
+ // Ignore parse errors
745
+ }
746
+ }
747
+ }
748
+
749
+ /**
750
+ * Clianta SDK - Plugin Base
751
+ * @see SDK_VERSION in core/config.ts
752
+ */
753
+ /**
754
+ * Base class for plugins
755
+ */
756
+ class BasePlugin {
757
+ constructor() {
758
+ this.tracker = null;
759
+ }
760
+ init(tracker) {
761
+ this.tracker = tracker;
762
+ }
763
+ destroy() {
764
+ this.tracker = null;
765
+ }
766
+ track(eventType, eventName, properties) {
767
+ if (this.tracker) {
768
+ this.tracker.track(eventType, eventName, properties);
769
+ }
770
+ }
771
+ }
772
+
773
+ /**
774
+ * Clianta SDK - Page View Plugin
775
+ * @see SDK_VERSION in core/config.ts
776
+ */
777
+ /**
778
+ * Page View Plugin - Tracks page views
779
+ */
780
+ class PageViewPlugin extends BasePlugin {
781
+ constructor() {
782
+ super(...arguments);
783
+ this.name = 'pageView';
784
+ }
785
+ init(tracker) {
786
+ super.init(tracker);
787
+ // Track initial page view
788
+ this.trackPageView();
789
+ // Track SPA navigation (History API)
790
+ if (typeof window !== 'undefined') {
791
+ // Intercept pushState and replaceState
792
+ const originalPushState = history.pushState;
793
+ const originalReplaceState = history.replaceState;
794
+ history.pushState = (...args) => {
795
+ originalPushState.apply(history, args);
796
+ this.trackPageView();
797
+ };
798
+ history.replaceState = (...args) => {
799
+ originalReplaceState.apply(history, args);
800
+ this.trackPageView();
801
+ };
802
+ // Handle back/forward navigation
803
+ window.addEventListener('popstate', () => {
804
+ this.trackPageView();
805
+ });
806
+ }
807
+ }
808
+ trackPageView() {
809
+ if (typeof window === 'undefined' || typeof document === 'undefined')
810
+ return;
811
+ this.track('page_view', 'Page Viewed', {
812
+ title: document.title,
813
+ path: window.location.pathname,
814
+ search: window.location.search,
815
+ hash: window.location.hash,
816
+ referrer: document.referrer || 'direct',
817
+ viewport: `${window.innerWidth}x${window.innerHeight}`,
818
+ });
819
+ }
820
+ }
821
+
822
+ /**
823
+ * Clianta SDK - Scroll Depth Plugin
824
+ * @see SDK_VERSION in core/config.ts
825
+ */
826
+ /**
827
+ * Scroll Depth Plugin - Tracks scroll milestones
828
+ */
829
+ class ScrollPlugin extends BasePlugin {
830
+ constructor() {
831
+ super(...arguments);
832
+ this.name = 'scroll';
833
+ this.milestonesReached = new Set();
834
+ this.maxScrollDepth = 0;
835
+ this.pageLoadTime = 0;
836
+ this.scrollTimeout = null;
837
+ this.boundHandler = null;
838
+ }
839
+ init(tracker) {
840
+ super.init(tracker);
841
+ this.pageLoadTime = Date.now();
842
+ if (typeof window !== 'undefined') {
843
+ this.boundHandler = this.handleScroll.bind(this);
844
+ window.addEventListener('scroll', this.boundHandler, { passive: true });
845
+ }
846
+ }
847
+ destroy() {
848
+ if (this.boundHandler && typeof window !== 'undefined') {
849
+ window.removeEventListener('scroll', this.boundHandler);
850
+ }
851
+ if (this.scrollTimeout) {
852
+ clearTimeout(this.scrollTimeout);
853
+ }
854
+ super.destroy();
855
+ }
856
+ handleScroll() {
857
+ // Debounce scroll tracking
858
+ if (this.scrollTimeout) {
859
+ clearTimeout(this.scrollTimeout);
860
+ }
861
+ this.scrollTimeout = setTimeout(() => this.trackScrollDepth(), 150);
862
+ }
863
+ trackScrollDepth() {
864
+ if (typeof window === 'undefined' || typeof document === 'undefined')
865
+ return;
866
+ const windowHeight = window.innerHeight;
867
+ const documentHeight = document.documentElement.scrollHeight;
868
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
869
+ const scrollPercent = Math.floor((scrollTop / (documentHeight - windowHeight)) * 100);
870
+ // Clamp to valid range
871
+ const clampedPercent = Math.max(0, Math.min(100, scrollPercent));
872
+ // Update max scroll depth
873
+ if (clampedPercent > this.maxScrollDepth) {
874
+ this.maxScrollDepth = clampedPercent;
875
+ }
876
+ // Track milestones
877
+ for (const milestone of SCROLL_MILESTONES) {
878
+ if (clampedPercent >= milestone && !this.milestonesReached.has(milestone)) {
879
+ this.milestonesReached.add(milestone);
880
+ this.track('scroll_depth', `Scrolled ${milestone}%`, {
881
+ depth: milestone,
882
+ maxDepth: this.maxScrollDepth,
883
+ timeToReach: Date.now() - this.pageLoadTime,
884
+ });
885
+ }
886
+ }
887
+ }
888
+ }
889
+
890
+ /**
891
+ * Clianta SDK - Form Tracking Plugin
892
+ * @see SDK_VERSION in core/config.ts
893
+ */
894
+ /**
895
+ * Form Tracking Plugin - Auto-tracks form views, interactions, and submissions
896
+ */
897
+ class FormsPlugin extends BasePlugin {
898
+ constructor() {
899
+ super(...arguments);
900
+ this.name = 'forms';
901
+ this.trackedForms = new WeakSet();
902
+ this.formInteractions = new Set();
903
+ this.observer = null;
904
+ }
905
+ init(tracker) {
906
+ super.init(tracker);
907
+ if (typeof document === 'undefined')
908
+ return;
909
+ // Track existing forms
910
+ this.trackAllForms();
911
+ // Watch for dynamically added forms
912
+ if (typeof MutationObserver !== 'undefined') {
913
+ this.observer = new MutationObserver(() => this.trackAllForms());
914
+ this.observer.observe(document.body, { childList: true, subtree: true });
915
+ }
916
+ }
917
+ destroy() {
918
+ if (this.observer) {
919
+ this.observer.disconnect();
920
+ this.observer = null;
921
+ }
922
+ super.destroy();
923
+ }
924
+ trackAllForms() {
925
+ document.querySelectorAll('form').forEach((form) => {
926
+ this.setupFormTracking(form);
927
+ });
928
+ }
929
+ setupFormTracking(form) {
930
+ if (this.trackedForms.has(form))
931
+ return;
932
+ this.trackedForms.add(form);
933
+ const formId = form.id || form.name || `form-${Math.random().toString(36).substr(2, 9)}`;
934
+ // Track form view
935
+ this.track('form_view', 'Form Viewed', {
936
+ formId,
937
+ action: form.action,
938
+ method: form.method,
939
+ fieldCount: form.elements.length,
940
+ });
941
+ // Track field interactions
942
+ Array.from(form.elements).forEach((field) => {
943
+ if (field instanceof HTMLInputElement ||
944
+ field instanceof HTMLSelectElement ||
945
+ field instanceof HTMLTextAreaElement) {
946
+ if (!field.name || field.type === 'submit' || field.type === 'button')
947
+ return;
948
+ ['focus', 'blur', 'change'].forEach((eventType) => {
949
+ field.addEventListener(eventType, () => {
950
+ const key = `${formId}-${field.name}-${eventType}`;
951
+ if (!this.formInteractions.has(key)) {
952
+ this.formInteractions.add(key);
953
+ this.track('form_interaction', 'Form Field Interaction', {
954
+ formId,
955
+ fieldName: field.name,
956
+ fieldType: field.type,
957
+ interactionType: eventType,
958
+ });
959
+ }
960
+ });
961
+ });
962
+ }
963
+ });
964
+ // Track form submission
965
+ form.addEventListener('submit', () => {
966
+ this.track('form_submit', 'Form Submitted', {
967
+ formId,
968
+ action: form.action,
969
+ method: form.method,
970
+ });
971
+ // Auto-identify if email field found
972
+ this.autoIdentify(form);
973
+ });
974
+ }
975
+ autoIdentify(form) {
976
+ const emailField = form.querySelector('input[type="email"], input[name*="email"]');
977
+ if (!emailField?.value || !this.tracker)
978
+ return;
979
+ const email = emailField.value;
980
+ const traits = {};
981
+ // Capture common fields
982
+ const firstNameField = form.querySelector('[name*="first"], [name*="fname"]');
983
+ const lastNameField = form.querySelector('[name*="last"], [name*="lname"]');
984
+ const companyField = form.querySelector('[name*="company"], [name*="organization"]');
985
+ const phoneField = form.querySelector('[type="tel"], [name*="phone"]');
986
+ if (firstNameField?.value)
987
+ traits.firstName = firstNameField.value;
988
+ if (lastNameField?.value)
989
+ traits.lastName = lastNameField.value;
990
+ if (companyField?.value)
991
+ traits.company = companyField.value;
992
+ if (phoneField?.value)
993
+ traits.phone = phoneField.value;
994
+ this.tracker.identify(email, traits);
995
+ }
996
+ }
997
+
998
+ /**
999
+ * Clianta SDK - Click Tracking Plugin
1000
+ * @see SDK_VERSION in core/config.ts
1001
+ */
1002
+ /**
1003
+ * Click Tracking Plugin - Tracks button and CTA clicks
1004
+ */
1005
+ class ClicksPlugin extends BasePlugin {
1006
+ constructor() {
1007
+ super(...arguments);
1008
+ this.name = 'clicks';
1009
+ this.boundHandler = null;
1010
+ }
1011
+ init(tracker) {
1012
+ super.init(tracker);
1013
+ if (typeof document !== 'undefined') {
1014
+ this.boundHandler = this.handleClick.bind(this);
1015
+ document.addEventListener('click', this.boundHandler, true);
1016
+ }
1017
+ }
1018
+ destroy() {
1019
+ if (this.boundHandler && typeof document !== 'undefined') {
1020
+ document.removeEventListener('click', this.boundHandler, true);
1021
+ }
1022
+ super.destroy();
1023
+ }
1024
+ handleClick(e) {
1025
+ const target = e.target;
1026
+ if (!target || !isTrackableClickElement(target))
1027
+ return;
1028
+ const buttonText = getElementText(target, 100);
1029
+ const elementInfo = getElementInfo(target);
1030
+ this.track('button_click', 'Button Clicked', {
1031
+ buttonText,
1032
+ elementType: target.tagName.toLowerCase(),
1033
+ elementId: elementInfo.id,
1034
+ elementClass: elementInfo.className,
1035
+ href: target.href || undefined,
1036
+ });
1037
+ }
1038
+ }
1039
+
1040
+ /**
1041
+ * Clianta SDK - Engagement Plugin
1042
+ * @see SDK_VERSION in core/config.ts
1043
+ */
1044
+ /**
1045
+ * Engagement Plugin - Tracks user engagement and time on page
1046
+ */
1047
+ class EngagementPlugin extends BasePlugin {
1048
+ constructor() {
1049
+ super(...arguments);
1050
+ this.name = 'engagement';
1051
+ this.pageLoadTime = 0;
1052
+ this.engagementStartTime = 0;
1053
+ this.isEngaged = false;
1054
+ this.engagementTimeout = null;
1055
+ this.boundMarkEngaged = null;
1056
+ this.boundTrackTimeOnPage = null;
1057
+ }
1058
+ init(tracker) {
1059
+ super.init(tracker);
1060
+ this.pageLoadTime = Date.now();
1061
+ this.engagementStartTime = Date.now();
1062
+ if (typeof document === 'undefined' || typeof window === 'undefined')
1063
+ return;
1064
+ // Setup engagement detection
1065
+ this.boundMarkEngaged = this.markEngaged.bind(this);
1066
+ this.boundTrackTimeOnPage = this.trackTimeOnPage.bind(this);
1067
+ ['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
1068
+ document.addEventListener(event, this.boundMarkEngaged, { passive: true });
1069
+ });
1070
+ // Track time on page before unload
1071
+ window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
1072
+ window.addEventListener('visibilitychange', () => {
1073
+ if (document.visibilityState === 'hidden') {
1074
+ this.trackTimeOnPage();
1075
+ }
1076
+ else {
1077
+ // Reset engagement timer when page becomes visible again
1078
+ this.engagementStartTime = Date.now();
1079
+ }
1080
+ });
1081
+ }
1082
+ destroy() {
1083
+ if (this.boundMarkEngaged && typeof document !== 'undefined') {
1084
+ ['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
1085
+ document.removeEventListener(event, this.boundMarkEngaged);
1086
+ });
1087
+ }
1088
+ if (this.boundTrackTimeOnPage && typeof window !== 'undefined') {
1089
+ window.removeEventListener('beforeunload', this.boundTrackTimeOnPage);
1090
+ }
1091
+ if (this.engagementTimeout) {
1092
+ clearTimeout(this.engagementTimeout);
1093
+ }
1094
+ super.destroy();
1095
+ }
1096
+ markEngaged() {
1097
+ if (!this.isEngaged) {
1098
+ this.isEngaged = true;
1099
+ this.track('engagement', 'User Engaged', {
1100
+ timeToEngage: Date.now() - this.pageLoadTime,
1101
+ });
1102
+ }
1103
+ // Reset engagement timeout
1104
+ if (this.engagementTimeout) {
1105
+ clearTimeout(this.engagementTimeout);
1106
+ }
1107
+ this.engagementTimeout = setTimeout(() => {
1108
+ this.isEngaged = false;
1109
+ }, 30000); // 30 seconds of inactivity
1110
+ }
1111
+ trackTimeOnPage() {
1112
+ const timeSpent = Math.floor((Date.now() - this.engagementStartTime) / 1000);
1113
+ if (timeSpent > 0) {
1114
+ this.track('time_on_page', 'Time Spent', {
1115
+ seconds: timeSpent,
1116
+ engaged: this.isEngaged,
1117
+ });
1118
+ }
1119
+ }
1120
+ }
1121
+
1122
+ /**
1123
+ * Clianta SDK - Downloads Plugin
1124
+ * @see SDK_VERSION in core/config.ts
1125
+ */
1126
+ /**
1127
+ * Downloads Plugin - Tracks file downloads
1128
+ */
1129
+ class DownloadsPlugin extends BasePlugin {
1130
+ constructor() {
1131
+ super(...arguments);
1132
+ this.name = 'downloads';
1133
+ this.trackedDownloads = new Set();
1134
+ this.boundHandler = null;
1135
+ }
1136
+ init(tracker) {
1137
+ super.init(tracker);
1138
+ if (typeof document !== 'undefined') {
1139
+ this.boundHandler = this.handleClick.bind(this);
1140
+ document.addEventListener('click', this.boundHandler, true);
1141
+ }
1142
+ }
1143
+ destroy() {
1144
+ if (this.boundHandler && typeof document !== 'undefined') {
1145
+ document.removeEventListener('click', this.boundHandler, true);
1146
+ }
1147
+ super.destroy();
1148
+ }
1149
+ handleClick(e) {
1150
+ const link = e.target.closest('a');
1151
+ if (!link || !link.href)
1152
+ return;
1153
+ const url = link.href;
1154
+ // Check if it's a download link
1155
+ if (!isDownloadUrl(url))
1156
+ return;
1157
+ // Avoid tracking the same download multiple times
1158
+ if (this.trackedDownloads.has(url))
1159
+ return;
1160
+ this.trackedDownloads.add(url);
1161
+ this.track('download', 'File Download', {
1162
+ url,
1163
+ filename: getFilenameFromUrl(url),
1164
+ fileType: getFileExtension(url),
1165
+ linkText: getElementText(link, 100),
1166
+ });
1167
+ }
1168
+ }
1169
+
1170
+ /**
1171
+ * Clianta SDK - Exit Intent Plugin
1172
+ * @see SDK_VERSION in core/config.ts
1173
+ */
1174
+ /**
1175
+ * Exit Intent Plugin - Detects when user intends to leave the page
1176
+ */
1177
+ class ExitIntentPlugin extends BasePlugin {
1178
+ constructor() {
1179
+ super(...arguments);
1180
+ this.name = 'exitIntent';
1181
+ this.exitIntentShown = false;
1182
+ this.pageLoadTime = 0;
1183
+ this.boundHandler = null;
1184
+ }
1185
+ init(tracker) {
1186
+ super.init(tracker);
1187
+ this.pageLoadTime = Date.now();
1188
+ // Skip on mobile (no mouse events)
1189
+ if (isMobile())
1190
+ return;
1191
+ if (typeof document !== 'undefined') {
1192
+ this.boundHandler = this.handleMouseLeave.bind(this);
1193
+ document.addEventListener('mouseleave', this.boundHandler);
1194
+ }
1195
+ }
1196
+ destroy() {
1197
+ if (this.boundHandler && typeof document !== 'undefined') {
1198
+ document.removeEventListener('mouseleave', this.boundHandler);
1199
+ }
1200
+ super.destroy();
1201
+ }
1202
+ handleMouseLeave(e) {
1203
+ // Only trigger when mouse leaves from the top of the page
1204
+ if (e.clientY > 0 || this.exitIntentShown)
1205
+ return;
1206
+ this.exitIntentShown = true;
1207
+ this.track('exit_intent', 'Exit Intent Detected', {
1208
+ timeOnPage: Date.now() - this.pageLoadTime,
1209
+ });
1210
+ }
1211
+ }
1212
+
1213
+ /**
1214
+ * Clianta SDK - Error Tracking Plugin
1215
+ * @see SDK_VERSION in core/config.ts
1216
+ */
1217
+ /**
1218
+ * Error Tracking Plugin - Tracks JavaScript errors
1219
+ */
1220
+ class ErrorsPlugin extends BasePlugin {
1221
+ constructor() {
1222
+ super(...arguments);
1223
+ this.name = 'errors';
1224
+ this.boundErrorHandler = null;
1225
+ this.boundRejectionHandler = null;
1226
+ }
1227
+ init(tracker) {
1228
+ super.init(tracker);
1229
+ if (typeof window !== 'undefined') {
1230
+ this.boundErrorHandler = this.handleError.bind(this);
1231
+ this.boundRejectionHandler = this.handleRejection.bind(this);
1232
+ window.addEventListener('error', this.boundErrorHandler);
1233
+ window.addEventListener('unhandledrejection', this.boundRejectionHandler);
1234
+ }
1235
+ }
1236
+ destroy() {
1237
+ if (typeof window !== 'undefined') {
1238
+ if (this.boundErrorHandler) {
1239
+ window.removeEventListener('error', this.boundErrorHandler);
1240
+ }
1241
+ if (this.boundRejectionHandler) {
1242
+ window.removeEventListener('unhandledrejection', this.boundRejectionHandler);
1243
+ }
1244
+ }
1245
+ super.destroy();
1246
+ }
1247
+ handleError(e) {
1248
+ this.track('error', 'JavaScript Error', {
1249
+ message: e.message,
1250
+ filename: e.filename,
1251
+ line: e.lineno,
1252
+ column: e.colno,
1253
+ stack: e.error?.stack?.substring(0, 500),
1254
+ });
1255
+ }
1256
+ handleRejection(e) {
1257
+ this.track('error', 'Unhandled Promise Rejection', {
1258
+ reason: String(e.reason).substring(0, 200),
1259
+ });
1260
+ }
1261
+ }
1262
+
1263
+ /**
1264
+ * Clianta SDK - Performance Plugin
1265
+ * @see SDK_VERSION in core/config.ts
1266
+ */
1267
+ /**
1268
+ * Performance Plugin - Tracks page performance and Web Vitals
1269
+ */
1270
+ class PerformancePlugin extends BasePlugin {
1271
+ constructor() {
1272
+ super(...arguments);
1273
+ this.name = 'performance';
1274
+ }
1275
+ init(tracker) {
1276
+ super.init(tracker);
1277
+ if (typeof window !== 'undefined') {
1278
+ // Track performance after page load
1279
+ window.addEventListener('load', () => {
1280
+ // Delay to ensure all metrics are available
1281
+ setTimeout(() => this.trackPerformance(), 100);
1282
+ });
1283
+ }
1284
+ }
1285
+ trackPerformance() {
1286
+ if (typeof performance === 'undefined')
1287
+ return;
1288
+ // Use Navigation Timing API
1289
+ const timing = performance.timing;
1290
+ if (!timing)
1291
+ return;
1292
+ const loadTime = timing.loadEventEnd - timing.navigationStart;
1293
+ const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;
1294
+ const ttfb = timing.responseStart - timing.navigationStart;
1295
+ const domInteractive = timing.domInteractive - timing.navigationStart;
1296
+ this.track('performance', 'Page Performance', {
1297
+ loadTime,
1298
+ domReady,
1299
+ ttfb, // Time to First Byte
1300
+ domInteractive,
1301
+ });
1302
+ // Track Web Vitals if available
1303
+ this.trackWebVitals();
1304
+ }
1305
+ trackWebVitals() {
1306
+ // LCP (Largest Contentful Paint)
1307
+ if ('PerformanceObserver' in window) {
1308
+ try {
1309
+ const lcpObserver = new PerformanceObserver((entryList) => {
1310
+ const entries = entryList.getEntries();
1311
+ const lastEntry = entries[entries.length - 1];
1312
+ if (lastEntry) {
1313
+ this.track('performance', 'Web Vital - LCP', {
1314
+ metric: 'LCP',
1315
+ value: Math.round(lastEntry.startTime),
1316
+ });
1317
+ }
1318
+ });
1319
+ lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
1320
+ }
1321
+ catch {
1322
+ // LCP not supported
1323
+ }
1324
+ // FID (First Input Delay)
1325
+ try {
1326
+ const fidObserver = new PerformanceObserver((entryList) => {
1327
+ const entries = entryList.getEntries();
1328
+ const firstEntry = entries[0];
1329
+ if (firstEntry) {
1330
+ this.track('performance', 'Web Vital - FID', {
1331
+ metric: 'FID',
1332
+ value: Math.round(firstEntry.processingStart - firstEntry.startTime),
1333
+ });
1334
+ }
1335
+ });
1336
+ fidObserver.observe({ type: 'first-input', buffered: true });
1337
+ }
1338
+ catch {
1339
+ // FID not supported
1340
+ }
1341
+ // CLS (Cumulative Layout Shift)
1342
+ try {
1343
+ let clsValue = 0;
1344
+ const clsObserver = new PerformanceObserver((entryList) => {
1345
+ const entries = entryList.getEntries();
1346
+ entries.forEach((entry) => {
1347
+ if (!entry.hadRecentInput) {
1348
+ clsValue += entry.value || 0;
1349
+ }
1350
+ });
1351
+ });
1352
+ clsObserver.observe({ type: 'layout-shift', buffered: true });
1353
+ // Report CLS after page is hidden
1354
+ window.addEventListener('visibilitychange', () => {
1355
+ if (document.visibilityState === 'hidden' && clsValue > 0) {
1356
+ this.track('performance', 'Web Vital - CLS', {
1357
+ metric: 'CLS',
1358
+ value: Math.round(clsValue * 1000) / 1000,
1359
+ });
1360
+ }
1361
+ }, { once: true });
1362
+ }
1363
+ catch {
1364
+ // CLS not supported
1365
+ }
1366
+ }
1367
+ }
1368
+ }
1369
+
1370
+ /**
1371
+ * Clianta Tracking SDK - Popup Forms Plugin
1372
+ * @see SDK_VERSION in core/config.ts
1373
+ *
1374
+ * Auto-loads and displays lead capture popups based on triggers
1375
+ */
1376
+ /**
1377
+ * Popup Forms Plugin - Fetches and displays lead capture forms
1378
+ */
1379
+ class PopupFormsPlugin extends BasePlugin {
1380
+ constructor() {
1381
+ super(...arguments);
1382
+ this.name = 'popupForms';
1383
+ this.forms = [];
1384
+ this.shownForms = new Set();
1385
+ this.scrollHandler = null;
1386
+ this.exitHandler = null;
1387
+ }
1388
+ async init(tracker) {
1389
+ super.init(tracker);
1390
+ if (typeof window === 'undefined')
1391
+ return;
1392
+ // Load shown forms from storage
1393
+ this.loadShownForms();
1394
+ // Fetch active forms
1395
+ await this.fetchForms();
1396
+ // Setup triggers
1397
+ this.setupTriggers();
1398
+ }
1399
+ destroy() {
1400
+ this.removeTriggers();
1401
+ super.destroy();
1402
+ }
1403
+ loadShownForms() {
1404
+ try {
1405
+ const stored = localStorage.getItem('clianta_shown_forms');
1406
+ if (stored) {
1407
+ const data = JSON.parse(stored);
1408
+ this.shownForms = new Set(data.forms || []);
1409
+ }
1410
+ }
1411
+ catch (e) {
1412
+ // Ignore storage errors
1413
+ }
1414
+ }
1415
+ saveShownForms() {
1416
+ try {
1417
+ localStorage.setItem('clianta_shown_forms', JSON.stringify({
1418
+ forms: Array.from(this.shownForms),
1419
+ timestamp: Date.now(),
1420
+ }));
1421
+ }
1422
+ catch (e) {
1423
+ // Ignore storage errors
1424
+ }
1425
+ }
1426
+ async fetchForms() {
1427
+ if (!this.tracker)
1428
+ return;
1429
+ const config = this.tracker.getConfig();
1430
+ const workspaceId = this.tracker.getWorkspaceId();
1431
+ const apiEndpoint = config.apiEndpoint || 'https://api.clianta.online';
1432
+ try {
1433
+ const url = encodeURIComponent(window.location.href);
1434
+ const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${workspaceId}?url=${url}`);
1435
+ if (!response.ok)
1436
+ return;
1437
+ const data = await response.json();
1438
+ if (data.success && Array.isArray(data.data)) {
1439
+ this.forms = data.data.filter((form) => this.shouldShowForm(form));
1440
+ }
1441
+ }
1442
+ catch (error) {
1443
+ console.error('[Clianta] Failed to fetch forms:', error);
1444
+ }
1445
+ }
1446
+ shouldShowForm(form) {
1447
+ // Check show frequency
1448
+ if (form.showFrequency === 'once_per_visitor') {
1449
+ if (this.shownForms.has(form._id))
1450
+ return false;
1451
+ }
1452
+ else if (form.showFrequency === 'once_per_session') {
1453
+ const sessionKey = `clianta_form_${form._id}_shown`;
1454
+ if (sessionStorage.getItem(sessionKey))
1455
+ return false;
1456
+ }
1457
+ return true;
1458
+ }
1459
+ setupTriggers() {
1460
+ this.forms.forEach(form => {
1461
+ switch (form.trigger.type) {
1462
+ case 'delay':
1463
+ setTimeout(() => this.showForm(form), (form.trigger.value || 5) * 1000);
1464
+ break;
1465
+ case 'scroll':
1466
+ this.setupScrollTrigger(form);
1467
+ break;
1468
+ case 'exit_intent':
1469
+ this.setupExitIntentTrigger(form);
1470
+ break;
1471
+ case 'click':
1472
+ this.setupClickTrigger(form);
1473
+ break;
1474
+ }
1475
+ });
1476
+ }
1477
+ setupScrollTrigger(form) {
1478
+ const threshold = form.trigger.value || 50;
1479
+ this.scrollHandler = () => {
1480
+ const scrollPercent = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
1481
+ if (scrollPercent >= threshold) {
1482
+ this.showForm(form);
1483
+ if (this.scrollHandler) {
1484
+ window.removeEventListener('scroll', this.scrollHandler);
1485
+ }
1486
+ }
1487
+ };
1488
+ window.addEventListener('scroll', this.scrollHandler, { passive: true });
1489
+ }
1490
+ setupExitIntentTrigger(form) {
1491
+ this.exitHandler = (e) => {
1492
+ if (e.clientY <= 0) {
1493
+ this.showForm(form);
1494
+ if (this.exitHandler) {
1495
+ document.removeEventListener('mouseout', this.exitHandler);
1496
+ }
1497
+ }
1498
+ };
1499
+ document.addEventListener('mouseout', this.exitHandler);
1500
+ }
1501
+ setupClickTrigger(form) {
1502
+ if (!form.trigger.selector)
1503
+ return;
1504
+ const elements = document.querySelectorAll(form.trigger.selector);
1505
+ elements.forEach(el => {
1506
+ el.addEventListener('click', () => this.showForm(form));
1507
+ });
1508
+ }
1509
+ removeTriggers() {
1510
+ if (this.scrollHandler) {
1511
+ window.removeEventListener('scroll', this.scrollHandler);
1512
+ }
1513
+ if (this.exitHandler) {
1514
+ document.removeEventListener('mouseout', this.exitHandler);
1515
+ }
1516
+ }
1517
+ async showForm(form) {
1518
+ // Check if already shown in this session
1519
+ if (!this.shouldShowForm(form))
1520
+ return;
1521
+ // Mark as shown
1522
+ this.shownForms.add(form._id);
1523
+ this.saveShownForms();
1524
+ sessionStorage.setItem(`clianta_form_${form._id}_shown`, 'true');
1525
+ // Track view
1526
+ await this.trackFormView(form._id);
1527
+ // Render form
1528
+ this.renderForm(form);
1529
+ }
1530
+ async trackFormView(formId) {
1531
+ if (!this.tracker)
1532
+ return;
1533
+ const config = this.tracker.getConfig();
1534
+ const apiEndpoint = config.apiEndpoint || 'https://api.clianta.online';
1535
+ try {
1536
+ await fetch(`${apiEndpoint}/api/public/lead-forms/${formId}/view`, {
1537
+ method: 'POST',
1538
+ headers: { 'Content-Type': 'application/json' },
1539
+ });
1540
+ }
1541
+ catch (e) {
1542
+ // Ignore tracking errors
1543
+ }
1544
+ }
1545
+ renderForm(form) {
1546
+ // Create overlay
1547
+ const overlay = document.createElement('div');
1548
+ overlay.id = `clianta-form-overlay-${form._id}`;
1549
+ overlay.style.cssText = `
1550
+ position: fixed;
1551
+ top: 0;
1552
+ left: 0;
1553
+ right: 0;
1554
+ bottom: 0;
1555
+ background: rgba(0, 0, 0, 0.5);
1556
+ z-index: 999998;
1557
+ display: flex;
1558
+ align-items: center;
1559
+ justify-content: center;
1560
+ opacity: 0;
1561
+ transition: opacity 0.3s ease;
1562
+ `;
1563
+ // Create form container
1564
+ const container = document.createElement('div');
1565
+ container.id = `clianta-form-${form._id}`;
1566
+ const style = form.style || {};
1567
+ container.style.cssText = `
1568
+ background: ${style.backgroundColor || '#FFFFFF'};
1569
+ border-radius: ${style.borderRadius || 12}px;
1570
+ padding: 24px;
1571
+ max-width: 400px;
1572
+ width: 90%;
1573
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
1574
+ transform: translateY(20px);
1575
+ opacity: 0;
1576
+ transition: all 0.3s ease;
1577
+ `;
1578
+ // Build form HTML
1579
+ container.innerHTML = this.buildFormHTML(form);
1580
+ overlay.appendChild(container);
1581
+ document.body.appendChild(overlay);
1582
+ // Animate in
1583
+ requestAnimationFrame(() => {
1584
+ overlay.style.opacity = '1';
1585
+ container.style.transform = 'translateY(0)';
1586
+ container.style.opacity = '1';
1587
+ });
1588
+ // Setup event listeners
1589
+ this.setupFormEvents(form, overlay, container);
1590
+ }
1591
+ buildFormHTML(form) {
1592
+ const style = form.style || {};
1593
+ const primaryColor = style.primaryColor || '#10B981';
1594
+ const textColor = style.textColor || '#18181B';
1595
+ let fieldsHTML = form.fields.map(field => {
1596
+ const requiredMark = field.required ? '<span style="color: #EF4444;">*</span>' : '';
1597
+ if (field.type === 'textarea') {
1598
+ return `
1599
+ <div style="margin-bottom: 12px;">
1600
+ <label style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${textColor};">
1601
+ ${field.label} ${requiredMark}
1602
+ </label>
1603
+ <textarea
1604
+ name="${field.name}"
1605
+ placeholder="${field.placeholder || ''}"
1606
+ ${field.required ? 'required' : ''}
1607
+ style="width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; resize: vertical; min-height: 80px;"
1608
+ ></textarea>
1609
+ </div>
1610
+ `;
1611
+ }
1612
+ else if (field.type === 'checkbox') {
1613
+ return `
1614
+ <div style="margin-bottom: 12px;">
1615
+ <label style="display: flex; align-items: center; gap: 8px; font-size: 14px; color: ${textColor}; cursor: pointer;">
1616
+ <input
1617
+ type="checkbox"
1618
+ name="${field.name}"
1619
+ ${field.required ? 'required' : ''}
1620
+ style="width: 16px; height: 16px;"
1621
+ />
1622
+ ${field.label} ${requiredMark}
1623
+ </label>
1624
+ </div>
1625
+ `;
1626
+ }
1627
+ else {
1628
+ return `
1629
+ <div style="margin-bottom: 12px;">
1630
+ <label style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${textColor};">
1631
+ ${field.label} ${requiredMark}
1632
+ </label>
1633
+ <input
1634
+ type="${field.type}"
1635
+ name="${field.name}"
1636
+ placeholder="${field.placeholder || ''}"
1637
+ ${field.required ? 'required' : ''}
1638
+ style="width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box;"
1639
+ />
1640
+ </div>
1641
+ `;
1642
+ }
1643
+ }).join('');
1644
+ return `
1645
+ <button id="clianta-form-close" style="
1646
+ position: absolute;
1647
+ top: 12px;
1648
+ right: 12px;
1649
+ background: none;
1650
+ border: none;
1651
+ font-size: 20px;
1652
+ cursor: pointer;
1653
+ color: #71717A;
1654
+ padding: 4px;
1655
+ ">&times;</button>
1656
+ <h2 style="font-size: 20px; font-weight: 700; margin-bottom: 8px; color: ${textColor};">
1657
+ ${form.headline || 'Stay in touch'}
1658
+ </h2>
1659
+ <p style="font-size: 14px; color: #71717A; margin-bottom: 16px;">
1660
+ ${form.subheadline || 'Get the latest updates'}
1661
+ </p>
1662
+ <form id="clianta-form-element">
1663
+ ${fieldsHTML}
1664
+ <button type="submit" style="
1665
+ width: 100%;
1666
+ padding: 10px 16px;
1667
+ background: ${primaryColor};
1668
+ color: white;
1669
+ border: none;
1670
+ border-radius: 6px;
1671
+ font-size: 14px;
1672
+ font-weight: 500;
1673
+ cursor: pointer;
1674
+ margin-top: 8px;
1675
+ ">
1676
+ ${form.submitButtonText || 'Subscribe'}
1677
+ </button>
1678
+ </form>
1679
+ `;
1680
+ }
1681
+ setupFormEvents(form, overlay, container) {
1682
+ // Close button
1683
+ const closeBtn = container.querySelector('#clianta-form-close');
1684
+ if (closeBtn) {
1685
+ closeBtn.addEventListener('click', () => this.closeForm(form._id, overlay, container));
1686
+ }
1687
+ // Overlay click
1688
+ overlay.addEventListener('click', (e) => {
1689
+ if (e.target === overlay) {
1690
+ this.closeForm(form._id, overlay, container);
1691
+ }
1692
+ });
1693
+ // Form submit
1694
+ const formElement = container.querySelector('#clianta-form-element');
1695
+ if (formElement) {
1696
+ formElement.addEventListener('submit', async (e) => {
1697
+ e.preventDefault();
1698
+ await this.handleSubmit(form, formElement, container);
1699
+ });
1700
+ }
1701
+ }
1702
+ closeForm(formId, overlay, container) {
1703
+ container.style.transform = 'translateY(20px)';
1704
+ container.style.opacity = '0';
1705
+ overlay.style.opacity = '0';
1706
+ setTimeout(() => {
1707
+ overlay.remove();
1708
+ }, 300);
1709
+ }
1710
+ async handleSubmit(form, formElement, container) {
1711
+ if (!this.tracker)
1712
+ return;
1713
+ const config = this.tracker.getConfig();
1714
+ const apiEndpoint = config.apiEndpoint || 'https://api.clianta.online';
1715
+ const visitorId = this.tracker.getVisitorId();
1716
+ // Collect form data
1717
+ const formData = new FormData(formElement);
1718
+ const data = {};
1719
+ formData.forEach((value, key) => {
1720
+ data[key] = value;
1721
+ });
1722
+ // Disable submit button
1723
+ const submitBtn = formElement.querySelector('button[type="submit"]');
1724
+ if (submitBtn) {
1725
+ submitBtn.disabled = true;
1726
+ submitBtn.innerHTML = 'Submitting...';
1727
+ }
1728
+ try {
1729
+ const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${form._id}/submit`, {
1730
+ method: 'POST',
1731
+ headers: { 'Content-Type': 'application/json' },
1732
+ body: JSON.stringify({
1733
+ visitorId,
1734
+ data,
1735
+ url: window.location.href,
1736
+ }),
1737
+ });
1738
+ const result = await response.json();
1739
+ if (result.success) {
1740
+ // Show success message
1741
+ container.innerHTML = `
1742
+ <div style="text-align: center; padding: 20px;">
1743
+ <div style="width: 48px; height: 48px; background: #10B981; border-radius: 50%; margin: 0 auto 16px; display: flex; align-items: center; justify-content: center;">
1744
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
1745
+ <polyline points="20 6 9 17 4 12"></polyline>
1746
+ </svg>
1747
+ </div>
1748
+ <p style="font-size: 16px; font-weight: 500; color: #18181B;">
1749
+ ${form.successMessage || 'Thank you!'}
1750
+ </p>
1751
+ </div>
1752
+ `;
1753
+ // Track identify
1754
+ if (data.email) {
1755
+ this.tracker?.identify(data.email, data);
1756
+ }
1757
+ // Redirect if configured
1758
+ if (form.redirectUrl) {
1759
+ setTimeout(() => {
1760
+ window.location.href = form.redirectUrl;
1761
+ }, 1500);
1762
+ }
1763
+ // Close after delay
1764
+ setTimeout(() => {
1765
+ const overlay = document.getElementById(`clianta-form-overlay-${form._id}`);
1766
+ if (overlay) {
1767
+ this.closeForm(form._id, overlay, container);
1768
+ }
1769
+ }, 2000);
1770
+ }
1771
+ }
1772
+ catch (error) {
1773
+ console.error('[Clianta] Form submit error:', error);
1774
+ if (submitBtn) {
1775
+ submitBtn.disabled = false;
1776
+ submitBtn.innerHTML = form.submitButtonText || 'Subscribe';
1777
+ }
1778
+ }
1779
+ }
1780
+ }
1781
+
1782
+ /**
1783
+ * Clianta SDK - Plugins Index
1784
+ * Version is defined in core/config.ts as SDK_VERSION
1785
+ */
1786
+ /**
1787
+ * Get plugin instance by name
1788
+ */
1789
+ function getPlugin(name) {
1790
+ switch (name) {
1791
+ case 'pageView':
1792
+ return new PageViewPlugin();
1793
+ case 'scroll':
1794
+ return new ScrollPlugin();
1795
+ case 'forms':
1796
+ return new FormsPlugin();
1797
+ case 'clicks':
1798
+ return new ClicksPlugin();
1799
+ case 'engagement':
1800
+ return new EngagementPlugin();
1801
+ case 'downloads':
1802
+ return new DownloadsPlugin();
1803
+ case 'exitIntent':
1804
+ return new ExitIntentPlugin();
1805
+ case 'errors':
1806
+ return new ErrorsPlugin();
1807
+ case 'performance':
1808
+ return new PerformancePlugin();
1809
+ case 'popupForms':
1810
+ return new PopupFormsPlugin();
1811
+ default:
1812
+ throw new Error(`Unknown plugin: ${name}`);
1813
+ }
1814
+ }
1815
+
1816
+ /**
1817
+ * Clianta SDK - Consent Storage
1818
+ * Handles persistence of consent state
1819
+ * @see SDK_VERSION in core/config.ts
1820
+ */
1821
+ const CONSENT_VERSION = 1;
1822
+ /**
1823
+ * Save consent state to storage
1824
+ */
1825
+ function saveConsent(state) {
1826
+ try {
1827
+ if (typeof localStorage === 'undefined')
1828
+ return false;
1829
+ const stored = {
1830
+ state,
1831
+ timestamp: Date.now(),
1832
+ version: CONSENT_VERSION,
1833
+ };
1834
+ localStorage.setItem(STORAGE_KEYS.CONSENT, JSON.stringify(stored));
1835
+ return true;
1836
+ }
1837
+ catch {
1838
+ return false;
1839
+ }
1840
+ }
1841
+ /**
1842
+ * Load consent state from storage
1843
+ */
1844
+ function loadConsent() {
1845
+ try {
1846
+ if (typeof localStorage === 'undefined')
1847
+ return null;
1848
+ const stored = localStorage.getItem(STORAGE_KEYS.CONSENT);
1849
+ if (!stored)
1850
+ return null;
1851
+ const parsed = JSON.parse(stored);
1852
+ // Validate version
1853
+ if (parsed.version !== CONSENT_VERSION) {
1854
+ clearConsent();
1855
+ return null;
1856
+ }
1857
+ return parsed;
1858
+ }
1859
+ catch {
1860
+ return null;
1861
+ }
1862
+ }
1863
+ /**
1864
+ * Clear consent state from storage
1865
+ */
1866
+ function clearConsent() {
1867
+ try {
1868
+ if (typeof localStorage === 'undefined')
1869
+ return false;
1870
+ localStorage.removeItem(STORAGE_KEYS.CONSENT);
1871
+ return true;
1872
+ }
1873
+ catch {
1874
+ return false;
1875
+ }
1876
+ }
1877
+ /**
1878
+ * Check if consent has been explicitly set
1879
+ */
1880
+ function hasStoredConsent() {
1881
+ return loadConsent() !== null;
1882
+ }
1883
+
1884
+ /**
1885
+ * Clianta SDK - Consent Manager
1886
+ * Manages consent state and event buffering for GDPR/CCPA compliance
1887
+ * @see SDK_VERSION in core/config.ts
1888
+ */
1889
+ /**
1890
+ * Manages user consent state for tracking
1891
+ */
1892
+ class ConsentManager {
1893
+ constructor(config = {}) {
1894
+ this.eventBuffer = [];
1895
+ this.callbacks = [];
1896
+ this.hasExplicitConsent = false;
1897
+ this.config = {
1898
+ defaultConsent: { analytics: true, marketing: false, personalization: false },
1899
+ waitForConsent: false,
1900
+ storageKey: 'mb_consent',
1901
+ ...config,
1902
+ };
1903
+ // Load stored consent or use default
1904
+ const stored = loadConsent();
1905
+ if (stored) {
1906
+ this.state = stored.state;
1907
+ this.hasExplicitConsent = true;
1908
+ logger.debug('Loaded stored consent:', this.state);
1909
+ }
1910
+ else {
1911
+ this.state = this.config.defaultConsent || { analytics: true };
1912
+ this.hasExplicitConsent = false;
1913
+ logger.debug('Using default consent:', this.state);
1914
+ }
1915
+ // Register callback if provided
1916
+ if (config.onConsentChange) {
1917
+ this.callbacks.push(config.onConsentChange);
1918
+ }
1919
+ }
1920
+ /**
1921
+ * Grant consent for specified categories
1922
+ */
1923
+ grant(categories) {
1924
+ const previous = { ...this.state };
1925
+ this.state = { ...this.state, ...categories };
1926
+ this.hasExplicitConsent = true;
1927
+ saveConsent(this.state);
1928
+ logger.info('Consent granted:', categories);
1929
+ this.notifyChange(previous);
1930
+ }
1931
+ /**
1932
+ * Revoke consent for specified categories
1933
+ */
1934
+ revoke(categories) {
1935
+ const previous = { ...this.state };
1936
+ for (const category of categories) {
1937
+ this.state[category] = false;
1938
+ }
1939
+ this.hasExplicitConsent = true;
1940
+ saveConsent(this.state);
1941
+ logger.info('Consent revoked:', categories);
1942
+ this.notifyChange(previous);
1943
+ }
1944
+ /**
1945
+ * Update entire consent state
1946
+ */
1947
+ update(state) {
1948
+ const previous = { ...this.state };
1949
+ this.state = { ...state };
1950
+ this.hasExplicitConsent = true;
1951
+ saveConsent(this.state);
1952
+ logger.info('Consent updated:', this.state);
1953
+ this.notifyChange(previous);
1954
+ }
1955
+ /**
1956
+ * Reset consent to default (clear stored consent)
1957
+ */
1958
+ reset() {
1959
+ const previous = { ...this.state };
1960
+ this.state = this.config.defaultConsent || { analytics: true };
1961
+ this.hasExplicitConsent = false;
1962
+ this.eventBuffer = [];
1963
+ clearConsent();
1964
+ logger.info('Consent reset to defaults');
1965
+ this.notifyChange(previous);
1966
+ }
1967
+ /**
1968
+ * Get current consent state
1969
+ */
1970
+ getState() {
1971
+ return { ...this.state };
1972
+ }
1973
+ /**
1974
+ * Check if a specific consent category is granted
1975
+ */
1976
+ hasConsent(category) {
1977
+ return this.state[category] === true;
1978
+ }
1979
+ /**
1980
+ * Check if analytics consent is granted (most common check)
1981
+ */
1982
+ canTrack() {
1983
+ // If waiting for consent and no explicit consent given, cannot track
1984
+ if (this.config.waitForConsent && !this.hasExplicitConsent) {
1985
+ return false;
1986
+ }
1987
+ return this.state.analytics === true;
1988
+ }
1989
+ /**
1990
+ * Check if explicit consent has been given
1991
+ */
1992
+ hasExplicit() {
1993
+ return this.hasExplicitConsent;
1994
+ }
1995
+ /**
1996
+ * Check if there's stored consent
1997
+ */
1998
+ hasStored() {
1999
+ return hasStoredConsent();
2000
+ }
2001
+ /**
2002
+ * Buffer an event (for waitForConsent mode)
2003
+ */
2004
+ bufferEvent(event) {
2005
+ this.eventBuffer.push(event);
2006
+ logger.debug('Event buffered (waiting for consent):', event.eventName);
2007
+ }
2008
+ /**
2009
+ * Get and clear buffered events
2010
+ */
2011
+ flushBuffer() {
2012
+ const events = [...this.eventBuffer];
2013
+ this.eventBuffer = [];
2014
+ if (events.length > 0) {
2015
+ logger.debug(`Flushing ${events.length} buffered events`);
2016
+ }
2017
+ return events;
2018
+ }
2019
+ /**
2020
+ * Get buffered event count
2021
+ */
2022
+ getBufferSize() {
2023
+ return this.eventBuffer.length;
2024
+ }
2025
+ /**
2026
+ * Register a consent change callback
2027
+ */
2028
+ onChange(callback) {
2029
+ this.callbacks.push(callback);
2030
+ // Return unsubscribe function
2031
+ return () => {
2032
+ const index = this.callbacks.indexOf(callback);
2033
+ if (index > -1) {
2034
+ this.callbacks.splice(index, 1);
2035
+ }
2036
+ };
2037
+ }
2038
+ /**
2039
+ * Notify all callbacks of consent change
2040
+ */
2041
+ notifyChange(previous) {
2042
+ for (const callback of this.callbacks) {
2043
+ try {
2044
+ callback(this.state, previous);
2045
+ }
2046
+ catch (error) {
2047
+ logger.error('Consent change callback error:', error);
2048
+ }
2049
+ }
2050
+ }
2051
+ }
2052
+
2053
+ /**
2054
+ * Clianta SDK - Main Tracker Class
2055
+ * @see SDK_VERSION in core/config.ts
2056
+ */
2057
+ /**
2058
+ * Main Clianta Tracker Class
2059
+ */
2060
+ class Tracker {
2061
+ constructor(workspaceId, userConfig = {}) {
2062
+ this.plugins = [];
2063
+ this.isInitialized = false;
2064
+ if (!workspaceId) {
2065
+ throw new Error('[Clianta] Workspace ID is required');
2066
+ }
2067
+ this.workspaceId = workspaceId;
2068
+ this.config = mergeConfig(userConfig);
2069
+ // Setup debug mode
2070
+ logger.enabled = this.config.debug;
2071
+ logger.info(`Initializing SDK v${SDK_VERSION}`, { workspaceId });
2072
+ // Initialize consent manager
2073
+ this.consentManager = new ConsentManager({
2074
+ ...this.config.consent,
2075
+ onConsentChange: (state, previous) => {
2076
+ this.onConsentChange(state, previous);
2077
+ },
2078
+ });
2079
+ // Initialize transport and queue
2080
+ this.transport = new Transport({ apiEndpoint: this.config.apiEndpoint });
2081
+ this.queue = new EventQueue(this.transport, {
2082
+ batchSize: this.config.batchSize,
2083
+ flushInterval: this.config.flushInterval,
2084
+ });
2085
+ // Get or create visitor and session IDs based on mode
2086
+ this.visitorId = this.createVisitorId();
2087
+ this.sessionId = this.createSessionId();
2088
+ logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
2089
+ // Initialize plugins
2090
+ this.initPlugins();
2091
+ this.isInitialized = true;
2092
+ logger.info('SDK initialized successfully');
2093
+ }
2094
+ /**
2095
+ * Create visitor ID based on storage mode
2096
+ */
2097
+ createVisitorId() {
2098
+ // Anonymous mode: use temporary ID until consent
2099
+ if (this.config.consent.anonymousMode && !this.consentManager.hasExplicit()) {
2100
+ const key = STORAGE_KEYS.VISITOR_ID + '_anon';
2101
+ let anonId = getSessionStorage(key);
2102
+ if (!anonId) {
2103
+ anonId = 'anon_' + generateUUID();
2104
+ setSessionStorage(key, anonId);
2105
+ }
2106
+ return anonId;
2107
+ }
2108
+ // Cookie-less mode: use sessionStorage only
2109
+ if (this.config.cookielessMode) {
2110
+ let visitorId = getSessionStorage(STORAGE_KEYS.VISITOR_ID);
2111
+ if (!visitorId) {
2112
+ visitorId = generateUUID();
2113
+ setSessionStorage(STORAGE_KEYS.VISITOR_ID, visitorId);
2114
+ }
2115
+ return visitorId;
2116
+ }
2117
+ // Normal mode
2118
+ return getOrCreateVisitorId(this.config.useCookies);
2119
+ }
2120
+ /**
2121
+ * Create session ID
2122
+ */
2123
+ createSessionId() {
2124
+ return getOrCreateSessionId(this.config.sessionTimeout);
2125
+ }
2126
+ /**
2127
+ * Handle consent state changes
2128
+ */
2129
+ onConsentChange(state, previous) {
2130
+ logger.debug('Consent changed:', { from: previous, to: state });
2131
+ // If analytics consent was just granted
2132
+ if (state.analytics && !previous.analytics) {
2133
+ // Upgrade from anonymous ID to persistent ID
2134
+ if (this.config.consent.anonymousMode) {
2135
+ this.visitorId = getOrCreateVisitorId(this.config.useCookies);
2136
+ logger.info('Upgraded from anonymous to persistent visitor ID');
2137
+ }
2138
+ // Flush buffered events
2139
+ const buffered = this.consentManager.flushBuffer();
2140
+ for (const event of buffered) {
2141
+ // Update event with new visitor ID
2142
+ event.visitorId = this.visitorId;
2143
+ this.queue.push(event);
2144
+ }
2145
+ }
2146
+ }
2147
+ /**
2148
+ * Initialize enabled plugins
2149
+ */
2150
+ initPlugins() {
2151
+ const pluginsToLoad = this.config.plugins;
2152
+ // Skip pageView plugin if autoPageView is disabled
2153
+ const filteredPlugins = this.config.autoPageView
2154
+ ? pluginsToLoad
2155
+ : pluginsToLoad.filter((p) => p !== 'pageView');
2156
+ for (const pluginName of filteredPlugins) {
2157
+ try {
2158
+ const plugin = getPlugin(pluginName);
2159
+ plugin.init(this);
2160
+ this.plugins.push(plugin);
2161
+ logger.debug(`Plugin loaded: ${pluginName}`);
2162
+ }
2163
+ catch (error) {
2164
+ logger.error(`Failed to load plugin: ${pluginName}`, error);
2165
+ }
2166
+ }
2167
+ }
2168
+ /**
2169
+ * Track a custom event
2170
+ */
2171
+ track(eventType, eventName, properties = {}) {
2172
+ if (!this.isInitialized) {
2173
+ logger.warn('SDK not initialized, event dropped');
2174
+ return;
2175
+ }
2176
+ const event = {
2177
+ workspaceId: this.workspaceId,
2178
+ visitorId: this.visitorId,
2179
+ sessionId: this.sessionId,
2180
+ eventType: eventType,
2181
+ eventName,
2182
+ url: typeof window !== 'undefined' ? window.location.href : '',
2183
+ referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
2184
+ properties,
2185
+ device: getDeviceInfo(),
2186
+ utm: getUTMParams(),
2187
+ timestamp: new Date().toISOString(),
2188
+ sdkVersion: SDK_VERSION,
2189
+ };
2190
+ // Check consent before tracking
2191
+ if (!this.consentManager.canTrack()) {
2192
+ // Buffer event for later if waitForConsent is enabled
2193
+ if (this.config.consent.waitForConsent) {
2194
+ this.consentManager.bufferEvent(event);
2195
+ return;
2196
+ }
2197
+ // Otherwise drop the event
2198
+ logger.debug('Event dropped (no consent):', eventName);
2199
+ return;
2200
+ }
2201
+ this.queue.push(event);
2202
+ logger.debug('Event tracked:', eventName, properties);
2203
+ }
2204
+ /**
2205
+ * Track a page view
2206
+ */
2207
+ page(name, properties = {}) {
2208
+ const pageName = name || (typeof document !== 'undefined' ? document.title : 'Page View');
2209
+ this.track('page_view', pageName, {
2210
+ ...properties,
2211
+ path: typeof window !== 'undefined' ? window.location.pathname : '',
2212
+ });
2213
+ }
2214
+ /**
2215
+ * Identify a visitor
2216
+ */
2217
+ async identify(email, traits = {}) {
2218
+ if (!email) {
2219
+ logger.warn('Email is required for identification');
2220
+ return;
2221
+ }
2222
+ logger.info('Identifying visitor:', email);
2223
+ const result = await this.transport.sendIdentify({
2224
+ workspaceId: this.workspaceId,
2225
+ visitorId: this.visitorId,
2226
+ email,
2227
+ properties: traits,
2228
+ });
2229
+ if (result.success) {
2230
+ logger.info('Visitor identified successfully');
2231
+ }
2232
+ else {
2233
+ logger.error('Failed to identify visitor:', result.error);
2234
+ }
2235
+ }
2236
+ /**
2237
+ * Update consent state
2238
+ */
2239
+ consent(state) {
2240
+ this.consentManager.update(state);
2241
+ }
2242
+ /**
2243
+ * Get current consent state
2244
+ */
2245
+ getConsentState() {
2246
+ return this.consentManager.getState();
2247
+ }
2248
+ /**
2249
+ * Toggle debug mode
2250
+ */
2251
+ debug(enabled) {
2252
+ logger.enabled = enabled;
2253
+ logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
2254
+ }
2255
+ /**
2256
+ * Get visitor ID
2257
+ */
2258
+ getVisitorId() {
2259
+ return this.visitorId;
2260
+ }
2261
+ /**
2262
+ * Get session ID
2263
+ */
2264
+ getSessionId() {
2265
+ return this.sessionId;
2266
+ }
2267
+ /**
2268
+ * Get workspace ID
2269
+ */
2270
+ getWorkspaceId() {
2271
+ return this.workspaceId;
2272
+ }
2273
+ /**
2274
+ * Get current configuration
2275
+ */
2276
+ getConfig() {
2277
+ return { ...this.config };
2278
+ }
2279
+ /**
2280
+ * Force flush event queue
2281
+ */
2282
+ async flush() {
2283
+ await this.queue.flush();
2284
+ }
2285
+ /**
2286
+ * Reset visitor and session (for logout)
2287
+ */
2288
+ reset() {
2289
+ logger.info('Resetting visitor data');
2290
+ resetIds(this.config.useCookies);
2291
+ this.visitorId = this.createVisitorId();
2292
+ this.sessionId = this.createSessionId();
2293
+ this.queue.clear();
2294
+ }
2295
+ /**
2296
+ * Delete all stored user data (GDPR right-to-erasure)
2297
+ */
2298
+ deleteData() {
2299
+ logger.info('Deleting all user data (GDPR request)');
2300
+ // Clear queue
2301
+ this.queue.clear();
2302
+ // Reset consent
2303
+ this.consentManager.reset();
2304
+ // Clear all stored IDs
2305
+ resetIds(this.config.useCookies);
2306
+ // Clear session storage items
2307
+ if (typeof sessionStorage !== 'undefined') {
2308
+ try {
2309
+ sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
2310
+ sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID + '_anon');
2311
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_ID);
2312
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_TIMESTAMP);
2313
+ }
2314
+ catch {
2315
+ // Ignore errors
2316
+ }
2317
+ }
2318
+ // Clear localStorage items
2319
+ if (typeof localStorage !== 'undefined') {
2320
+ try {
2321
+ localStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
2322
+ localStorage.removeItem(STORAGE_KEYS.CONSENT);
2323
+ localStorage.removeItem(STORAGE_KEYS.EVENT_QUEUE);
2324
+ }
2325
+ catch {
2326
+ // Ignore errors
2327
+ }
2328
+ }
2329
+ // Generate new IDs
2330
+ this.visitorId = this.createVisitorId();
2331
+ this.sessionId = this.createSessionId();
2332
+ logger.info('All user data deleted');
2333
+ }
2334
+ /**
2335
+ * Destroy tracker and cleanup
2336
+ */
2337
+ destroy() {
2338
+ logger.info('Destroying tracker');
2339
+ // Flush any remaining events
2340
+ this.queue.flush();
2341
+ // Destroy plugins
2342
+ for (const plugin of this.plugins) {
2343
+ if (plugin.destroy) {
2344
+ plugin.destroy();
2345
+ }
2346
+ }
2347
+ this.plugins = [];
2348
+ // Destroy queue
2349
+ this.queue.destroy();
2350
+ this.isInitialized = false;
2351
+ }
2352
+ }
2353
+
2354
+ /**
2355
+ * Clianta SDK - CRM API Client
2356
+ * @see SDK_VERSION in core/config.ts
2357
+ */
2358
+ /**
2359
+ * CRM API Client for managing contacts and opportunities
2360
+ */
2361
+ class CRMClient {
2362
+ constructor(apiEndpoint, workspaceId, authToken) {
2363
+ this.apiEndpoint = apiEndpoint;
2364
+ this.workspaceId = workspaceId;
2365
+ this.authToken = authToken;
2366
+ }
2367
+ /**
2368
+ * Set authentication token for API requests
2369
+ */
2370
+ setAuthToken(token) {
2371
+ this.authToken = token;
2372
+ }
2373
+ /**
2374
+ * Make authenticated API request
2375
+ */
2376
+ async request(endpoint, options = {}) {
2377
+ const url = `${this.apiEndpoint}${endpoint}`;
2378
+ const headers = {
2379
+ 'Content-Type': 'application/json',
2380
+ ...(options.headers || {}),
2381
+ };
2382
+ if (this.authToken) {
2383
+ headers['Authorization'] = `Bearer ${this.authToken}`;
2384
+ }
2385
+ try {
2386
+ const response = await fetch(url, {
2387
+ ...options,
2388
+ headers,
2389
+ });
2390
+ const data = await response.json();
2391
+ if (!response.ok) {
2392
+ return {
2393
+ success: false,
2394
+ error: data.message || 'Request failed',
2395
+ status: response.status,
2396
+ };
2397
+ }
2398
+ return {
2399
+ success: true,
2400
+ data: data.data || data,
2401
+ status: response.status,
2402
+ };
2403
+ }
2404
+ catch (error) {
2405
+ return {
2406
+ success: false,
2407
+ error: error instanceof Error ? error.message : 'Network error',
2408
+ status: 0,
2409
+ };
2410
+ }
2411
+ }
2412
+ // ============================================
2413
+ // CONTACTS API
2414
+ // ============================================
2415
+ /**
2416
+ * Get all contacts with pagination
2417
+ */
2418
+ async getContacts(params) {
2419
+ const queryParams = new URLSearchParams();
2420
+ if (params?.page)
2421
+ queryParams.set('page', params.page.toString());
2422
+ if (params?.limit)
2423
+ queryParams.set('limit', params.limit.toString());
2424
+ if (params?.search)
2425
+ queryParams.set('search', params.search);
2426
+ if (params?.status)
2427
+ queryParams.set('status', params.status);
2428
+ const query = queryParams.toString();
2429
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts${query ? `?${query}` : ''}`;
2430
+ return this.request(endpoint);
2431
+ }
2432
+ /**
2433
+ * Get a single contact by ID
2434
+ */
2435
+ async getContact(contactId) {
2436
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
2437
+ }
2438
+ /**
2439
+ * Create a new contact
2440
+ */
2441
+ async createContact(contact) {
2442
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts`, {
2443
+ method: 'POST',
2444
+ body: JSON.stringify(contact),
2445
+ });
2446
+ }
2447
+ /**
2448
+ * Update an existing contact
2449
+ */
2450
+ async updateContact(contactId, updates) {
2451
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2452
+ method: 'PUT',
2453
+ body: JSON.stringify(updates),
2454
+ });
2455
+ }
2456
+ /**
2457
+ * Delete a contact
2458
+ */
2459
+ async deleteContact(contactId) {
2460
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2461
+ method: 'DELETE',
2462
+ });
2463
+ }
2464
+ // ============================================
2465
+ // OPPORTUNITIES API
2466
+ // ============================================
2467
+ /**
2468
+ * Get all opportunities with pagination
2469
+ */
2470
+ async getOpportunities(params) {
2471
+ const queryParams = new URLSearchParams();
2472
+ if (params?.page)
2473
+ queryParams.set('page', params.page.toString());
2474
+ if (params?.limit)
2475
+ queryParams.set('limit', params.limit.toString());
2476
+ if (params?.pipelineId)
2477
+ queryParams.set('pipelineId', params.pipelineId);
2478
+ if (params?.stageId)
2479
+ queryParams.set('stageId', params.stageId);
2480
+ const query = queryParams.toString();
2481
+ const endpoint = `/api/workspaces/${this.workspaceId}/opportunities${query ? `?${query}` : ''}`;
2482
+ return this.request(endpoint);
2483
+ }
2484
+ /**
2485
+ * Get a single opportunity by ID
2486
+ */
2487
+ async getOpportunity(opportunityId) {
2488
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`);
2489
+ }
2490
+ /**
2491
+ * Create a new opportunity
2492
+ */
2493
+ async createOpportunity(opportunity) {
2494
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities`, {
2495
+ method: 'POST',
2496
+ body: JSON.stringify(opportunity),
2497
+ });
2498
+ }
2499
+ /**
2500
+ * Update an existing opportunity
2501
+ */
2502
+ async updateOpportunity(opportunityId, updates) {
2503
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
2504
+ method: 'PUT',
2505
+ body: JSON.stringify(updates),
2506
+ });
2507
+ }
2508
+ /**
2509
+ * Delete an opportunity
2510
+ */
2511
+ async deleteOpportunity(opportunityId) {
2512
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
2513
+ method: 'DELETE',
2514
+ });
2515
+ }
2516
+ /**
2517
+ * Move opportunity to a different stage
2518
+ */
2519
+ async moveOpportunity(opportunityId, stageId) {
2520
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/move`, {
2521
+ method: 'POST',
2522
+ body: JSON.stringify({ stageId }),
2523
+ });
2524
+ }
2525
+ }
2526
+
2527
+ /**
2528
+ * Clianta SDK
2529
+ * Professional CRM and tracking SDK for lead generation
2530
+ * @see SDK_VERSION in core/config.ts
2531
+ */
2532
+ // Global instance cache
2533
+ let globalInstance = null;
2534
+ /**
2535
+ * Initialize or get the Clianta tracker instance
2536
+ *
2537
+ * @example
2538
+ * // Simple initialization
2539
+ * const tracker = clianta('your-workspace-id');
2540
+ *
2541
+ * @example
2542
+ * // With configuration
2543
+ * const tracker = clianta('your-workspace-id', {
2544
+ * debug: true,
2545
+ * plugins: ['pageView', 'forms', 'scroll'],
2546
+ * });
2547
+ *
2548
+ * @example
2549
+ * // With consent configuration
2550
+ * const tracker = clianta('your-workspace-id', {
2551
+ * consent: {
2552
+ * waitForConsent: true,
2553
+ * anonymousMode: true,
2554
+ * },
2555
+ * cookielessMode: true, // GDPR-friendly mode
2556
+ * });
2557
+ */
2558
+ function clianta(workspaceId, config) {
2559
+ // Return existing instance if same workspace
2560
+ if (globalInstance && globalInstance.getWorkspaceId() === workspaceId) {
2561
+ return globalInstance;
2562
+ }
2563
+ // Destroy existing instance if workspace changed
2564
+ if (globalInstance) {
2565
+ globalInstance.destroy();
2566
+ }
2567
+ // Create new instance
2568
+ globalInstance = new Tracker(workspaceId, config);
2569
+ return globalInstance;
2570
+ }
2571
+ // Attach to window for <script> usage
2572
+ if (typeof window !== 'undefined') {
2573
+ window.clianta = clianta;
2574
+ window.Clianta = {
2575
+ clianta,
2576
+ Tracker,
2577
+ CRMClient,
2578
+ ConsentManager,
2579
+ };
2580
+ }
2581
+
2582
+ /**
2583
+ * Clianta SDK - React Integration
2584
+ *
2585
+ * Provides CliantaProvider component for easy React/Next.js integration
2586
+ * using the clianta.config.ts pattern.
2587
+ */
2588
+ // Context for accessing tracker throughout the app
2589
+ const CliantaContext = createContext(null);
2590
+ /**
2591
+ * CliantaProvider - Wrap your app to enable tracking
2592
+ *
2593
+ * @example
2594
+ * // In clianta.config.ts:
2595
+ * import { CliantaConfig } from '@clianta/sdk';
2596
+ *
2597
+ * const config: CliantaConfig = {
2598
+ * projectId: 'your-project-id',
2599
+ * apiEndpoint: 'https://api.clianta.online',
2600
+ * debug: process.env.NODE_ENV === 'development',
2601
+ * };
2602
+ *
2603
+ * export default config;
2604
+ *
2605
+ * // In app/layout.tsx or main.tsx:
2606
+ * import { CliantaProvider } from '@clianta/sdk/react';
2607
+ * import cliantaConfig from '../clianta.config';
2608
+ *
2609
+ * <CliantaProvider config={cliantaConfig}>
2610
+ * {children}
2611
+ * </CliantaProvider>
2612
+ */
2613
+ function CliantaProvider({ config, children }) {
2614
+ const trackerRef = useRef(null);
2615
+ useEffect(() => {
2616
+ // Initialize tracker with config
2617
+ const projectId = config.projectId;
2618
+ if (!projectId) {
2619
+ console.error('[Clianta] Missing projectId in config. Please add projectId to your clianta.config.ts');
2620
+ return;
2621
+ }
2622
+ // Extract projectId (handled separately) and pass rest as options
2623
+ const { projectId: _, ...options } = config;
2624
+ trackerRef.current = clianta(projectId, options);
2625
+ // Cleanup: flush pending events on unmount
2626
+ return () => {
2627
+ trackerRef.current?.flush();
2628
+ };
2629
+ }, [config]);
2630
+ return (jsx(CliantaContext.Provider, { value: trackerRef.current, children: children }));
2631
+ }
2632
+ /**
2633
+ * useClianta - Hook to access tracker in any component
2634
+ *
2635
+ * @example
2636
+ * const tracker = useClianta();
2637
+ * tracker?.track('button_click', 'CTA Button');
2638
+ */
2639
+ function useClianta() {
2640
+ return useContext(CliantaContext);
2641
+ }
2642
+ /**
2643
+ * useClinataTrack - Convenience hook for tracking events
2644
+ *
2645
+ * @example
2646
+ * const track = useCliantaTrack();
2647
+ * track('purchase', 'Order Completed', { orderId: '123' });
2648
+ */
2649
+ function useCliantaTrack() {
2650
+ const tracker = useClianta();
2651
+ return (eventType, eventName, properties) => {
2652
+ tracker?.track(eventType, eventName, properties);
2653
+ };
2654
+ }
2655
+
2656
+ export { CliantaProvider, useClianta, useCliantaTrack };
2657
+ //# sourceMappingURL=react.esm.js.map