@clianta/sdk 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3178 @@
1
+ /*!
2
+ * Clianta SDK v1.2.0
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.2.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
+ /** Rate limit: max events per window */
579
+ const RATE_LIMIT_MAX_EVENTS = 100;
580
+ /** Rate limit window in ms (1 minute) */
581
+ const RATE_LIMIT_WINDOW_MS = 60000;
582
+ /**
583
+ * Event queue with batching, persistence, rate limiting, and auto-flush
584
+ */
585
+ class EventQueue {
586
+ constructor(transport, config = {}) {
587
+ this.queue = [];
588
+ this.flushTimer = null;
589
+ this.isFlushing = false;
590
+ /** Rate limiting: timestamps of recent events */
591
+ this.eventTimestamps = [];
592
+ this.transport = transport;
593
+ this.config = {
594
+ batchSize: config.batchSize ?? 10,
595
+ flushInterval: config.flushInterval ?? 5000,
596
+ maxQueueSize: config.maxQueueSize ?? MAX_QUEUE_SIZE,
597
+ storageKey: config.storageKey ?? STORAGE_KEYS.EVENT_QUEUE,
598
+ };
599
+ // Restore persisted queue
600
+ this.restoreQueue();
601
+ // Start auto-flush timer
602
+ this.startFlushTimer();
603
+ // Setup unload handlers
604
+ this.setupUnloadHandlers();
605
+ }
606
+ /**
607
+ * Add an event to the queue
608
+ */
609
+ push(event) {
610
+ // Rate limiting check
611
+ if (!this.checkRateLimit()) {
612
+ logger.warn('Rate limit exceeded, event dropped:', event.eventName);
613
+ return;
614
+ }
615
+ // Don't exceed max queue size
616
+ if (this.queue.length >= this.config.maxQueueSize) {
617
+ logger.warn('Queue full, dropping oldest event');
618
+ this.queue.shift();
619
+ }
620
+ this.queue.push(event);
621
+ logger.debug('Event queued:', event.eventName, `(${this.queue.length} in queue)`);
622
+ // Flush if batch size reached
623
+ if (this.queue.length >= this.config.batchSize) {
624
+ this.flush();
625
+ }
626
+ }
627
+ /**
628
+ * Check and enforce rate limiting
629
+ * @returns true if event is allowed, false if rate limited
630
+ */
631
+ checkRateLimit() {
632
+ const now = Date.now();
633
+ // Remove timestamps outside the window
634
+ this.eventTimestamps = this.eventTimestamps.filter(ts => now - ts < RATE_LIMIT_WINDOW_MS);
635
+ // Check if under limit
636
+ if (this.eventTimestamps.length >= RATE_LIMIT_MAX_EVENTS) {
637
+ return false;
638
+ }
639
+ // Record this event
640
+ this.eventTimestamps.push(now);
641
+ return true;
642
+ }
643
+ /**
644
+ * Flush the queue (send all events)
645
+ */
646
+ async flush() {
647
+ if (this.isFlushing || this.queue.length === 0) {
648
+ return;
649
+ }
650
+ this.isFlushing = true;
651
+ // Atomically take snapshot of current queue length to avoid race condition
652
+ const count = this.queue.length;
653
+ const events = this.queue.splice(0, count);
654
+ try {
655
+ logger.debug(`Flushing ${events.length} events`);
656
+ // Clear persisted queue
657
+ this.persistQueue([]);
658
+ // Send to backend
659
+ const result = await this.transport.sendEvents(events);
660
+ if (!result.success) {
661
+ // Re-queue events on failure (at the front)
662
+ logger.warn('Flush failed, re-queuing events');
663
+ this.queue.unshift(...events);
664
+ this.persistQueue(this.queue);
665
+ }
666
+ else {
667
+ logger.debug('Flush successful');
668
+ }
669
+ }
670
+ catch (error) {
671
+ logger.error('Flush error:', error);
672
+ }
673
+ finally {
674
+ this.isFlushing = false;
675
+ }
676
+ }
677
+ /**
678
+ * Flush synchronously using sendBeacon (for page unload)
679
+ */
680
+ flushSync() {
681
+ if (this.queue.length === 0)
682
+ return;
683
+ const events = this.queue.splice(0, this.queue.length);
684
+ logger.debug(`Sync flushing ${events.length} events via beacon`);
685
+ const success = this.transport.sendBeacon(events);
686
+ if (!success) {
687
+ // Re-queue and persist for next page load
688
+ this.queue.unshift(...events);
689
+ this.persistQueue(this.queue);
690
+ }
691
+ }
692
+ /**
693
+ * Get current queue length
694
+ */
695
+ get length() {
696
+ return this.queue.length;
697
+ }
698
+ /**
699
+ * Clear the queue
700
+ */
701
+ clear() {
702
+ this.queue = [];
703
+ this.persistQueue([]);
704
+ }
705
+ /**
706
+ * Stop the flush timer
707
+ */
708
+ destroy() {
709
+ if (this.flushTimer) {
710
+ clearInterval(this.flushTimer);
711
+ this.flushTimer = null;
712
+ }
713
+ }
714
+ /**
715
+ * Start auto-flush timer
716
+ */
717
+ startFlushTimer() {
718
+ if (this.flushTimer) {
719
+ clearInterval(this.flushTimer);
720
+ }
721
+ this.flushTimer = setInterval(() => {
722
+ this.flush();
723
+ }, this.config.flushInterval);
724
+ }
725
+ /**
726
+ * Setup page unload handlers
727
+ */
728
+ setupUnloadHandlers() {
729
+ if (typeof window === 'undefined')
730
+ return;
731
+ // Flush on page unload
732
+ window.addEventListener('beforeunload', () => {
733
+ this.flushSync();
734
+ });
735
+ // Flush when page becomes hidden
736
+ window.addEventListener('visibilitychange', () => {
737
+ if (document.visibilityState === 'hidden') {
738
+ this.flushSync();
739
+ }
740
+ });
741
+ // Flush on page hide (iOS Safari)
742
+ window.addEventListener('pagehide', () => {
743
+ this.flushSync();
744
+ });
745
+ }
746
+ /**
747
+ * Persist queue to localStorage
748
+ */
749
+ persistQueue(events) {
750
+ try {
751
+ setLocalStorage(this.config.storageKey, JSON.stringify(events));
752
+ }
753
+ catch {
754
+ // Ignore storage errors
755
+ }
756
+ }
757
+ /**
758
+ * Restore queue from localStorage
759
+ */
760
+ restoreQueue() {
761
+ try {
762
+ const stored = getLocalStorage(this.config.storageKey);
763
+ if (stored) {
764
+ const events = JSON.parse(stored);
765
+ if (Array.isArray(events) && events.length > 0) {
766
+ this.queue = events;
767
+ logger.debug(`Restored ${events.length} events from storage`);
768
+ }
769
+ }
770
+ }
771
+ catch {
772
+ // Ignore parse errors
773
+ }
774
+ }
775
+ }
776
+
777
+ /**
778
+ * Clianta SDK - Plugin Base
779
+ * @see SDK_VERSION in core/config.ts
780
+ */
781
+ /**
782
+ * Base class for plugins
783
+ */
784
+ class BasePlugin {
785
+ constructor() {
786
+ this.tracker = null;
787
+ }
788
+ init(tracker) {
789
+ this.tracker = tracker;
790
+ }
791
+ destroy() {
792
+ this.tracker = null;
793
+ }
794
+ track(eventType, eventName, properties) {
795
+ if (this.tracker) {
796
+ this.tracker.track(eventType, eventName, properties);
797
+ }
798
+ }
799
+ }
800
+
801
+ /**
802
+ * Clianta SDK - Page View Plugin
803
+ * @see SDK_VERSION in core/config.ts
804
+ */
805
+ /**
806
+ * Page View Plugin - Tracks page views
807
+ */
808
+ class PageViewPlugin extends BasePlugin {
809
+ constructor() {
810
+ super(...arguments);
811
+ this.name = 'pageView';
812
+ this.originalPushState = null;
813
+ this.originalReplaceState = null;
814
+ this.popstateHandler = null;
815
+ }
816
+ init(tracker) {
817
+ super.init(tracker);
818
+ // Track initial page view
819
+ this.trackPageView();
820
+ // Track SPA navigation (History API)
821
+ if (typeof window !== 'undefined') {
822
+ // Store originals for cleanup
823
+ this.originalPushState = history.pushState;
824
+ this.originalReplaceState = history.replaceState;
825
+ // Intercept pushState and replaceState
826
+ const self = this;
827
+ history.pushState = function (...args) {
828
+ self.originalPushState.apply(history, args);
829
+ self.trackPageView();
830
+ };
831
+ history.replaceState = function (...args) {
832
+ self.originalReplaceState.apply(history, args);
833
+ self.trackPageView();
834
+ };
835
+ // Handle back/forward navigation
836
+ this.popstateHandler = () => this.trackPageView();
837
+ window.addEventListener('popstate', this.popstateHandler);
838
+ }
839
+ }
840
+ destroy() {
841
+ // Restore original history methods
842
+ if (this.originalPushState) {
843
+ history.pushState = this.originalPushState;
844
+ this.originalPushState = null;
845
+ }
846
+ if (this.originalReplaceState) {
847
+ history.replaceState = this.originalReplaceState;
848
+ this.originalReplaceState = null;
849
+ }
850
+ // Remove popstate listener
851
+ if (this.popstateHandler && typeof window !== 'undefined') {
852
+ window.removeEventListener('popstate', this.popstateHandler);
853
+ this.popstateHandler = null;
854
+ }
855
+ super.destroy();
856
+ }
857
+ trackPageView() {
858
+ if (typeof window === 'undefined' || typeof document === 'undefined')
859
+ return;
860
+ this.track('page_view', 'Page Viewed', {
861
+ title: document.title,
862
+ path: window.location.pathname,
863
+ search: window.location.search,
864
+ hash: window.location.hash,
865
+ referrer: document.referrer || 'direct',
866
+ viewport: `${window.innerWidth}x${window.innerHeight}`,
867
+ });
868
+ }
869
+ }
870
+
871
+ /**
872
+ * Clianta SDK - Scroll Depth Plugin
873
+ * @see SDK_VERSION in core/config.ts
874
+ */
875
+ /**
876
+ * Scroll Depth Plugin - Tracks scroll milestones
877
+ */
878
+ class ScrollPlugin extends BasePlugin {
879
+ constructor() {
880
+ super(...arguments);
881
+ this.name = 'scroll';
882
+ this.milestonesReached = new Set();
883
+ this.maxScrollDepth = 0;
884
+ this.pageLoadTime = 0;
885
+ this.scrollTimeout = null;
886
+ this.boundHandler = null;
887
+ }
888
+ init(tracker) {
889
+ super.init(tracker);
890
+ this.pageLoadTime = Date.now();
891
+ if (typeof window !== 'undefined') {
892
+ this.boundHandler = this.handleScroll.bind(this);
893
+ window.addEventListener('scroll', this.boundHandler, { passive: true });
894
+ }
895
+ }
896
+ destroy() {
897
+ if (this.boundHandler && typeof window !== 'undefined') {
898
+ window.removeEventListener('scroll', this.boundHandler);
899
+ }
900
+ if (this.scrollTimeout) {
901
+ clearTimeout(this.scrollTimeout);
902
+ }
903
+ super.destroy();
904
+ }
905
+ handleScroll() {
906
+ // Debounce scroll tracking
907
+ if (this.scrollTimeout) {
908
+ clearTimeout(this.scrollTimeout);
909
+ }
910
+ this.scrollTimeout = setTimeout(() => this.trackScrollDepth(), 150);
911
+ }
912
+ trackScrollDepth() {
913
+ if (typeof window === 'undefined' || typeof document === 'undefined')
914
+ return;
915
+ const windowHeight = window.innerHeight;
916
+ const documentHeight = document.documentElement.scrollHeight;
917
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
918
+ const scrollableHeight = documentHeight - windowHeight;
919
+ // Guard against divide-by-zero on short pages
920
+ if (scrollableHeight <= 0)
921
+ return;
922
+ const scrollPercent = Math.floor((scrollTop / scrollableHeight) * 100);
923
+ // Clamp to valid range
924
+ const clampedPercent = Math.max(0, Math.min(100, scrollPercent));
925
+ // Update max scroll depth
926
+ if (clampedPercent > this.maxScrollDepth) {
927
+ this.maxScrollDepth = clampedPercent;
928
+ }
929
+ // Track milestones
930
+ for (const milestone of SCROLL_MILESTONES) {
931
+ if (clampedPercent >= milestone && !this.milestonesReached.has(milestone)) {
932
+ this.milestonesReached.add(milestone);
933
+ this.track('scroll_depth', `Scrolled ${milestone}%`, {
934
+ depth: milestone,
935
+ maxDepth: this.maxScrollDepth,
936
+ timeToReach: Date.now() - this.pageLoadTime,
937
+ });
938
+ }
939
+ }
940
+ }
941
+ }
942
+
943
+ /**
944
+ * Clianta SDK - Form Tracking Plugin
945
+ * @see SDK_VERSION in core/config.ts
946
+ */
947
+ /**
948
+ * Form Tracking Plugin - Auto-tracks form views, interactions, and submissions
949
+ */
950
+ class FormsPlugin extends BasePlugin {
951
+ constructor() {
952
+ super(...arguments);
953
+ this.name = 'forms';
954
+ this.trackedForms = new WeakSet();
955
+ this.formInteractions = new Set();
956
+ this.observer = null;
957
+ }
958
+ init(tracker) {
959
+ super.init(tracker);
960
+ if (typeof document === 'undefined')
961
+ return;
962
+ // Track existing forms
963
+ this.trackAllForms();
964
+ // Watch for dynamically added forms
965
+ if (typeof MutationObserver !== 'undefined') {
966
+ this.observer = new MutationObserver(() => this.trackAllForms());
967
+ this.observer.observe(document.body, { childList: true, subtree: true });
968
+ }
969
+ }
970
+ destroy() {
971
+ if (this.observer) {
972
+ this.observer.disconnect();
973
+ this.observer = null;
974
+ }
975
+ super.destroy();
976
+ }
977
+ trackAllForms() {
978
+ document.querySelectorAll('form').forEach((form) => {
979
+ this.setupFormTracking(form);
980
+ });
981
+ }
982
+ setupFormTracking(form) {
983
+ if (this.trackedForms.has(form))
984
+ return;
985
+ this.trackedForms.add(form);
986
+ const formId = form.id || form.name || `form-${Math.random().toString(36).substr(2, 9)}`;
987
+ // Track form view
988
+ this.track('form_view', 'Form Viewed', {
989
+ formId,
990
+ action: form.action,
991
+ method: form.method,
992
+ fieldCount: form.elements.length,
993
+ });
994
+ // Track field interactions
995
+ Array.from(form.elements).forEach((field) => {
996
+ if (field instanceof HTMLInputElement ||
997
+ field instanceof HTMLSelectElement ||
998
+ field instanceof HTMLTextAreaElement) {
999
+ if (!field.name || field.type === 'submit' || field.type === 'button')
1000
+ return;
1001
+ ['focus', 'blur', 'change'].forEach((eventType) => {
1002
+ field.addEventListener(eventType, () => {
1003
+ const key = `${formId}-${field.name}-${eventType}`;
1004
+ if (!this.formInteractions.has(key)) {
1005
+ this.formInteractions.add(key);
1006
+ this.track('form_interaction', 'Form Field Interaction', {
1007
+ formId,
1008
+ fieldName: field.name,
1009
+ fieldType: field.type,
1010
+ interactionType: eventType,
1011
+ });
1012
+ }
1013
+ });
1014
+ });
1015
+ }
1016
+ });
1017
+ // Track form submission
1018
+ form.addEventListener('submit', () => {
1019
+ this.track('form_submit', 'Form Submitted', {
1020
+ formId,
1021
+ action: form.action,
1022
+ method: form.method,
1023
+ });
1024
+ // Auto-identify if email field found
1025
+ this.autoIdentify(form);
1026
+ });
1027
+ }
1028
+ autoIdentify(form) {
1029
+ const emailField = form.querySelector('input[type="email"], input[name*="email"]');
1030
+ if (!emailField?.value || !this.tracker)
1031
+ return;
1032
+ const email = emailField.value;
1033
+ const traits = {};
1034
+ // Capture common fields
1035
+ const firstNameField = form.querySelector('[name*="first"], [name*="fname"]');
1036
+ const lastNameField = form.querySelector('[name*="last"], [name*="lname"]');
1037
+ const companyField = form.querySelector('[name*="company"], [name*="organization"]');
1038
+ const phoneField = form.querySelector('[type="tel"], [name*="phone"]');
1039
+ if (firstNameField?.value)
1040
+ traits.firstName = firstNameField.value;
1041
+ if (lastNameField?.value)
1042
+ traits.lastName = lastNameField.value;
1043
+ if (companyField?.value)
1044
+ traits.company = companyField.value;
1045
+ if (phoneField?.value)
1046
+ traits.phone = phoneField.value;
1047
+ this.tracker.identify(email, traits);
1048
+ }
1049
+ }
1050
+
1051
+ /**
1052
+ * Clianta SDK - Click Tracking Plugin
1053
+ * @see SDK_VERSION in core/config.ts
1054
+ */
1055
+ /**
1056
+ * Click Tracking Plugin - Tracks button and CTA clicks
1057
+ */
1058
+ class ClicksPlugin extends BasePlugin {
1059
+ constructor() {
1060
+ super(...arguments);
1061
+ this.name = 'clicks';
1062
+ this.boundHandler = null;
1063
+ }
1064
+ init(tracker) {
1065
+ super.init(tracker);
1066
+ if (typeof document !== 'undefined') {
1067
+ this.boundHandler = this.handleClick.bind(this);
1068
+ document.addEventListener('click', this.boundHandler, true);
1069
+ }
1070
+ }
1071
+ destroy() {
1072
+ if (this.boundHandler && typeof document !== 'undefined') {
1073
+ document.removeEventListener('click', this.boundHandler, true);
1074
+ }
1075
+ super.destroy();
1076
+ }
1077
+ handleClick(e) {
1078
+ const target = e.target;
1079
+ if (!target || !isTrackableClickElement(target))
1080
+ return;
1081
+ const buttonText = getElementText(target, 100);
1082
+ const elementInfo = getElementInfo(target);
1083
+ this.track('button_click', 'Button Clicked', {
1084
+ buttonText,
1085
+ elementType: target.tagName.toLowerCase(),
1086
+ elementId: elementInfo.id,
1087
+ elementClass: elementInfo.className,
1088
+ href: target.href || undefined,
1089
+ });
1090
+ }
1091
+ }
1092
+
1093
+ /**
1094
+ * Clianta SDK - Engagement Plugin
1095
+ * @see SDK_VERSION in core/config.ts
1096
+ */
1097
+ /**
1098
+ * Engagement Plugin - Tracks user engagement and time on page
1099
+ */
1100
+ class EngagementPlugin extends BasePlugin {
1101
+ constructor() {
1102
+ super(...arguments);
1103
+ this.name = 'engagement';
1104
+ this.pageLoadTime = 0;
1105
+ this.engagementStartTime = 0;
1106
+ this.isEngaged = false;
1107
+ this.engagementTimeout = null;
1108
+ this.boundMarkEngaged = null;
1109
+ this.boundTrackTimeOnPage = null;
1110
+ }
1111
+ init(tracker) {
1112
+ super.init(tracker);
1113
+ this.pageLoadTime = Date.now();
1114
+ this.engagementStartTime = Date.now();
1115
+ if (typeof document === 'undefined' || typeof window === 'undefined')
1116
+ return;
1117
+ // Setup engagement detection
1118
+ this.boundMarkEngaged = this.markEngaged.bind(this);
1119
+ this.boundTrackTimeOnPage = this.trackTimeOnPage.bind(this);
1120
+ ['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
1121
+ document.addEventListener(event, this.boundMarkEngaged, { passive: true });
1122
+ });
1123
+ // Track time on page before unload
1124
+ window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
1125
+ window.addEventListener('visibilitychange', () => {
1126
+ if (document.visibilityState === 'hidden') {
1127
+ this.trackTimeOnPage();
1128
+ }
1129
+ else {
1130
+ // Reset engagement timer when page becomes visible again
1131
+ this.engagementStartTime = Date.now();
1132
+ }
1133
+ });
1134
+ }
1135
+ destroy() {
1136
+ if (this.boundMarkEngaged && typeof document !== 'undefined') {
1137
+ ['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
1138
+ document.removeEventListener(event, this.boundMarkEngaged);
1139
+ });
1140
+ }
1141
+ if (this.boundTrackTimeOnPage && typeof window !== 'undefined') {
1142
+ window.removeEventListener('beforeunload', this.boundTrackTimeOnPage);
1143
+ }
1144
+ if (this.engagementTimeout) {
1145
+ clearTimeout(this.engagementTimeout);
1146
+ }
1147
+ super.destroy();
1148
+ }
1149
+ markEngaged() {
1150
+ if (!this.isEngaged) {
1151
+ this.isEngaged = true;
1152
+ this.track('engagement', 'User Engaged', {
1153
+ timeToEngage: Date.now() - this.pageLoadTime,
1154
+ });
1155
+ }
1156
+ // Reset engagement timeout
1157
+ if (this.engagementTimeout) {
1158
+ clearTimeout(this.engagementTimeout);
1159
+ }
1160
+ this.engagementTimeout = setTimeout(() => {
1161
+ this.isEngaged = false;
1162
+ }, 30000); // 30 seconds of inactivity
1163
+ }
1164
+ trackTimeOnPage() {
1165
+ const timeSpent = Math.floor((Date.now() - this.engagementStartTime) / 1000);
1166
+ if (timeSpent > 0) {
1167
+ this.track('time_on_page', 'Time Spent', {
1168
+ seconds: timeSpent,
1169
+ engaged: this.isEngaged,
1170
+ });
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ /**
1176
+ * Clianta SDK - Downloads Plugin
1177
+ * @see SDK_VERSION in core/config.ts
1178
+ */
1179
+ /**
1180
+ * Downloads Plugin - Tracks file downloads
1181
+ */
1182
+ class DownloadsPlugin extends BasePlugin {
1183
+ constructor() {
1184
+ super(...arguments);
1185
+ this.name = 'downloads';
1186
+ this.trackedDownloads = new Set();
1187
+ this.boundHandler = null;
1188
+ }
1189
+ init(tracker) {
1190
+ super.init(tracker);
1191
+ if (typeof document !== 'undefined') {
1192
+ this.boundHandler = this.handleClick.bind(this);
1193
+ document.addEventListener('click', this.boundHandler, true);
1194
+ }
1195
+ }
1196
+ destroy() {
1197
+ if (this.boundHandler && typeof document !== 'undefined') {
1198
+ document.removeEventListener('click', this.boundHandler, true);
1199
+ }
1200
+ super.destroy();
1201
+ }
1202
+ handleClick(e) {
1203
+ const link = e.target.closest('a');
1204
+ if (!link || !link.href)
1205
+ return;
1206
+ const url = link.href;
1207
+ // Check if it's a download link
1208
+ if (!isDownloadUrl(url))
1209
+ return;
1210
+ // Avoid tracking the same download multiple times
1211
+ if (this.trackedDownloads.has(url))
1212
+ return;
1213
+ this.trackedDownloads.add(url);
1214
+ this.track('download', 'File Download', {
1215
+ url,
1216
+ filename: getFilenameFromUrl(url),
1217
+ fileType: getFileExtension(url),
1218
+ linkText: getElementText(link, 100),
1219
+ });
1220
+ }
1221
+ }
1222
+
1223
+ /**
1224
+ * Clianta SDK - Exit Intent Plugin
1225
+ * @see SDK_VERSION in core/config.ts
1226
+ */
1227
+ /**
1228
+ * Exit Intent Plugin - Detects when user intends to leave the page
1229
+ */
1230
+ class ExitIntentPlugin extends BasePlugin {
1231
+ constructor() {
1232
+ super(...arguments);
1233
+ this.name = 'exitIntent';
1234
+ this.exitIntentShown = false;
1235
+ this.pageLoadTime = 0;
1236
+ this.boundHandler = null;
1237
+ }
1238
+ init(tracker) {
1239
+ super.init(tracker);
1240
+ this.pageLoadTime = Date.now();
1241
+ // Skip on mobile (no mouse events)
1242
+ if (isMobile())
1243
+ return;
1244
+ if (typeof document !== 'undefined') {
1245
+ this.boundHandler = this.handleMouseLeave.bind(this);
1246
+ document.addEventListener('mouseleave', this.boundHandler);
1247
+ }
1248
+ }
1249
+ destroy() {
1250
+ if (this.boundHandler && typeof document !== 'undefined') {
1251
+ document.removeEventListener('mouseleave', this.boundHandler);
1252
+ }
1253
+ super.destroy();
1254
+ }
1255
+ handleMouseLeave(e) {
1256
+ // Only trigger when mouse leaves from the top of the page
1257
+ if (e.clientY > 0 || this.exitIntentShown)
1258
+ return;
1259
+ this.exitIntentShown = true;
1260
+ this.track('exit_intent', 'Exit Intent Detected', {
1261
+ timeOnPage: Date.now() - this.pageLoadTime,
1262
+ });
1263
+ }
1264
+ }
1265
+
1266
+ /**
1267
+ * Clianta SDK - Error Tracking Plugin
1268
+ * @see SDK_VERSION in core/config.ts
1269
+ */
1270
+ /**
1271
+ * Error Tracking Plugin - Tracks JavaScript errors
1272
+ */
1273
+ class ErrorsPlugin extends BasePlugin {
1274
+ constructor() {
1275
+ super(...arguments);
1276
+ this.name = 'errors';
1277
+ this.boundErrorHandler = null;
1278
+ this.boundRejectionHandler = null;
1279
+ }
1280
+ init(tracker) {
1281
+ super.init(tracker);
1282
+ if (typeof window !== 'undefined') {
1283
+ this.boundErrorHandler = this.handleError.bind(this);
1284
+ this.boundRejectionHandler = this.handleRejection.bind(this);
1285
+ window.addEventListener('error', this.boundErrorHandler);
1286
+ window.addEventListener('unhandledrejection', this.boundRejectionHandler);
1287
+ }
1288
+ }
1289
+ destroy() {
1290
+ if (typeof window !== 'undefined') {
1291
+ if (this.boundErrorHandler) {
1292
+ window.removeEventListener('error', this.boundErrorHandler);
1293
+ }
1294
+ if (this.boundRejectionHandler) {
1295
+ window.removeEventListener('unhandledrejection', this.boundRejectionHandler);
1296
+ }
1297
+ }
1298
+ super.destroy();
1299
+ }
1300
+ handleError(e) {
1301
+ this.track('error', 'JavaScript Error', {
1302
+ message: e.message,
1303
+ filename: e.filename,
1304
+ line: e.lineno,
1305
+ column: e.colno,
1306
+ stack: e.error?.stack?.substring(0, 500),
1307
+ });
1308
+ }
1309
+ handleRejection(e) {
1310
+ this.track('error', 'Unhandled Promise Rejection', {
1311
+ reason: String(e.reason).substring(0, 200),
1312
+ });
1313
+ }
1314
+ }
1315
+
1316
+ /**
1317
+ * Clianta SDK - Performance Plugin
1318
+ * @see SDK_VERSION in core/config.ts
1319
+ */
1320
+ /**
1321
+ * Performance Plugin - Tracks page performance and Web Vitals
1322
+ */
1323
+ class PerformancePlugin extends BasePlugin {
1324
+ constructor() {
1325
+ super(...arguments);
1326
+ this.name = 'performance';
1327
+ }
1328
+ init(tracker) {
1329
+ super.init(tracker);
1330
+ if (typeof window !== 'undefined') {
1331
+ // Track performance after page load
1332
+ window.addEventListener('load', () => {
1333
+ // Delay to ensure all metrics are available
1334
+ setTimeout(() => this.trackPerformance(), 100);
1335
+ });
1336
+ }
1337
+ }
1338
+ trackPerformance() {
1339
+ if (typeof performance === 'undefined')
1340
+ return;
1341
+ // Use modern Navigation Timing API (PerformanceNavigationTiming)
1342
+ const entries = performance.getEntriesByType('navigation');
1343
+ if (entries.length > 0) {
1344
+ const navTiming = entries[0];
1345
+ const loadTime = Math.round(navTiming.loadEventEnd - navTiming.startTime);
1346
+ const domReady = Math.round(navTiming.domContentLoadedEventEnd - navTiming.startTime);
1347
+ const ttfb = Math.round(navTiming.responseStart - navTiming.requestStart);
1348
+ const domInteractive = Math.round(navTiming.domInteractive - navTiming.startTime);
1349
+ this.track('performance', 'Page Performance', {
1350
+ loadTime,
1351
+ domReady,
1352
+ ttfb, // Time to First Byte
1353
+ domInteractive,
1354
+ // Additional modern metrics
1355
+ dns: Math.round(navTiming.domainLookupEnd - navTiming.domainLookupStart),
1356
+ connection: Math.round(navTiming.connectEnd - navTiming.connectStart),
1357
+ transferSize: navTiming.transferSize,
1358
+ });
1359
+ }
1360
+ else {
1361
+ // Fallback for older browsers using deprecated API
1362
+ const timing = performance.timing;
1363
+ if (!timing)
1364
+ return;
1365
+ const loadTime = timing.loadEventEnd - timing.navigationStart;
1366
+ const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;
1367
+ const ttfb = timing.responseStart - timing.navigationStart;
1368
+ const domInteractive = timing.domInteractive - timing.navigationStart;
1369
+ this.track('performance', 'Page Performance', {
1370
+ loadTime,
1371
+ domReady,
1372
+ ttfb,
1373
+ domInteractive,
1374
+ });
1375
+ }
1376
+ // Track Web Vitals if available
1377
+ this.trackWebVitals();
1378
+ }
1379
+ trackWebVitals() {
1380
+ // LCP (Largest Contentful Paint)
1381
+ if ('PerformanceObserver' in window) {
1382
+ try {
1383
+ const lcpObserver = new PerformanceObserver((entryList) => {
1384
+ const entries = entryList.getEntries();
1385
+ const lastEntry = entries[entries.length - 1];
1386
+ if (lastEntry) {
1387
+ this.track('performance', 'Web Vital - LCP', {
1388
+ metric: 'LCP',
1389
+ value: Math.round(lastEntry.startTime),
1390
+ });
1391
+ }
1392
+ });
1393
+ lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
1394
+ }
1395
+ catch {
1396
+ // LCP not supported
1397
+ }
1398
+ // FID (First Input Delay)
1399
+ try {
1400
+ const fidObserver = new PerformanceObserver((entryList) => {
1401
+ const entries = entryList.getEntries();
1402
+ const firstEntry = entries[0];
1403
+ if (firstEntry) {
1404
+ this.track('performance', 'Web Vital - FID', {
1405
+ metric: 'FID',
1406
+ value: Math.round(firstEntry.processingStart - firstEntry.startTime),
1407
+ });
1408
+ }
1409
+ });
1410
+ fidObserver.observe({ type: 'first-input', buffered: true });
1411
+ }
1412
+ catch {
1413
+ // FID not supported
1414
+ }
1415
+ // CLS (Cumulative Layout Shift)
1416
+ try {
1417
+ let clsValue = 0;
1418
+ const clsObserver = new PerformanceObserver((entryList) => {
1419
+ const entries = entryList.getEntries();
1420
+ entries.forEach((entry) => {
1421
+ if (!entry.hadRecentInput) {
1422
+ clsValue += entry.value || 0;
1423
+ }
1424
+ });
1425
+ });
1426
+ clsObserver.observe({ type: 'layout-shift', buffered: true });
1427
+ // Report CLS after page is hidden
1428
+ window.addEventListener('visibilitychange', () => {
1429
+ if (document.visibilityState === 'hidden' && clsValue > 0) {
1430
+ this.track('performance', 'Web Vital - CLS', {
1431
+ metric: 'CLS',
1432
+ value: Math.round(clsValue * 1000) / 1000,
1433
+ });
1434
+ }
1435
+ }, { once: true });
1436
+ }
1437
+ catch {
1438
+ // CLS not supported
1439
+ }
1440
+ }
1441
+ }
1442
+ }
1443
+
1444
+ /**
1445
+ * Clianta Tracking SDK - Popup Forms Plugin
1446
+ * @see SDK_VERSION in core/config.ts
1447
+ *
1448
+ * Auto-loads and displays lead capture popups based on triggers
1449
+ */
1450
+ /**
1451
+ * Popup Forms Plugin - Fetches and displays lead capture forms
1452
+ */
1453
+ class PopupFormsPlugin extends BasePlugin {
1454
+ constructor() {
1455
+ super(...arguments);
1456
+ this.name = 'popupForms';
1457
+ this.forms = [];
1458
+ this.shownForms = new Set();
1459
+ this.scrollHandler = null;
1460
+ this.exitHandler = null;
1461
+ }
1462
+ async init(tracker) {
1463
+ super.init(tracker);
1464
+ if (typeof window === 'undefined')
1465
+ return;
1466
+ // Load shown forms from storage
1467
+ this.loadShownForms();
1468
+ // Fetch active forms
1469
+ await this.fetchForms();
1470
+ // Setup triggers
1471
+ this.setupTriggers();
1472
+ }
1473
+ destroy() {
1474
+ this.removeTriggers();
1475
+ super.destroy();
1476
+ }
1477
+ loadShownForms() {
1478
+ try {
1479
+ const stored = localStorage.getItem('clianta_shown_forms');
1480
+ if (stored) {
1481
+ const data = JSON.parse(stored);
1482
+ this.shownForms = new Set(data.forms || []);
1483
+ }
1484
+ }
1485
+ catch (e) {
1486
+ // Ignore storage errors
1487
+ }
1488
+ }
1489
+ saveShownForms() {
1490
+ try {
1491
+ localStorage.setItem('clianta_shown_forms', JSON.stringify({
1492
+ forms: Array.from(this.shownForms),
1493
+ timestamp: Date.now(),
1494
+ }));
1495
+ }
1496
+ catch (e) {
1497
+ // Ignore storage errors
1498
+ }
1499
+ }
1500
+ async fetchForms() {
1501
+ if (!this.tracker)
1502
+ return;
1503
+ const config = this.tracker.getConfig();
1504
+ const workspaceId = this.tracker.getWorkspaceId();
1505
+ const apiEndpoint = config.apiEndpoint || 'https://api.clianta.online';
1506
+ try {
1507
+ const url = encodeURIComponent(window.location.href);
1508
+ const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${workspaceId}?url=${url}`);
1509
+ if (!response.ok)
1510
+ return;
1511
+ const data = await response.json();
1512
+ if (data.success && Array.isArray(data.data)) {
1513
+ this.forms = data.data.filter((form) => this.shouldShowForm(form));
1514
+ }
1515
+ }
1516
+ catch (error) {
1517
+ console.error('[Clianta] Failed to fetch forms:', error);
1518
+ }
1519
+ }
1520
+ shouldShowForm(form) {
1521
+ // Check show frequency
1522
+ if (form.showFrequency === 'once_per_visitor') {
1523
+ if (this.shownForms.has(form._id))
1524
+ return false;
1525
+ }
1526
+ else if (form.showFrequency === 'once_per_session') {
1527
+ const sessionKey = `clianta_form_${form._id}_shown`;
1528
+ if (sessionStorage.getItem(sessionKey))
1529
+ return false;
1530
+ }
1531
+ return true;
1532
+ }
1533
+ setupTriggers() {
1534
+ this.forms.forEach(form => {
1535
+ switch (form.trigger.type) {
1536
+ case 'delay':
1537
+ setTimeout(() => this.showForm(form), (form.trigger.value || 5) * 1000);
1538
+ break;
1539
+ case 'scroll':
1540
+ this.setupScrollTrigger(form);
1541
+ break;
1542
+ case 'exit_intent':
1543
+ this.setupExitIntentTrigger(form);
1544
+ break;
1545
+ case 'click':
1546
+ this.setupClickTrigger(form);
1547
+ break;
1548
+ }
1549
+ });
1550
+ }
1551
+ setupScrollTrigger(form) {
1552
+ const threshold = form.trigger.value || 50;
1553
+ this.scrollHandler = () => {
1554
+ const scrollPercent = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
1555
+ if (scrollPercent >= threshold) {
1556
+ this.showForm(form);
1557
+ if (this.scrollHandler) {
1558
+ window.removeEventListener('scroll', this.scrollHandler);
1559
+ }
1560
+ }
1561
+ };
1562
+ window.addEventListener('scroll', this.scrollHandler, { passive: true });
1563
+ }
1564
+ setupExitIntentTrigger(form) {
1565
+ this.exitHandler = (e) => {
1566
+ if (e.clientY <= 0) {
1567
+ this.showForm(form);
1568
+ if (this.exitHandler) {
1569
+ document.removeEventListener('mouseout', this.exitHandler);
1570
+ }
1571
+ }
1572
+ };
1573
+ document.addEventListener('mouseout', this.exitHandler);
1574
+ }
1575
+ setupClickTrigger(form) {
1576
+ if (!form.trigger.selector)
1577
+ return;
1578
+ const elements = document.querySelectorAll(form.trigger.selector);
1579
+ elements.forEach(el => {
1580
+ el.addEventListener('click', () => this.showForm(form));
1581
+ });
1582
+ }
1583
+ removeTriggers() {
1584
+ if (this.scrollHandler) {
1585
+ window.removeEventListener('scroll', this.scrollHandler);
1586
+ }
1587
+ if (this.exitHandler) {
1588
+ document.removeEventListener('mouseout', this.exitHandler);
1589
+ }
1590
+ }
1591
+ async showForm(form) {
1592
+ // Check if already shown in this session
1593
+ if (!this.shouldShowForm(form))
1594
+ return;
1595
+ // Mark as shown
1596
+ this.shownForms.add(form._id);
1597
+ this.saveShownForms();
1598
+ sessionStorage.setItem(`clianta_form_${form._id}_shown`, 'true');
1599
+ // Track view
1600
+ await this.trackFormView(form._id);
1601
+ // Render form
1602
+ this.renderForm(form);
1603
+ }
1604
+ async trackFormView(formId) {
1605
+ if (!this.tracker)
1606
+ return;
1607
+ const config = this.tracker.getConfig();
1608
+ const apiEndpoint = config.apiEndpoint || 'https://api.clianta.online';
1609
+ try {
1610
+ await fetch(`${apiEndpoint}/api/public/lead-forms/${formId}/view`, {
1611
+ method: 'POST',
1612
+ headers: { 'Content-Type': 'application/json' },
1613
+ });
1614
+ }
1615
+ catch (e) {
1616
+ // Ignore tracking errors
1617
+ }
1618
+ }
1619
+ renderForm(form) {
1620
+ // Create overlay
1621
+ const overlay = document.createElement('div');
1622
+ overlay.id = `clianta-form-overlay-${form._id}`;
1623
+ overlay.style.cssText = `
1624
+ position: fixed;
1625
+ top: 0;
1626
+ left: 0;
1627
+ right: 0;
1628
+ bottom: 0;
1629
+ background: rgba(0, 0, 0, 0.5);
1630
+ z-index: 999998;
1631
+ display: flex;
1632
+ align-items: center;
1633
+ justify-content: center;
1634
+ opacity: 0;
1635
+ transition: opacity 0.3s ease;
1636
+ `;
1637
+ // Create form container
1638
+ const container = document.createElement('div');
1639
+ container.id = `clianta-form-${form._id}`;
1640
+ const style = form.style || {};
1641
+ container.style.cssText = `
1642
+ background: ${style.backgroundColor || '#FFFFFF'};
1643
+ border-radius: ${style.borderRadius || 12}px;
1644
+ padding: 24px;
1645
+ max-width: 400px;
1646
+ width: 90%;
1647
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
1648
+ transform: translateY(20px);
1649
+ opacity: 0;
1650
+ transition: all 0.3s ease;
1651
+ `;
1652
+ // Build form using safe DOM APIs (no innerHTML for user content)
1653
+ this.buildFormDOM(form, container);
1654
+ overlay.appendChild(container);
1655
+ document.body.appendChild(overlay);
1656
+ // Animate in
1657
+ requestAnimationFrame(() => {
1658
+ overlay.style.opacity = '1';
1659
+ container.style.transform = 'translateY(0)';
1660
+ container.style.opacity = '1';
1661
+ });
1662
+ // Setup event listeners
1663
+ this.setupFormEvents(form, overlay, container);
1664
+ }
1665
+ /**
1666
+ * Escape HTML to prevent XSS - used only for static structure
1667
+ */
1668
+ escapeHTML(str) {
1669
+ const div = document.createElement('div');
1670
+ div.textContent = str;
1671
+ return div.innerHTML;
1672
+ }
1673
+ /**
1674
+ * Build form using safe DOM APIs (prevents XSS)
1675
+ */
1676
+ buildFormDOM(form, container) {
1677
+ const style = form.style || {};
1678
+ const primaryColor = style.primaryColor || '#10B981';
1679
+ const textColor = style.textColor || '#18181B';
1680
+ // Close button
1681
+ const closeBtn = document.createElement('button');
1682
+ closeBtn.id = 'clianta-form-close';
1683
+ closeBtn.style.cssText = `
1684
+ position: absolute;
1685
+ top: 12px;
1686
+ right: 12px;
1687
+ background: none;
1688
+ border: none;
1689
+ font-size: 20px;
1690
+ cursor: pointer;
1691
+ color: #71717A;
1692
+ padding: 4px;
1693
+ `;
1694
+ closeBtn.textContent = '×';
1695
+ container.appendChild(closeBtn);
1696
+ // Headline
1697
+ const headline = document.createElement('h2');
1698
+ headline.style.cssText = `font-size: 20px; font-weight: 700; margin-bottom: 8px; color: ${this.escapeHTML(textColor)};`;
1699
+ headline.textContent = form.headline || 'Stay in touch';
1700
+ container.appendChild(headline);
1701
+ // Subheadline
1702
+ const subheadline = document.createElement('p');
1703
+ subheadline.style.cssText = 'font-size: 14px; color: #71717A; margin-bottom: 16px;';
1704
+ subheadline.textContent = form.subheadline || 'Get the latest updates';
1705
+ container.appendChild(subheadline);
1706
+ // Form element
1707
+ const formElement = document.createElement('form');
1708
+ formElement.id = 'clianta-form-element';
1709
+ // Build fields
1710
+ form.fields.forEach(field => {
1711
+ const fieldWrapper = document.createElement('div');
1712
+ fieldWrapper.style.marginBottom = '12px';
1713
+ if (field.type === 'checkbox') {
1714
+ // Checkbox layout
1715
+ const label = document.createElement('label');
1716
+ label.style.cssText = `display: flex; align-items: center; gap: 8px; font-size: 14px; color: ${this.escapeHTML(textColor)}; cursor: pointer;`;
1717
+ const input = document.createElement('input');
1718
+ input.type = 'checkbox';
1719
+ input.name = field.name;
1720
+ if (field.required)
1721
+ input.required = true;
1722
+ input.style.cssText = 'width: 16px; height: 16px;';
1723
+ label.appendChild(input);
1724
+ const labelText = document.createTextNode(field.label + ' ');
1725
+ label.appendChild(labelText);
1726
+ if (field.required) {
1727
+ const requiredMark = document.createElement('span');
1728
+ requiredMark.style.color = '#EF4444';
1729
+ requiredMark.textContent = '*';
1730
+ label.appendChild(requiredMark);
1731
+ }
1732
+ fieldWrapper.appendChild(label);
1733
+ }
1734
+ else {
1735
+ // Label
1736
+ const label = document.createElement('label');
1737
+ label.style.cssText = `display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${this.escapeHTML(textColor)};`;
1738
+ label.textContent = field.label + ' ';
1739
+ if (field.required) {
1740
+ const requiredMark = document.createElement('span');
1741
+ requiredMark.style.color = '#EF4444';
1742
+ requiredMark.textContent = '*';
1743
+ label.appendChild(requiredMark);
1744
+ }
1745
+ fieldWrapper.appendChild(label);
1746
+ // Input/Textarea
1747
+ if (field.type === 'textarea') {
1748
+ const textarea = document.createElement('textarea');
1749
+ textarea.name = field.name;
1750
+ if (field.placeholder)
1751
+ textarea.placeholder = field.placeholder;
1752
+ if (field.required)
1753
+ textarea.required = true;
1754
+ textarea.style.cssText = 'width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; resize: vertical; min-height: 80px; box-sizing: border-box;';
1755
+ fieldWrapper.appendChild(textarea);
1756
+ }
1757
+ else {
1758
+ const input = document.createElement('input');
1759
+ input.type = field.type;
1760
+ input.name = field.name;
1761
+ if (field.placeholder)
1762
+ input.placeholder = field.placeholder;
1763
+ if (field.required)
1764
+ input.required = true;
1765
+ input.style.cssText = 'width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box;';
1766
+ fieldWrapper.appendChild(input);
1767
+ }
1768
+ }
1769
+ formElement.appendChild(fieldWrapper);
1770
+ });
1771
+ // Submit button
1772
+ const submitBtn = document.createElement('button');
1773
+ submitBtn.type = 'submit';
1774
+ submitBtn.style.cssText = `
1775
+ width: 100%;
1776
+ padding: 10px 16px;
1777
+ background: ${this.escapeHTML(primaryColor)};
1778
+ color: white;
1779
+ border: none;
1780
+ border-radius: 6px;
1781
+ font-size: 14px;
1782
+ font-weight: 500;
1783
+ cursor: pointer;
1784
+ margin-top: 8px;
1785
+ `;
1786
+ submitBtn.textContent = form.submitButtonText || 'Subscribe';
1787
+ formElement.appendChild(submitBtn);
1788
+ container.appendChild(formElement);
1789
+ }
1790
+ buildFormHTML(form) {
1791
+ const style = form.style || {};
1792
+ const primaryColor = style.primaryColor || '#10B981';
1793
+ const textColor = style.textColor || '#18181B';
1794
+ let fieldsHTML = form.fields.map(field => {
1795
+ const requiredMark = field.required ? '<span style="color: #EF4444;">*</span>' : '';
1796
+ if (field.type === 'textarea') {
1797
+ return `
1798
+ <div style="margin-bottom: 12px;">
1799
+ <label style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${textColor};">
1800
+ ${field.label} ${requiredMark}
1801
+ </label>
1802
+ <textarea
1803
+ name="${field.name}"
1804
+ placeholder="${field.placeholder || ''}"
1805
+ ${field.required ? 'required' : ''}
1806
+ style="width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; resize: vertical; min-height: 80px;"
1807
+ ></textarea>
1808
+ </div>
1809
+ `;
1810
+ }
1811
+ else if (field.type === 'checkbox') {
1812
+ return `
1813
+ <div style="margin-bottom: 12px;">
1814
+ <label style="display: flex; align-items: center; gap: 8px; font-size: 14px; color: ${textColor}; cursor: pointer;">
1815
+ <input
1816
+ type="checkbox"
1817
+ name="${field.name}"
1818
+ ${field.required ? 'required' : ''}
1819
+ style="width: 16px; height: 16px;"
1820
+ />
1821
+ ${field.label} ${requiredMark}
1822
+ </label>
1823
+ </div>
1824
+ `;
1825
+ }
1826
+ else {
1827
+ return `
1828
+ <div style="margin-bottom: 12px;">
1829
+ <label style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${textColor};">
1830
+ ${field.label} ${requiredMark}
1831
+ </label>
1832
+ <input
1833
+ type="${field.type}"
1834
+ name="${field.name}"
1835
+ placeholder="${field.placeholder || ''}"
1836
+ ${field.required ? 'required' : ''}
1837
+ style="width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box;"
1838
+ />
1839
+ </div>
1840
+ `;
1841
+ }
1842
+ }).join('');
1843
+ return `
1844
+ <button id="clianta-form-close" style="
1845
+ position: absolute;
1846
+ top: 12px;
1847
+ right: 12px;
1848
+ background: none;
1849
+ border: none;
1850
+ font-size: 20px;
1851
+ cursor: pointer;
1852
+ color: #71717A;
1853
+ padding: 4px;
1854
+ ">&times;</button>
1855
+ <h2 style="font-size: 20px; font-weight: 700; margin-bottom: 8px; color: ${textColor};">
1856
+ ${form.headline || 'Stay in touch'}
1857
+ </h2>
1858
+ <p style="font-size: 14px; color: #71717A; margin-bottom: 16px;">
1859
+ ${form.subheadline || 'Get the latest updates'}
1860
+ </p>
1861
+ <form id="clianta-form-element">
1862
+ ${fieldsHTML}
1863
+ <button type="submit" style="
1864
+ width: 100%;
1865
+ padding: 10px 16px;
1866
+ background: ${primaryColor};
1867
+ color: white;
1868
+ border: none;
1869
+ border-radius: 6px;
1870
+ font-size: 14px;
1871
+ font-weight: 500;
1872
+ cursor: pointer;
1873
+ margin-top: 8px;
1874
+ ">
1875
+ ${form.submitButtonText || 'Subscribe'}
1876
+ </button>
1877
+ </form>
1878
+ `;
1879
+ }
1880
+ setupFormEvents(form, overlay, container) {
1881
+ // Close button
1882
+ const closeBtn = container.querySelector('#clianta-form-close');
1883
+ if (closeBtn) {
1884
+ closeBtn.addEventListener('click', () => this.closeForm(form._id, overlay, container));
1885
+ }
1886
+ // Overlay click
1887
+ overlay.addEventListener('click', (e) => {
1888
+ if (e.target === overlay) {
1889
+ this.closeForm(form._id, overlay, container);
1890
+ }
1891
+ });
1892
+ // Form submit
1893
+ const formElement = container.querySelector('#clianta-form-element');
1894
+ if (formElement) {
1895
+ formElement.addEventListener('submit', async (e) => {
1896
+ e.preventDefault();
1897
+ await this.handleSubmit(form, formElement, container);
1898
+ });
1899
+ }
1900
+ }
1901
+ closeForm(formId, overlay, container) {
1902
+ container.style.transform = 'translateY(20px)';
1903
+ container.style.opacity = '0';
1904
+ overlay.style.opacity = '0';
1905
+ setTimeout(() => {
1906
+ overlay.remove();
1907
+ }, 300);
1908
+ }
1909
+ async handleSubmit(form, formElement, container) {
1910
+ if (!this.tracker)
1911
+ return;
1912
+ const config = this.tracker.getConfig();
1913
+ const apiEndpoint = config.apiEndpoint || 'https://api.clianta.online';
1914
+ const visitorId = this.tracker.getVisitorId();
1915
+ // Collect form data
1916
+ const formData = new FormData(formElement);
1917
+ const data = {};
1918
+ formData.forEach((value, key) => {
1919
+ data[key] = value;
1920
+ });
1921
+ // Disable submit button
1922
+ const submitBtn = formElement.querySelector('button[type="submit"]');
1923
+ if (submitBtn) {
1924
+ submitBtn.disabled = true;
1925
+ submitBtn.innerHTML = 'Submitting...';
1926
+ }
1927
+ try {
1928
+ const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${form._id}/submit`, {
1929
+ method: 'POST',
1930
+ headers: { 'Content-Type': 'application/json' },
1931
+ body: JSON.stringify({
1932
+ visitorId,
1933
+ data,
1934
+ url: window.location.href,
1935
+ }),
1936
+ });
1937
+ const result = await response.json();
1938
+ if (result.success) {
1939
+ // Show success message using safe DOM APIs
1940
+ container.innerHTML = '';
1941
+ const successWrapper = document.createElement('div');
1942
+ successWrapper.style.cssText = 'text-align: center; padding: 20px;';
1943
+ const iconWrapper = document.createElement('div');
1944
+ iconWrapper.style.cssText = 'width: 48px; height: 48px; background: #10B981; border-radius: 50%; margin: 0 auto 16px; display: flex; align-items: center; justify-content: center;';
1945
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1946
+ svg.setAttribute('width', '24');
1947
+ svg.setAttribute('height', '24');
1948
+ svg.setAttribute('viewBox', '0 0 24 24');
1949
+ svg.setAttribute('fill', 'none');
1950
+ svg.setAttribute('stroke', 'white');
1951
+ svg.setAttribute('stroke-width', '2');
1952
+ const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
1953
+ polyline.setAttribute('points', '20 6 9 17 4 12');
1954
+ svg.appendChild(polyline);
1955
+ iconWrapper.appendChild(svg);
1956
+ const message = document.createElement('p');
1957
+ message.style.cssText = 'font-size: 16px; font-weight: 500; color: #18181B;';
1958
+ message.textContent = form.successMessage || 'Thank you!';
1959
+ successWrapper.appendChild(iconWrapper);
1960
+ successWrapper.appendChild(message);
1961
+ container.appendChild(successWrapper);
1962
+ // Track identify
1963
+ if (data.email) {
1964
+ this.tracker?.identify(data.email, data);
1965
+ }
1966
+ // Redirect if configured
1967
+ if (form.redirectUrl) {
1968
+ setTimeout(() => {
1969
+ window.location.href = form.redirectUrl;
1970
+ }, 1500);
1971
+ }
1972
+ // Close after delay
1973
+ setTimeout(() => {
1974
+ const overlay = document.getElementById(`clianta-form-overlay-${form._id}`);
1975
+ if (overlay) {
1976
+ this.closeForm(form._id, overlay, container);
1977
+ }
1978
+ }, 2000);
1979
+ }
1980
+ }
1981
+ catch (error) {
1982
+ console.error('[Clianta] Form submit error:', error);
1983
+ if (submitBtn) {
1984
+ submitBtn.disabled = false;
1985
+ submitBtn.innerHTML = form.submitButtonText || 'Subscribe';
1986
+ }
1987
+ }
1988
+ }
1989
+ }
1990
+
1991
+ /**
1992
+ * Clianta SDK - Plugins Index
1993
+ * Version is defined in core/config.ts as SDK_VERSION
1994
+ */
1995
+ /**
1996
+ * Get plugin instance by name
1997
+ */
1998
+ function getPlugin(name) {
1999
+ switch (name) {
2000
+ case 'pageView':
2001
+ return new PageViewPlugin();
2002
+ case 'scroll':
2003
+ return new ScrollPlugin();
2004
+ case 'forms':
2005
+ return new FormsPlugin();
2006
+ case 'clicks':
2007
+ return new ClicksPlugin();
2008
+ case 'engagement':
2009
+ return new EngagementPlugin();
2010
+ case 'downloads':
2011
+ return new DownloadsPlugin();
2012
+ case 'exitIntent':
2013
+ return new ExitIntentPlugin();
2014
+ case 'errors':
2015
+ return new ErrorsPlugin();
2016
+ case 'performance':
2017
+ return new PerformancePlugin();
2018
+ case 'popupForms':
2019
+ return new PopupFormsPlugin();
2020
+ default:
2021
+ throw new Error(`Unknown plugin: ${name}`);
2022
+ }
2023
+ }
2024
+
2025
+ /**
2026
+ * Clianta SDK - Consent Storage
2027
+ * Handles persistence of consent state
2028
+ * @see SDK_VERSION in core/config.ts
2029
+ */
2030
+ const CONSENT_VERSION = 1;
2031
+ /**
2032
+ * Save consent state to storage
2033
+ */
2034
+ function saveConsent(state) {
2035
+ try {
2036
+ if (typeof localStorage === 'undefined')
2037
+ return false;
2038
+ const stored = {
2039
+ state,
2040
+ timestamp: Date.now(),
2041
+ version: CONSENT_VERSION,
2042
+ };
2043
+ localStorage.setItem(STORAGE_KEYS.CONSENT, JSON.stringify(stored));
2044
+ return true;
2045
+ }
2046
+ catch {
2047
+ return false;
2048
+ }
2049
+ }
2050
+ /**
2051
+ * Load consent state from storage
2052
+ */
2053
+ function loadConsent() {
2054
+ try {
2055
+ if (typeof localStorage === 'undefined')
2056
+ return null;
2057
+ const stored = localStorage.getItem(STORAGE_KEYS.CONSENT);
2058
+ if (!stored)
2059
+ return null;
2060
+ const parsed = JSON.parse(stored);
2061
+ // Validate version
2062
+ if (parsed.version !== CONSENT_VERSION) {
2063
+ clearConsent();
2064
+ return null;
2065
+ }
2066
+ return parsed;
2067
+ }
2068
+ catch {
2069
+ return null;
2070
+ }
2071
+ }
2072
+ /**
2073
+ * Clear consent state from storage
2074
+ */
2075
+ function clearConsent() {
2076
+ try {
2077
+ if (typeof localStorage === 'undefined')
2078
+ return false;
2079
+ localStorage.removeItem(STORAGE_KEYS.CONSENT);
2080
+ return true;
2081
+ }
2082
+ catch {
2083
+ return false;
2084
+ }
2085
+ }
2086
+ /**
2087
+ * Check if consent has been explicitly set
2088
+ */
2089
+ function hasStoredConsent() {
2090
+ return loadConsent() !== null;
2091
+ }
2092
+
2093
+ /**
2094
+ * Clianta SDK - Consent Manager
2095
+ * Manages consent state and event buffering for GDPR/CCPA compliance
2096
+ * @see SDK_VERSION in core/config.ts
2097
+ */
2098
+ /** Maximum events to buffer while waiting for consent */
2099
+ const MAX_BUFFER_SIZE = 100;
2100
+ /**
2101
+ * Manages user consent state for tracking
2102
+ */
2103
+ class ConsentManager {
2104
+ constructor(config = {}) {
2105
+ this.eventBuffer = [];
2106
+ this.callbacks = [];
2107
+ this.hasExplicitConsent = false;
2108
+ this.config = {
2109
+ defaultConsent: { analytics: true, marketing: false, personalization: false },
2110
+ waitForConsent: false,
2111
+ storageKey: 'mb_consent',
2112
+ ...config,
2113
+ };
2114
+ // Load stored consent or use default
2115
+ const stored = loadConsent();
2116
+ if (stored) {
2117
+ this.state = stored.state;
2118
+ this.hasExplicitConsent = true;
2119
+ logger.debug('Loaded stored consent:', this.state);
2120
+ }
2121
+ else {
2122
+ this.state = this.config.defaultConsent || { analytics: true };
2123
+ this.hasExplicitConsent = false;
2124
+ logger.debug('Using default consent:', this.state);
2125
+ }
2126
+ // Register callback if provided
2127
+ if (config.onConsentChange) {
2128
+ this.callbacks.push(config.onConsentChange);
2129
+ }
2130
+ }
2131
+ /**
2132
+ * Grant consent for specified categories
2133
+ */
2134
+ grant(categories) {
2135
+ const previous = { ...this.state };
2136
+ this.state = { ...this.state, ...categories };
2137
+ this.hasExplicitConsent = true;
2138
+ saveConsent(this.state);
2139
+ logger.info('Consent granted:', categories);
2140
+ this.notifyChange(previous);
2141
+ }
2142
+ /**
2143
+ * Revoke consent for specified categories
2144
+ */
2145
+ revoke(categories) {
2146
+ const previous = { ...this.state };
2147
+ for (const category of categories) {
2148
+ this.state[category] = false;
2149
+ }
2150
+ this.hasExplicitConsent = true;
2151
+ saveConsent(this.state);
2152
+ logger.info('Consent revoked:', categories);
2153
+ this.notifyChange(previous);
2154
+ }
2155
+ /**
2156
+ * Update entire consent state
2157
+ */
2158
+ update(state) {
2159
+ const previous = { ...this.state };
2160
+ this.state = { ...state };
2161
+ this.hasExplicitConsent = true;
2162
+ saveConsent(this.state);
2163
+ logger.info('Consent updated:', this.state);
2164
+ this.notifyChange(previous);
2165
+ }
2166
+ /**
2167
+ * Reset consent to default (clear stored consent)
2168
+ */
2169
+ reset() {
2170
+ const previous = { ...this.state };
2171
+ this.state = this.config.defaultConsent || { analytics: true };
2172
+ this.hasExplicitConsent = false;
2173
+ this.eventBuffer = [];
2174
+ clearConsent();
2175
+ logger.info('Consent reset to defaults');
2176
+ this.notifyChange(previous);
2177
+ }
2178
+ /**
2179
+ * Get current consent state
2180
+ */
2181
+ getState() {
2182
+ return { ...this.state };
2183
+ }
2184
+ /**
2185
+ * Check if a specific consent category is granted
2186
+ */
2187
+ hasConsent(category) {
2188
+ return this.state[category] === true;
2189
+ }
2190
+ /**
2191
+ * Check if analytics consent is granted (most common check)
2192
+ */
2193
+ canTrack() {
2194
+ // If waiting for consent and no explicit consent given, cannot track
2195
+ if (this.config.waitForConsent && !this.hasExplicitConsent) {
2196
+ return false;
2197
+ }
2198
+ return this.state.analytics === true;
2199
+ }
2200
+ /**
2201
+ * Check if explicit consent has been given
2202
+ */
2203
+ hasExplicit() {
2204
+ return this.hasExplicitConsent;
2205
+ }
2206
+ /**
2207
+ * Check if there's stored consent
2208
+ */
2209
+ hasStored() {
2210
+ return hasStoredConsent();
2211
+ }
2212
+ /**
2213
+ * Buffer an event (for waitForConsent mode)
2214
+ */
2215
+ bufferEvent(event) {
2216
+ // Prevent unbounded buffer growth
2217
+ if (this.eventBuffer.length >= MAX_BUFFER_SIZE) {
2218
+ logger.warn('Consent event buffer full, dropping oldest event');
2219
+ this.eventBuffer.shift();
2220
+ }
2221
+ this.eventBuffer.push(event);
2222
+ logger.debug('Event buffered (waiting for consent):', event.eventName);
2223
+ }
2224
+ /**
2225
+ * Get and clear buffered events
2226
+ */
2227
+ flushBuffer() {
2228
+ const events = [...this.eventBuffer];
2229
+ this.eventBuffer = [];
2230
+ if (events.length > 0) {
2231
+ logger.debug(`Flushing ${events.length} buffered events`);
2232
+ }
2233
+ return events;
2234
+ }
2235
+ /**
2236
+ * Get buffered event count
2237
+ */
2238
+ getBufferSize() {
2239
+ return this.eventBuffer.length;
2240
+ }
2241
+ /**
2242
+ * Register a consent change callback
2243
+ */
2244
+ onChange(callback) {
2245
+ this.callbacks.push(callback);
2246
+ // Return unsubscribe function
2247
+ return () => {
2248
+ const index = this.callbacks.indexOf(callback);
2249
+ if (index > -1) {
2250
+ this.callbacks.splice(index, 1);
2251
+ }
2252
+ };
2253
+ }
2254
+ /**
2255
+ * Notify all callbacks of consent change
2256
+ */
2257
+ notifyChange(previous) {
2258
+ for (const callback of this.callbacks) {
2259
+ try {
2260
+ callback(this.state, previous);
2261
+ }
2262
+ catch (error) {
2263
+ logger.error('Consent change callback error:', error);
2264
+ }
2265
+ }
2266
+ }
2267
+ }
2268
+
2269
+ /**
2270
+ * Clianta SDK - Main Tracker Class
2271
+ * @see SDK_VERSION in core/config.ts
2272
+ */
2273
+ /**
2274
+ * Main Clianta Tracker Class
2275
+ */
2276
+ class Tracker {
2277
+ constructor(workspaceId, userConfig = {}) {
2278
+ this.plugins = [];
2279
+ this.isInitialized = false;
2280
+ if (!workspaceId) {
2281
+ throw new Error('[Clianta] Workspace ID is required');
2282
+ }
2283
+ this.workspaceId = workspaceId;
2284
+ this.config = mergeConfig(userConfig);
2285
+ // Setup debug mode
2286
+ logger.enabled = this.config.debug;
2287
+ logger.info(`Initializing SDK v${SDK_VERSION}`, { workspaceId });
2288
+ // Initialize consent manager
2289
+ this.consentManager = new ConsentManager({
2290
+ ...this.config.consent,
2291
+ onConsentChange: (state, previous) => {
2292
+ this.onConsentChange(state, previous);
2293
+ },
2294
+ });
2295
+ // Initialize transport and queue
2296
+ this.transport = new Transport({ apiEndpoint: this.config.apiEndpoint });
2297
+ this.queue = new EventQueue(this.transport, {
2298
+ batchSize: this.config.batchSize,
2299
+ flushInterval: this.config.flushInterval,
2300
+ });
2301
+ // Get or create visitor and session IDs based on mode
2302
+ this.visitorId = this.createVisitorId();
2303
+ this.sessionId = this.createSessionId();
2304
+ logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
2305
+ // Initialize plugins
2306
+ this.initPlugins();
2307
+ this.isInitialized = true;
2308
+ logger.info('SDK initialized successfully');
2309
+ }
2310
+ /**
2311
+ * Create visitor ID based on storage mode
2312
+ */
2313
+ createVisitorId() {
2314
+ // Anonymous mode: use temporary ID until consent
2315
+ if (this.config.consent.anonymousMode && !this.consentManager.hasExplicit()) {
2316
+ const key = STORAGE_KEYS.VISITOR_ID + '_anon';
2317
+ let anonId = getSessionStorage(key);
2318
+ if (!anonId) {
2319
+ anonId = 'anon_' + generateUUID();
2320
+ setSessionStorage(key, anonId);
2321
+ }
2322
+ return anonId;
2323
+ }
2324
+ // Cookie-less mode: use sessionStorage only
2325
+ if (this.config.cookielessMode) {
2326
+ let visitorId = getSessionStorage(STORAGE_KEYS.VISITOR_ID);
2327
+ if (!visitorId) {
2328
+ visitorId = generateUUID();
2329
+ setSessionStorage(STORAGE_KEYS.VISITOR_ID, visitorId);
2330
+ }
2331
+ return visitorId;
2332
+ }
2333
+ // Normal mode
2334
+ return getOrCreateVisitorId(this.config.useCookies);
2335
+ }
2336
+ /**
2337
+ * Create session ID
2338
+ */
2339
+ createSessionId() {
2340
+ return getOrCreateSessionId(this.config.sessionTimeout);
2341
+ }
2342
+ /**
2343
+ * Handle consent state changes
2344
+ */
2345
+ onConsentChange(state, previous) {
2346
+ logger.debug('Consent changed:', { from: previous, to: state });
2347
+ // If analytics consent was just granted
2348
+ if (state.analytics && !previous.analytics) {
2349
+ // Upgrade from anonymous ID to persistent ID
2350
+ if (this.config.consent.anonymousMode) {
2351
+ this.visitorId = getOrCreateVisitorId(this.config.useCookies);
2352
+ logger.info('Upgraded from anonymous to persistent visitor ID');
2353
+ }
2354
+ // Flush buffered events
2355
+ const buffered = this.consentManager.flushBuffer();
2356
+ for (const event of buffered) {
2357
+ // Update event with new visitor ID
2358
+ event.visitorId = this.visitorId;
2359
+ this.queue.push(event);
2360
+ }
2361
+ }
2362
+ }
2363
+ /**
2364
+ * Initialize enabled plugins
2365
+ * Handles both sync and async plugin init methods
2366
+ */
2367
+ initPlugins() {
2368
+ const pluginsToLoad = this.config.plugins;
2369
+ // Skip pageView plugin if autoPageView is disabled
2370
+ const filteredPlugins = this.config.autoPageView
2371
+ ? pluginsToLoad
2372
+ : pluginsToLoad.filter((p) => p !== 'pageView');
2373
+ for (const pluginName of filteredPlugins) {
2374
+ try {
2375
+ const plugin = getPlugin(pluginName);
2376
+ // Handle both sync and async init (fire-and-forget for async)
2377
+ const result = plugin.init(this);
2378
+ if (result instanceof Promise) {
2379
+ result.catch((error) => {
2380
+ logger.error(`Async plugin init failed: ${pluginName}`, error);
2381
+ });
2382
+ }
2383
+ this.plugins.push(plugin);
2384
+ logger.debug(`Plugin loaded: ${pluginName}`);
2385
+ }
2386
+ catch (error) {
2387
+ logger.error(`Failed to load plugin: ${pluginName}`, error);
2388
+ }
2389
+ }
2390
+ }
2391
+ /**
2392
+ * Track a custom event
2393
+ */
2394
+ track(eventType, eventName, properties = {}) {
2395
+ if (!this.isInitialized) {
2396
+ logger.warn('SDK not initialized, event dropped');
2397
+ return;
2398
+ }
2399
+ const event = {
2400
+ workspaceId: this.workspaceId,
2401
+ visitorId: this.visitorId,
2402
+ sessionId: this.sessionId,
2403
+ eventType: eventType,
2404
+ eventName,
2405
+ url: typeof window !== 'undefined' ? window.location.href : '',
2406
+ referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
2407
+ properties,
2408
+ device: getDeviceInfo(),
2409
+ utm: getUTMParams(),
2410
+ timestamp: new Date().toISOString(),
2411
+ sdkVersion: SDK_VERSION,
2412
+ };
2413
+ // Check consent before tracking
2414
+ if (!this.consentManager.canTrack()) {
2415
+ // Buffer event for later if waitForConsent is enabled
2416
+ if (this.config.consent.waitForConsent) {
2417
+ this.consentManager.bufferEvent(event);
2418
+ return;
2419
+ }
2420
+ // Otherwise drop the event
2421
+ logger.debug('Event dropped (no consent):', eventName);
2422
+ return;
2423
+ }
2424
+ this.queue.push(event);
2425
+ logger.debug('Event tracked:', eventName, properties);
2426
+ }
2427
+ /**
2428
+ * Track a page view
2429
+ */
2430
+ page(name, properties = {}) {
2431
+ const pageName = name || (typeof document !== 'undefined' ? document.title : 'Page View');
2432
+ this.track('page_view', pageName, {
2433
+ ...properties,
2434
+ path: typeof window !== 'undefined' ? window.location.pathname : '',
2435
+ });
2436
+ }
2437
+ /**
2438
+ * Identify a visitor
2439
+ */
2440
+ async identify(email, traits = {}) {
2441
+ if (!email) {
2442
+ logger.warn('Email is required for identification');
2443
+ return;
2444
+ }
2445
+ logger.info('Identifying visitor:', email);
2446
+ const result = await this.transport.sendIdentify({
2447
+ workspaceId: this.workspaceId,
2448
+ visitorId: this.visitorId,
2449
+ email,
2450
+ properties: traits,
2451
+ });
2452
+ if (result.success) {
2453
+ logger.info('Visitor identified successfully');
2454
+ }
2455
+ else {
2456
+ logger.error('Failed to identify visitor:', result.error);
2457
+ }
2458
+ }
2459
+ /**
2460
+ * Update consent state
2461
+ */
2462
+ consent(state) {
2463
+ this.consentManager.update(state);
2464
+ }
2465
+ /**
2466
+ * Get current consent state
2467
+ */
2468
+ getConsentState() {
2469
+ return this.consentManager.getState();
2470
+ }
2471
+ /**
2472
+ * Toggle debug mode
2473
+ */
2474
+ debug(enabled) {
2475
+ logger.enabled = enabled;
2476
+ logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
2477
+ }
2478
+ /**
2479
+ * Get visitor ID
2480
+ */
2481
+ getVisitorId() {
2482
+ return this.visitorId;
2483
+ }
2484
+ /**
2485
+ * Get session ID
2486
+ */
2487
+ getSessionId() {
2488
+ return this.sessionId;
2489
+ }
2490
+ /**
2491
+ * Get workspace ID
2492
+ */
2493
+ getWorkspaceId() {
2494
+ return this.workspaceId;
2495
+ }
2496
+ /**
2497
+ * Get current configuration
2498
+ */
2499
+ getConfig() {
2500
+ return { ...this.config };
2501
+ }
2502
+ /**
2503
+ * Force flush event queue
2504
+ */
2505
+ async flush() {
2506
+ await this.queue.flush();
2507
+ }
2508
+ /**
2509
+ * Reset visitor and session (for logout)
2510
+ */
2511
+ reset() {
2512
+ logger.info('Resetting visitor data');
2513
+ resetIds(this.config.useCookies);
2514
+ this.visitorId = this.createVisitorId();
2515
+ this.sessionId = this.createSessionId();
2516
+ this.queue.clear();
2517
+ }
2518
+ /**
2519
+ * Delete all stored user data (GDPR right-to-erasure)
2520
+ */
2521
+ deleteData() {
2522
+ logger.info('Deleting all user data (GDPR request)');
2523
+ // Clear queue
2524
+ this.queue.clear();
2525
+ // Reset consent
2526
+ this.consentManager.reset();
2527
+ // Clear all stored IDs
2528
+ resetIds(this.config.useCookies);
2529
+ // Clear session storage items
2530
+ if (typeof sessionStorage !== 'undefined') {
2531
+ try {
2532
+ sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
2533
+ sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID + '_anon');
2534
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_ID);
2535
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_TIMESTAMP);
2536
+ }
2537
+ catch {
2538
+ // Ignore errors
2539
+ }
2540
+ }
2541
+ // Clear localStorage items
2542
+ if (typeof localStorage !== 'undefined') {
2543
+ try {
2544
+ localStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
2545
+ localStorage.removeItem(STORAGE_KEYS.CONSENT);
2546
+ localStorage.removeItem(STORAGE_KEYS.EVENT_QUEUE);
2547
+ }
2548
+ catch {
2549
+ // Ignore errors
2550
+ }
2551
+ }
2552
+ // Generate new IDs
2553
+ this.visitorId = this.createVisitorId();
2554
+ this.sessionId = this.createSessionId();
2555
+ logger.info('All user data deleted');
2556
+ }
2557
+ /**
2558
+ * Destroy tracker and cleanup
2559
+ */
2560
+ async destroy() {
2561
+ logger.info('Destroying tracker');
2562
+ // Flush any remaining events (await to ensure completion)
2563
+ await this.queue.flush();
2564
+ // Destroy plugins
2565
+ for (const plugin of this.plugins) {
2566
+ if (plugin.destroy) {
2567
+ plugin.destroy();
2568
+ }
2569
+ }
2570
+ this.plugins = [];
2571
+ // Destroy queue
2572
+ this.queue.destroy();
2573
+ this.isInitialized = false;
2574
+ }
2575
+ }
2576
+
2577
+ /**
2578
+ * Clianta SDK - CRM API Client
2579
+ * @see SDK_VERSION in core/config.ts
2580
+ */
2581
+ /**
2582
+ * CRM API Client for managing contacts and opportunities
2583
+ */
2584
+ class CRMClient {
2585
+ constructor(apiEndpoint, workspaceId, authToken) {
2586
+ this.apiEndpoint = apiEndpoint;
2587
+ this.workspaceId = workspaceId;
2588
+ this.authToken = authToken;
2589
+ }
2590
+ /**
2591
+ * Set authentication token for API requests
2592
+ */
2593
+ setAuthToken(token) {
2594
+ this.authToken = token;
2595
+ }
2596
+ /**
2597
+ * Make authenticated API request
2598
+ */
2599
+ async request(endpoint, options = {}) {
2600
+ const url = `${this.apiEndpoint}${endpoint}`;
2601
+ const headers = {
2602
+ 'Content-Type': 'application/json',
2603
+ ...(options.headers || {}),
2604
+ };
2605
+ if (this.authToken) {
2606
+ headers['Authorization'] = `Bearer ${this.authToken}`;
2607
+ }
2608
+ try {
2609
+ const response = await fetch(url, {
2610
+ ...options,
2611
+ headers,
2612
+ });
2613
+ const data = await response.json();
2614
+ if (!response.ok) {
2615
+ return {
2616
+ success: false,
2617
+ error: data.message || 'Request failed',
2618
+ status: response.status,
2619
+ };
2620
+ }
2621
+ return {
2622
+ success: true,
2623
+ data: data.data || data,
2624
+ status: response.status,
2625
+ };
2626
+ }
2627
+ catch (error) {
2628
+ return {
2629
+ success: false,
2630
+ error: error instanceof Error ? error.message : 'Network error',
2631
+ status: 0,
2632
+ };
2633
+ }
2634
+ }
2635
+ // ============================================
2636
+ // CONTACTS API
2637
+ // ============================================
2638
+ /**
2639
+ * Get all contacts with pagination
2640
+ */
2641
+ async getContacts(params) {
2642
+ const queryParams = new URLSearchParams();
2643
+ if (params?.page)
2644
+ queryParams.set('page', params.page.toString());
2645
+ if (params?.limit)
2646
+ queryParams.set('limit', params.limit.toString());
2647
+ if (params?.search)
2648
+ queryParams.set('search', params.search);
2649
+ if (params?.status)
2650
+ queryParams.set('status', params.status);
2651
+ const query = queryParams.toString();
2652
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts${query ? `?${query}` : ''}`;
2653
+ return this.request(endpoint);
2654
+ }
2655
+ /**
2656
+ * Get a single contact by ID
2657
+ */
2658
+ async getContact(contactId) {
2659
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
2660
+ }
2661
+ /**
2662
+ * Create a new contact
2663
+ */
2664
+ async createContact(contact) {
2665
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts`, {
2666
+ method: 'POST',
2667
+ body: JSON.stringify(contact),
2668
+ });
2669
+ }
2670
+ /**
2671
+ * Update an existing contact
2672
+ */
2673
+ async updateContact(contactId, updates) {
2674
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2675
+ method: 'PUT',
2676
+ body: JSON.stringify(updates),
2677
+ });
2678
+ }
2679
+ /**
2680
+ * Delete a contact
2681
+ */
2682
+ async deleteContact(contactId) {
2683
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2684
+ method: 'DELETE',
2685
+ });
2686
+ }
2687
+ // ============================================
2688
+ // OPPORTUNITIES API
2689
+ // ============================================
2690
+ /**
2691
+ * Get all opportunities with pagination
2692
+ */
2693
+ async getOpportunities(params) {
2694
+ const queryParams = new URLSearchParams();
2695
+ if (params?.page)
2696
+ queryParams.set('page', params.page.toString());
2697
+ if (params?.limit)
2698
+ queryParams.set('limit', params.limit.toString());
2699
+ if (params?.pipelineId)
2700
+ queryParams.set('pipelineId', params.pipelineId);
2701
+ if (params?.stageId)
2702
+ queryParams.set('stageId', params.stageId);
2703
+ const query = queryParams.toString();
2704
+ const endpoint = `/api/workspaces/${this.workspaceId}/opportunities${query ? `?${query}` : ''}`;
2705
+ return this.request(endpoint);
2706
+ }
2707
+ /**
2708
+ * Get a single opportunity by ID
2709
+ */
2710
+ async getOpportunity(opportunityId) {
2711
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`);
2712
+ }
2713
+ /**
2714
+ * Create a new opportunity
2715
+ */
2716
+ async createOpportunity(opportunity) {
2717
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities`, {
2718
+ method: 'POST',
2719
+ body: JSON.stringify(opportunity),
2720
+ });
2721
+ }
2722
+ /**
2723
+ * Update an existing opportunity
2724
+ */
2725
+ async updateOpportunity(opportunityId, updates) {
2726
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
2727
+ method: 'PUT',
2728
+ body: JSON.stringify(updates),
2729
+ });
2730
+ }
2731
+ /**
2732
+ * Delete an opportunity
2733
+ */
2734
+ async deleteOpportunity(opportunityId) {
2735
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
2736
+ method: 'DELETE',
2737
+ });
2738
+ }
2739
+ /**
2740
+ * Move opportunity to a different stage
2741
+ */
2742
+ async moveOpportunity(opportunityId, stageId) {
2743
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/move`, {
2744
+ method: 'POST',
2745
+ body: JSON.stringify({ stageId }),
2746
+ });
2747
+ }
2748
+ // ============================================
2749
+ // COMPANIES API
2750
+ // ============================================
2751
+ /**
2752
+ * Get all companies with pagination
2753
+ */
2754
+ async getCompanies(params) {
2755
+ const queryParams = new URLSearchParams();
2756
+ if (params?.page)
2757
+ queryParams.set('page', params.page.toString());
2758
+ if (params?.limit)
2759
+ queryParams.set('limit', params.limit.toString());
2760
+ if (params?.search)
2761
+ queryParams.set('search', params.search);
2762
+ if (params?.status)
2763
+ queryParams.set('status', params.status);
2764
+ if (params?.industry)
2765
+ queryParams.set('industry', params.industry);
2766
+ const query = queryParams.toString();
2767
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies${query ? `?${query}` : ''}`;
2768
+ return this.request(endpoint);
2769
+ }
2770
+ /**
2771
+ * Get a single company by ID
2772
+ */
2773
+ async getCompany(companyId) {
2774
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`);
2775
+ }
2776
+ /**
2777
+ * Create a new company
2778
+ */
2779
+ async createCompany(company) {
2780
+ return this.request(`/api/workspaces/${this.workspaceId}/companies`, {
2781
+ method: 'POST',
2782
+ body: JSON.stringify(company),
2783
+ });
2784
+ }
2785
+ /**
2786
+ * Update an existing company
2787
+ */
2788
+ async updateCompany(companyId, updates) {
2789
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
2790
+ method: 'PUT',
2791
+ body: JSON.stringify(updates),
2792
+ });
2793
+ }
2794
+ /**
2795
+ * Delete a company
2796
+ */
2797
+ async deleteCompany(companyId) {
2798
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
2799
+ method: 'DELETE',
2800
+ });
2801
+ }
2802
+ /**
2803
+ * Get contacts belonging to a company
2804
+ */
2805
+ async getCompanyContacts(companyId, params) {
2806
+ const queryParams = new URLSearchParams();
2807
+ if (params?.page)
2808
+ queryParams.set('page', params.page.toString());
2809
+ if (params?.limit)
2810
+ queryParams.set('limit', params.limit.toString());
2811
+ const query = queryParams.toString();
2812
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/contacts${query ? `?${query}` : ''}`;
2813
+ return this.request(endpoint);
2814
+ }
2815
+ /**
2816
+ * Get deals/opportunities belonging to a company
2817
+ */
2818
+ async getCompanyDeals(companyId, params) {
2819
+ const queryParams = new URLSearchParams();
2820
+ if (params?.page)
2821
+ queryParams.set('page', params.page.toString());
2822
+ if (params?.limit)
2823
+ queryParams.set('limit', params.limit.toString());
2824
+ const query = queryParams.toString();
2825
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/deals${query ? `?${query}` : ''}`;
2826
+ return this.request(endpoint);
2827
+ }
2828
+ // ============================================
2829
+ // PIPELINES API
2830
+ // ============================================
2831
+ /**
2832
+ * Get all pipelines
2833
+ */
2834
+ async getPipelines() {
2835
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines`);
2836
+ }
2837
+ /**
2838
+ * Get a single pipeline by ID
2839
+ */
2840
+ async getPipeline(pipelineId) {
2841
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`);
2842
+ }
2843
+ /**
2844
+ * Create a new pipeline
2845
+ */
2846
+ async createPipeline(pipeline) {
2847
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines`, {
2848
+ method: 'POST',
2849
+ body: JSON.stringify(pipeline),
2850
+ });
2851
+ }
2852
+ /**
2853
+ * Update an existing pipeline
2854
+ */
2855
+ async updatePipeline(pipelineId, updates) {
2856
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
2857
+ method: 'PUT',
2858
+ body: JSON.stringify(updates),
2859
+ });
2860
+ }
2861
+ /**
2862
+ * Delete a pipeline
2863
+ */
2864
+ async deletePipeline(pipelineId) {
2865
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
2866
+ method: 'DELETE',
2867
+ });
2868
+ }
2869
+ // ============================================
2870
+ // TASKS API
2871
+ // ============================================
2872
+ /**
2873
+ * Get all tasks with pagination
2874
+ */
2875
+ async getTasks(params) {
2876
+ const queryParams = new URLSearchParams();
2877
+ if (params?.page)
2878
+ queryParams.set('page', params.page.toString());
2879
+ if (params?.limit)
2880
+ queryParams.set('limit', params.limit.toString());
2881
+ if (params?.status)
2882
+ queryParams.set('status', params.status);
2883
+ if (params?.priority)
2884
+ queryParams.set('priority', params.priority);
2885
+ if (params?.contactId)
2886
+ queryParams.set('contactId', params.contactId);
2887
+ if (params?.companyId)
2888
+ queryParams.set('companyId', params.companyId);
2889
+ if (params?.opportunityId)
2890
+ queryParams.set('opportunityId', params.opportunityId);
2891
+ const query = queryParams.toString();
2892
+ const endpoint = `/api/workspaces/${this.workspaceId}/tasks${query ? `?${query}` : ''}`;
2893
+ return this.request(endpoint);
2894
+ }
2895
+ /**
2896
+ * Get a single task by ID
2897
+ */
2898
+ async getTask(taskId) {
2899
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`);
2900
+ }
2901
+ /**
2902
+ * Create a new task
2903
+ */
2904
+ async createTask(task) {
2905
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
2906
+ method: 'POST',
2907
+ body: JSON.stringify(task),
2908
+ });
2909
+ }
2910
+ /**
2911
+ * Update an existing task
2912
+ */
2913
+ async updateTask(taskId, updates) {
2914
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
2915
+ method: 'PUT',
2916
+ body: JSON.stringify(updates),
2917
+ });
2918
+ }
2919
+ /**
2920
+ * Mark a task as completed
2921
+ */
2922
+ async completeTask(taskId) {
2923
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}/complete`, {
2924
+ method: 'PATCH',
2925
+ });
2926
+ }
2927
+ /**
2928
+ * Delete a task
2929
+ */
2930
+ async deleteTask(taskId) {
2931
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
2932
+ method: 'DELETE',
2933
+ });
2934
+ }
2935
+ // ============================================
2936
+ // ACTIVITIES API
2937
+ // ============================================
2938
+ /**
2939
+ * Get activities for a contact
2940
+ */
2941
+ async getContactActivities(contactId, params) {
2942
+ const queryParams = new URLSearchParams();
2943
+ if (params?.page)
2944
+ queryParams.set('page', params.page.toString());
2945
+ if (params?.limit)
2946
+ queryParams.set('limit', params.limit.toString());
2947
+ if (params?.type)
2948
+ queryParams.set('type', params.type);
2949
+ const query = queryParams.toString();
2950
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
2951
+ return this.request(endpoint);
2952
+ }
2953
+ /**
2954
+ * Get activities for an opportunity/deal
2955
+ */
2956
+ async getOpportunityActivities(opportunityId, params) {
2957
+ const queryParams = new URLSearchParams();
2958
+ if (params?.page)
2959
+ queryParams.set('page', params.page.toString());
2960
+ if (params?.limit)
2961
+ queryParams.set('limit', params.limit.toString());
2962
+ if (params?.type)
2963
+ queryParams.set('type', params.type);
2964
+ const query = queryParams.toString();
2965
+ const endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/activities${query ? `?${query}` : ''}`;
2966
+ return this.request(endpoint);
2967
+ }
2968
+ /**
2969
+ * Create a new activity
2970
+ */
2971
+ async createActivity(activity) {
2972
+ // Determine the correct endpoint based on related entity
2973
+ let endpoint;
2974
+ if (activity.opportunityId) {
2975
+ endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${activity.opportunityId}/activities`;
2976
+ }
2977
+ else if (activity.contactId) {
2978
+ endpoint = `/api/workspaces/${this.workspaceId}/contacts/${activity.contactId}/activities`;
2979
+ }
2980
+ else {
2981
+ endpoint = `/api/workspaces/${this.workspaceId}/activities`;
2982
+ }
2983
+ return this.request(endpoint, {
2984
+ method: 'POST',
2985
+ body: JSON.stringify(activity),
2986
+ });
2987
+ }
2988
+ /**
2989
+ * Update an existing activity
2990
+ */
2991
+ async updateActivity(activityId, updates) {
2992
+ return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
2993
+ method: 'PATCH',
2994
+ body: JSON.stringify(updates),
2995
+ });
2996
+ }
2997
+ /**
2998
+ * Delete an activity
2999
+ */
3000
+ async deleteActivity(activityId) {
3001
+ return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3002
+ method: 'DELETE',
3003
+ });
3004
+ }
3005
+ /**
3006
+ * Log a call activity
3007
+ */
3008
+ async logCall(data) {
3009
+ return this.createActivity({
3010
+ type: 'call',
3011
+ title: `${data.direction === 'inbound' ? 'Inbound' : 'Outbound'} Call`,
3012
+ direction: data.direction,
3013
+ duration: data.duration,
3014
+ outcome: data.outcome,
3015
+ description: data.notes,
3016
+ contactId: data.contactId,
3017
+ opportunityId: data.opportunityId,
3018
+ });
3019
+ }
3020
+ /**
3021
+ * Log a meeting activity
3022
+ */
3023
+ async logMeeting(data) {
3024
+ return this.createActivity({
3025
+ type: 'meeting',
3026
+ title: data.title,
3027
+ duration: data.duration,
3028
+ outcome: data.outcome,
3029
+ description: data.notes,
3030
+ contactId: data.contactId,
3031
+ opportunityId: data.opportunityId,
3032
+ });
3033
+ }
3034
+ /**
3035
+ * Add a note to a contact or opportunity
3036
+ */
3037
+ async addNote(data) {
3038
+ return this.createActivity({
3039
+ type: 'note',
3040
+ title: 'Note',
3041
+ description: data.content,
3042
+ contactId: data.contactId,
3043
+ opportunityId: data.opportunityId,
3044
+ });
3045
+ }
3046
+ }
3047
+
3048
+ /**
3049
+ * Clianta SDK
3050
+ * Professional CRM and tracking SDK for lead generation
3051
+ * @see SDK_VERSION in core/config.ts
3052
+ */
3053
+ // Global instance cache
3054
+ let globalInstance = null;
3055
+ /**
3056
+ * Initialize or get the Clianta tracker instance
3057
+ *
3058
+ * @example
3059
+ * // Simple initialization
3060
+ * const tracker = clianta('your-workspace-id');
3061
+ *
3062
+ * @example
3063
+ * // With configuration
3064
+ * const tracker = clianta('your-workspace-id', {
3065
+ * debug: true,
3066
+ * plugins: ['pageView', 'forms', 'scroll'],
3067
+ * });
3068
+ *
3069
+ * @example
3070
+ * // With consent configuration
3071
+ * const tracker = clianta('your-workspace-id', {
3072
+ * consent: {
3073
+ * waitForConsent: true,
3074
+ * anonymousMode: true,
3075
+ * },
3076
+ * cookielessMode: true, // GDPR-friendly mode
3077
+ * });
3078
+ */
3079
+ function clianta(workspaceId, config) {
3080
+ // Return existing instance if same workspace
3081
+ if (globalInstance && globalInstance.getWorkspaceId() === workspaceId) {
3082
+ return globalInstance;
3083
+ }
3084
+ // Destroy existing instance if workspace changed
3085
+ if (globalInstance) {
3086
+ globalInstance.destroy();
3087
+ }
3088
+ // Create new instance
3089
+ globalInstance = new Tracker(workspaceId, config);
3090
+ return globalInstance;
3091
+ }
3092
+ // Attach to window for <script> usage
3093
+ if (typeof window !== 'undefined') {
3094
+ window.clianta = clianta;
3095
+ window.Clianta = {
3096
+ clianta,
3097
+ Tracker,
3098
+ CRMClient,
3099
+ ConsentManager,
3100
+ };
3101
+ }
3102
+
3103
+ /**
3104
+ * Clianta SDK - React Integration
3105
+ *
3106
+ * Provides CliantaProvider component for easy React/Next.js integration
3107
+ * using the clianta.config.ts pattern.
3108
+ */
3109
+ // Context for accessing tracker throughout the app
3110
+ const CliantaContext = createContext(null);
3111
+ /**
3112
+ * CliantaProvider - Wrap your app to enable tracking
3113
+ *
3114
+ * @example
3115
+ * // In clianta.config.ts:
3116
+ * import { CliantaConfig } from '@clianta/sdk';
3117
+ *
3118
+ * const config: CliantaConfig = {
3119
+ * projectId: 'your-project-id',
3120
+ * apiEndpoint: 'https://api.clianta.online',
3121
+ * debug: process.env.NODE_ENV === 'development',
3122
+ * };
3123
+ *
3124
+ * export default config;
3125
+ *
3126
+ * // In app/layout.tsx or main.tsx:
3127
+ * import { CliantaProvider } from '@clianta/sdk/react';
3128
+ * import cliantaConfig from '../clianta.config';
3129
+ *
3130
+ * <CliantaProvider config={cliantaConfig}>
3131
+ * {children}
3132
+ * </CliantaProvider>
3133
+ */
3134
+ function CliantaProvider({ config, children }) {
3135
+ const trackerRef = useRef(null);
3136
+ useEffect(() => {
3137
+ // Initialize tracker with config
3138
+ const projectId = config.projectId;
3139
+ if (!projectId) {
3140
+ console.error('[Clianta] Missing projectId in config. Please add projectId to your clianta.config.ts');
3141
+ return;
3142
+ }
3143
+ // Extract projectId (handled separately) and pass rest as options
3144
+ const { projectId: _, ...options } = config;
3145
+ trackerRef.current = clianta(projectId, options);
3146
+ // Cleanup: flush pending events on unmount
3147
+ return () => {
3148
+ trackerRef.current?.flush();
3149
+ };
3150
+ }, [config]);
3151
+ return (jsx(CliantaContext.Provider, { value: trackerRef.current, children: children }));
3152
+ }
3153
+ /**
3154
+ * useClianta - Hook to access tracker in any component
3155
+ *
3156
+ * @example
3157
+ * const tracker = useClianta();
3158
+ * tracker?.track('button_click', 'CTA Button');
3159
+ */
3160
+ function useClianta() {
3161
+ return useContext(CliantaContext);
3162
+ }
3163
+ /**
3164
+ * useCliantaTrack - Convenience hook for tracking events
3165
+ *
3166
+ * @example
3167
+ * const track = useCliantaTrack();
3168
+ * track('purchase', 'Order Completed', { orderId: '123' });
3169
+ */
3170
+ function useCliantaTrack() {
3171
+ const tracker = useClianta();
3172
+ return (eventType, eventName, properties) => {
3173
+ tracker?.track(eventType, eventName, properties);
3174
+ };
3175
+ }
3176
+
3177
+ export { CliantaProvider, useClianta, useCliantaTrack };
3178
+ //# sourceMappingURL=react.esm.js.map