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