@clianta/sdk 1.2.0 → 1.4.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,4028 @@
1
+ /*!
2
+ * Clianta SDK v1.4.0
3
+ * (c) 2026 Clianta
4
+ * Released under the MIT License.
5
+ */
6
+ 'use strict';
7
+
8
+ var vue = require('vue');
9
+
10
+ /**
11
+ * Clianta SDK - Configuration
12
+ * @see SDK_VERSION in core/config.ts
13
+ */
14
+ /** SDK Version */
15
+ const SDK_VERSION = '1.4.0';
16
+ /** Default API endpoint based on environment */
17
+ const getDefaultApiEndpoint = () => {
18
+ if (typeof window === 'undefined')
19
+ return 'https://api.clianta.online';
20
+ const hostname = window.location.hostname;
21
+ if (hostname.includes('localhost') || hostname.includes('127.0.0.1')) {
22
+ return 'http://localhost:5000';
23
+ }
24
+ return 'https://api.clianta.online';
25
+ };
26
+ /** Core plugins enabled by default */
27
+ const DEFAULT_PLUGINS = [
28
+ 'pageView',
29
+ 'forms',
30
+ 'scroll',
31
+ 'clicks',
32
+ 'engagement',
33
+ 'downloads',
34
+ 'exitIntent',
35
+ ];
36
+ /** Default configuration values */
37
+ const DEFAULT_CONFIG = {
38
+ projectId: '',
39
+ apiEndpoint: getDefaultApiEndpoint(),
40
+ authToken: '',
41
+ apiKey: '',
42
+ debug: false,
43
+ autoPageView: true,
44
+ plugins: DEFAULT_PLUGINS,
45
+ sessionTimeout: 30 * 60 * 1000, // 30 minutes
46
+ batchSize: 10,
47
+ flushInterval: 5000, // 5 seconds
48
+ consent: {
49
+ defaultConsent: { analytics: true, marketing: false, personalization: false },
50
+ waitForConsent: false,
51
+ storageKey: 'mb_consent',
52
+ anonymousMode: false,
53
+ },
54
+ cookieDomain: '',
55
+ useCookies: false,
56
+ cookielessMode: false,
57
+ };
58
+ /** Storage keys */
59
+ const STORAGE_KEYS = {
60
+ VISITOR_ID: 'mb_vid',
61
+ SESSION_ID: 'mb_sid',
62
+ SESSION_TIMESTAMP: 'mb_st',
63
+ CONSENT: 'mb_consent',
64
+ EVENT_QUEUE: 'mb_queue',
65
+ };
66
+ /** Scroll depth milestones to track */
67
+ const SCROLL_MILESTONES = [25, 50, 75, 100];
68
+ /** File extensions to track as downloads */
69
+ const DOWNLOAD_EXTENSIONS = [
70
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
71
+ '.zip', '.rar', '.tar', '.gz', '.7z',
72
+ '.csv', '.txt', '.json', '.xml',
73
+ '.mp3', '.mp4', '.wav', '.avi', '.mov',
74
+ ];
75
+ /**
76
+ * Merge user config with defaults
77
+ */
78
+ function mergeConfig(userConfig = {}) {
79
+ return {
80
+ ...DEFAULT_CONFIG,
81
+ ...userConfig,
82
+ consent: {
83
+ ...DEFAULT_CONFIG.consent,
84
+ ...userConfig.consent,
85
+ },
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Clianta SDK - Debug Logger
91
+ * @see SDK_VERSION in core/config.ts
92
+ */
93
+ const LOG_PREFIX = '[Clianta]';
94
+ const LOG_STYLES = {
95
+ debug: 'color: #6b7280; font-weight: normal;',
96
+ info: 'color: #3b82f6; font-weight: normal;',
97
+ warn: 'color: #f59e0b; font-weight: bold;',
98
+ error: 'color: #ef4444; font-weight: bold;',
99
+ };
100
+ const LOG_LEVELS = {
101
+ debug: 0,
102
+ info: 1,
103
+ warn: 2,
104
+ error: 3,
105
+ };
106
+ /**
107
+ * Create a logger instance
108
+ */
109
+ function createLogger(enabled = false) {
110
+ let currentLevel = 'debug';
111
+ let isEnabled = enabled;
112
+ const shouldLog = (level) => {
113
+ if (!isEnabled)
114
+ return false;
115
+ return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
116
+ };
117
+ const formatArgs = (level, args) => {
118
+ if (typeof console !== 'undefined' && typeof window !== 'undefined') {
119
+ // Browser with styled console
120
+ return [`%c${LOG_PREFIX}`, LOG_STYLES[level], ...args];
121
+ }
122
+ // Node.js or basic console
123
+ return [`${LOG_PREFIX} [${level.toUpperCase()}]`, ...args];
124
+ };
125
+ return {
126
+ get enabled() {
127
+ return isEnabled;
128
+ },
129
+ set enabled(value) {
130
+ isEnabled = value;
131
+ },
132
+ debug(...args) {
133
+ if (shouldLog('debug') && typeof console !== 'undefined') {
134
+ console.log(...formatArgs('debug', args));
135
+ }
136
+ },
137
+ info(...args) {
138
+ if (shouldLog('info') && typeof console !== 'undefined') {
139
+ console.info(...formatArgs('info', args));
140
+ }
141
+ },
142
+ warn(...args) {
143
+ if (shouldLog('warn') && typeof console !== 'undefined') {
144
+ console.warn(...formatArgs('warn', args));
145
+ }
146
+ },
147
+ error(...args) {
148
+ if (shouldLog('error') && typeof console !== 'undefined') {
149
+ console.error(...formatArgs('error', args));
150
+ }
151
+ },
152
+ setLevel(level) {
153
+ currentLevel = level;
154
+ },
155
+ };
156
+ }
157
+ /** Global logger instance */
158
+ const logger = createLogger(false);
159
+
160
+ /**
161
+ * Clianta SDK - Transport Layer
162
+ * Handles sending events to the backend with retry logic
163
+ * @see SDK_VERSION in core/config.ts
164
+ */
165
+ const DEFAULT_TIMEOUT = 10000; // 10 seconds
166
+ const DEFAULT_MAX_RETRIES = 3;
167
+ const DEFAULT_RETRY_DELAY = 1000; // 1 second
168
+ /**
169
+ * Transport class for sending data to the backend
170
+ */
171
+ class Transport {
172
+ constructor(config) {
173
+ this.config = {
174
+ apiEndpoint: config.apiEndpoint,
175
+ maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
176
+ retryDelay: config.retryDelay ?? DEFAULT_RETRY_DELAY,
177
+ timeout: config.timeout ?? DEFAULT_TIMEOUT,
178
+ };
179
+ }
180
+ /**
181
+ * Send events to the tracking endpoint
182
+ */
183
+ async sendEvents(events) {
184
+ const url = `${this.config.apiEndpoint}/api/public/track/event`;
185
+ const payload = JSON.stringify({ events });
186
+ return this.send(url, payload);
187
+ }
188
+ /**
189
+ * Send identify request.
190
+ * Returns contactId from the server response so the Tracker can store it.
191
+ */
192
+ async sendIdentify(data) {
193
+ const url = `${this.config.apiEndpoint}/api/public/track/identify`;
194
+ try {
195
+ const response = await this.fetchWithTimeout(url, {
196
+ method: 'POST',
197
+ headers: { 'Content-Type': 'application/json' },
198
+ body: JSON.stringify(data),
199
+ keepalive: true,
200
+ });
201
+ const body = await response.json().catch(() => ({}));
202
+ if (response.ok) {
203
+ logger.debug('Identify successful, contactId:', body.contactId);
204
+ return {
205
+ success: true,
206
+ status: response.status,
207
+ contactId: body.contactId ?? undefined,
208
+ };
209
+ }
210
+ if (response.status >= 500) {
211
+ logger.warn(`Identify server error (${response.status})`);
212
+ }
213
+ else {
214
+ logger.error(`Identify failed with status ${response.status}:`, body.message);
215
+ }
216
+ return { success: false, status: response.status };
217
+ }
218
+ catch (error) {
219
+ logger.error('Identify request failed:', error);
220
+ return { success: false, error: error };
221
+ }
222
+ }
223
+ /**
224
+ * Send events synchronously (for page unload)
225
+ * Uses navigator.sendBeacon for reliability
226
+ */
227
+ sendBeacon(events) {
228
+ if (typeof navigator === 'undefined' || !navigator.sendBeacon) {
229
+ logger.warn('sendBeacon not available, events may be lost');
230
+ return false;
231
+ }
232
+ const url = `${this.config.apiEndpoint}/api/public/track/event`;
233
+ const payload = JSON.stringify({ events });
234
+ const blob = new Blob([payload], { type: 'application/json' });
235
+ try {
236
+ const success = navigator.sendBeacon(url, blob);
237
+ if (success) {
238
+ logger.debug(`Beacon sent ${events.length} events`);
239
+ }
240
+ else {
241
+ logger.warn('sendBeacon returned false');
242
+ }
243
+ return success;
244
+ }
245
+ catch (error) {
246
+ logger.error('sendBeacon error:', error);
247
+ return false;
248
+ }
249
+ }
250
+ /**
251
+ * Internal send with retry logic
252
+ */
253
+ async send(url, payload, attempt = 1) {
254
+ try {
255
+ const response = await this.fetchWithTimeout(url, {
256
+ method: 'POST',
257
+ headers: {
258
+ 'Content-Type': 'application/json',
259
+ },
260
+ body: payload,
261
+ keepalive: true,
262
+ });
263
+ if (response.ok) {
264
+ logger.debug('Request successful:', url);
265
+ return { success: true, status: response.status };
266
+ }
267
+ // Server error - may retry
268
+ if (response.status >= 500 && attempt < this.config.maxRetries) {
269
+ logger.warn(`Server error (${response.status}), retrying...`);
270
+ await this.delay(this.config.retryDelay * attempt);
271
+ return this.send(url, payload, attempt + 1);
272
+ }
273
+ // Client error - don't retry
274
+ logger.error(`Request failed with status ${response.status}`);
275
+ return { success: false, status: response.status };
276
+ }
277
+ catch (error) {
278
+ // Network error - retry if possible
279
+ if (attempt < this.config.maxRetries) {
280
+ logger.warn(`Network error, retrying (${attempt}/${this.config.maxRetries})...`);
281
+ await this.delay(this.config.retryDelay * attempt);
282
+ return this.send(url, payload, attempt + 1);
283
+ }
284
+ logger.error('Request failed after retries:', error);
285
+ return { success: false, error: error };
286
+ }
287
+ }
288
+ /**
289
+ * Fetch with timeout
290
+ */
291
+ async fetchWithTimeout(url, options) {
292
+ const controller = new AbortController();
293
+ const timeout = setTimeout(() => controller.abort(), this.config.timeout);
294
+ try {
295
+ const response = await fetch(url, {
296
+ ...options,
297
+ signal: controller.signal,
298
+ });
299
+ return response;
300
+ }
301
+ finally {
302
+ clearTimeout(timeout);
303
+ }
304
+ }
305
+ /**
306
+ * Delay helper
307
+ */
308
+ delay(ms) {
309
+ return new Promise((resolve) => setTimeout(resolve, ms));
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Clianta SDK - Utility Functions
315
+ * @see SDK_VERSION in core/config.ts
316
+ */
317
+ // ============================================
318
+ // UUID GENERATION
319
+ // ============================================
320
+ /**
321
+ * Generate a UUID v4
322
+ */
323
+ function generateUUID() {
324
+ // Use crypto.randomUUID if available (modern browsers)
325
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
326
+ return crypto.randomUUID();
327
+ }
328
+ // Fallback to manual generation
329
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
330
+ const r = (Math.random() * 16) | 0;
331
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
332
+ return v.toString(16);
333
+ });
334
+ }
335
+ // ============================================
336
+ // STORAGE UTILITIES
337
+ // ============================================
338
+ /**
339
+ * Safely get from localStorage
340
+ */
341
+ function getLocalStorage(key) {
342
+ try {
343
+ if (typeof localStorage !== 'undefined') {
344
+ return localStorage.getItem(key);
345
+ }
346
+ }
347
+ catch {
348
+ // localStorage not available or blocked
349
+ }
350
+ return null;
351
+ }
352
+ /**
353
+ * Safely set to localStorage
354
+ */
355
+ function setLocalStorage(key, value) {
356
+ try {
357
+ if (typeof localStorage !== 'undefined') {
358
+ localStorage.setItem(key, value);
359
+ return true;
360
+ }
361
+ }
362
+ catch {
363
+ // localStorage not available or blocked
364
+ }
365
+ return false;
366
+ }
367
+ /**
368
+ * Safely get from sessionStorage
369
+ */
370
+ function getSessionStorage(key) {
371
+ try {
372
+ if (typeof sessionStorage !== 'undefined') {
373
+ return sessionStorage.getItem(key);
374
+ }
375
+ }
376
+ catch {
377
+ // sessionStorage not available or blocked
378
+ }
379
+ return null;
380
+ }
381
+ /**
382
+ * Safely set to sessionStorage
383
+ */
384
+ function setSessionStorage(key, value) {
385
+ try {
386
+ if (typeof sessionStorage !== 'undefined') {
387
+ sessionStorage.setItem(key, value);
388
+ return true;
389
+ }
390
+ }
391
+ catch {
392
+ // sessionStorage not available or blocked
393
+ }
394
+ return false;
395
+ }
396
+ /**
397
+ * Get or set a cookie
398
+ */
399
+ function cookie(name, value, days) {
400
+ if (typeof document === 'undefined')
401
+ return null;
402
+ // Get cookie
403
+ if (value === undefined) {
404
+ const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
405
+ return match ? match[2] : null;
406
+ }
407
+ // Set cookie
408
+ let expires = '';
409
+ if (days) {
410
+ const date = new Date();
411
+ date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
412
+ expires = '; expires=' + date.toUTCString();
413
+ }
414
+ document.cookie = name + '=' + value + expires + '; path=/; SameSite=Lax';
415
+ return value;
416
+ }
417
+ // ============================================
418
+ // VISITOR & SESSION MANAGEMENT
419
+ // ============================================
420
+ /**
421
+ * Get or create a persistent visitor ID
422
+ */
423
+ function getOrCreateVisitorId(useCookies = false) {
424
+ const key = STORAGE_KEYS.VISITOR_ID;
425
+ // Try to get existing ID
426
+ let visitorId = null;
427
+ if (useCookies) {
428
+ visitorId = cookie(key);
429
+ }
430
+ else {
431
+ visitorId = getLocalStorage(key);
432
+ }
433
+ // Create new ID if not found
434
+ if (!visitorId) {
435
+ visitorId = generateUUID();
436
+ if (useCookies) {
437
+ cookie(key, visitorId, 365); // 1 year
438
+ }
439
+ else {
440
+ setLocalStorage(key, visitorId);
441
+ }
442
+ }
443
+ return visitorId;
444
+ }
445
+ /**
446
+ * Get or create a session ID (expires after timeout)
447
+ */
448
+ function getOrCreateSessionId(timeout) {
449
+ const sidKey = STORAGE_KEYS.SESSION_ID;
450
+ const tsKey = STORAGE_KEYS.SESSION_TIMESTAMP;
451
+ let sessionId = getSessionStorage(sidKey);
452
+ const lastActivity = parseInt(getSessionStorage(tsKey) || '0', 10);
453
+ const now = Date.now();
454
+ // Check if session expired
455
+ if (!sessionId || now - lastActivity > timeout) {
456
+ sessionId = generateUUID();
457
+ setSessionStorage(sidKey, sessionId);
458
+ }
459
+ // Update last activity
460
+ setSessionStorage(tsKey, now.toString());
461
+ return sessionId;
462
+ }
463
+ /**
464
+ * Reset visitor and session IDs
465
+ */
466
+ function resetIds(useCookies = false) {
467
+ const visitorKey = STORAGE_KEYS.VISITOR_ID;
468
+ if (useCookies) {
469
+ cookie(visitorKey, '', -1); // Delete cookie
470
+ }
471
+ else {
472
+ try {
473
+ localStorage.removeItem(visitorKey);
474
+ }
475
+ catch {
476
+ // Ignore
477
+ }
478
+ }
479
+ try {
480
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_ID);
481
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_TIMESTAMP);
482
+ }
483
+ catch {
484
+ // Ignore
485
+ }
486
+ }
487
+ // ============================================
488
+ // URL UTILITIES
489
+ // ============================================
490
+ /**
491
+ * Extract UTM parameters from URL
492
+ */
493
+ function getUTMParams() {
494
+ if (typeof window === 'undefined')
495
+ return {};
496
+ try {
497
+ const params = new URLSearchParams(window.location.search);
498
+ return {
499
+ utmSource: params.get('utm_source') || undefined,
500
+ utmMedium: params.get('utm_medium') || undefined,
501
+ utmCampaign: params.get('utm_campaign') || undefined,
502
+ utmTerm: params.get('utm_term') || undefined,
503
+ utmContent: params.get('utm_content') || undefined,
504
+ };
505
+ }
506
+ catch {
507
+ return {};
508
+ }
509
+ }
510
+ /**
511
+ * Check if URL is a download link
512
+ */
513
+ function isDownloadUrl(url) {
514
+ const lowerUrl = url.toLowerCase();
515
+ return DOWNLOAD_EXTENSIONS.some((ext) => lowerUrl.includes(ext));
516
+ }
517
+ /**
518
+ * Extract filename from URL
519
+ */
520
+ function getFilenameFromUrl(url) {
521
+ try {
522
+ return url.split('/').pop()?.split('?')[0] || 'unknown';
523
+ }
524
+ catch {
525
+ return 'unknown';
526
+ }
527
+ }
528
+ /**
529
+ * Extract file extension from URL
530
+ */
531
+ function getFileExtension(url) {
532
+ const filename = getFilenameFromUrl(url);
533
+ const parts = filename.split('.');
534
+ return parts.length > 1 ? parts.pop() || 'unknown' : 'unknown';
535
+ }
536
+ // ============================================
537
+ // DOM UTILITIES
538
+ // ============================================
539
+ /**
540
+ * Get text content from element (truncated)
541
+ */
542
+ function getElementText(element, maxLength = 100) {
543
+ const text = element.innerText ||
544
+ element.textContent ||
545
+ element.value ||
546
+ '';
547
+ return text.trim().substring(0, maxLength);
548
+ }
549
+ /**
550
+ * Get element identification info
551
+ */
552
+ function getElementInfo(element) {
553
+ return {
554
+ tag: element.tagName?.toLowerCase() || 'unknown',
555
+ id: element.id || '',
556
+ className: element.className || '',
557
+ text: getElementText(element, 50),
558
+ };
559
+ }
560
+ /**
561
+ * Check if element is a trackable click target
562
+ */
563
+ function isTrackableClickElement(element) {
564
+ const trackableTags = ['BUTTON', 'A', 'INPUT'];
565
+ return (trackableTags.includes(element.tagName) ||
566
+ element.hasAttribute('data-track-click') ||
567
+ element.classList.contains('track-click'));
568
+ }
569
+ /**
570
+ * Check if device is mobile
571
+ */
572
+ function isMobile() {
573
+ if (typeof navigator === 'undefined')
574
+ return false;
575
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
576
+ }
577
+ // ============================================
578
+ // DEVICE INFO
579
+ // ============================================
580
+ /**
581
+ * Get current device information
582
+ */
583
+ function getDeviceInfo() {
584
+ if (typeof window === 'undefined' || typeof navigator === 'undefined') {
585
+ return {
586
+ userAgent: 'unknown',
587
+ screen: 'unknown',
588
+ language: 'unknown',
589
+ timezone: 'unknown',
590
+ };
591
+ }
592
+ return {
593
+ userAgent: navigator.userAgent,
594
+ screen: `${screen.width}x${screen.height}`,
595
+ language: navigator.language,
596
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'unknown',
597
+ };
598
+ }
599
+
600
+ /**
601
+ * Clianta SDK - Event Queue
602
+ * Handles batching and flushing of events
603
+ * @see SDK_VERSION in core/config.ts
604
+ */
605
+ const MAX_QUEUE_SIZE = 1000;
606
+ /** Rate limit: max events per window */
607
+ const RATE_LIMIT_MAX_EVENTS = 100;
608
+ /** Rate limit window in ms (1 minute) */
609
+ const RATE_LIMIT_WINDOW_MS = 60000;
610
+ /**
611
+ * Event queue with batching, persistence, rate limiting, and auto-flush
612
+ */
613
+ class EventQueue {
614
+ constructor(transport, config = {}) {
615
+ this.queue = [];
616
+ this.flushTimer = null;
617
+ this.isFlushing = false;
618
+ /** Rate limiting: timestamps of recent events */
619
+ this.eventTimestamps = [];
620
+ /** Unload handler references for cleanup */
621
+ this.boundBeforeUnload = null;
622
+ this.boundVisibilityChange = null;
623
+ this.boundPageHide = null;
624
+ this.transport = transport;
625
+ this.config = {
626
+ batchSize: config.batchSize ?? 10,
627
+ flushInterval: config.flushInterval ?? 5000,
628
+ maxQueueSize: config.maxQueueSize ?? MAX_QUEUE_SIZE,
629
+ storageKey: config.storageKey ?? STORAGE_KEYS.EVENT_QUEUE,
630
+ };
631
+ // Restore persisted queue
632
+ this.restoreQueue();
633
+ // Start auto-flush timer
634
+ this.startFlushTimer();
635
+ // Setup unload handlers
636
+ this.setupUnloadHandlers();
637
+ }
638
+ /**
639
+ * Add an event to the queue
640
+ */
641
+ push(event) {
642
+ // Rate limiting check
643
+ if (!this.checkRateLimit()) {
644
+ logger.warn('Rate limit exceeded, event dropped:', event.eventName);
645
+ return;
646
+ }
647
+ // Don't exceed max queue size
648
+ if (this.queue.length >= this.config.maxQueueSize) {
649
+ logger.warn('Queue full, dropping oldest event');
650
+ this.queue.shift();
651
+ }
652
+ this.queue.push(event);
653
+ logger.debug('Event queued:', event.eventName, `(${this.queue.length} in queue)`);
654
+ // Flush if batch size reached
655
+ if (this.queue.length >= this.config.batchSize) {
656
+ this.flush();
657
+ }
658
+ }
659
+ /**
660
+ * Check and enforce rate limiting
661
+ * @returns true if event is allowed, false if rate limited
662
+ */
663
+ checkRateLimit() {
664
+ const now = Date.now();
665
+ // Remove timestamps outside the window
666
+ this.eventTimestamps = this.eventTimestamps.filter(ts => now - ts < RATE_LIMIT_WINDOW_MS);
667
+ // Check if under limit
668
+ if (this.eventTimestamps.length >= RATE_LIMIT_MAX_EVENTS) {
669
+ return false;
670
+ }
671
+ // Record this event
672
+ this.eventTimestamps.push(now);
673
+ return true;
674
+ }
675
+ /**
676
+ * Flush the queue (send all events)
677
+ */
678
+ async flush() {
679
+ if (this.isFlushing || this.queue.length === 0) {
680
+ return;
681
+ }
682
+ this.isFlushing = true;
683
+ // Atomically take snapshot of current queue length to avoid race condition
684
+ const count = this.queue.length;
685
+ const events = this.queue.splice(0, count);
686
+ try {
687
+ logger.debug(`Flushing ${events.length} events`);
688
+ // Clear persisted queue
689
+ this.persistQueue([]);
690
+ // Send to backend
691
+ const result = await this.transport.sendEvents(events);
692
+ if (!result.success) {
693
+ // Re-queue events on failure (at the front)
694
+ logger.warn('Flush failed, re-queuing events');
695
+ this.queue.unshift(...events);
696
+ this.persistQueue(this.queue);
697
+ }
698
+ else {
699
+ logger.debug('Flush successful');
700
+ }
701
+ }
702
+ catch (error) {
703
+ logger.error('Flush error:', error);
704
+ }
705
+ finally {
706
+ this.isFlushing = false;
707
+ }
708
+ }
709
+ /**
710
+ * Flush synchronously using sendBeacon (for page unload)
711
+ */
712
+ flushSync() {
713
+ if (this.queue.length === 0)
714
+ return;
715
+ const events = this.queue.splice(0, this.queue.length);
716
+ logger.debug(`Sync flushing ${events.length} events via beacon`);
717
+ const success = this.transport.sendBeacon(events);
718
+ if (!success) {
719
+ // Re-queue and persist for next page load
720
+ this.queue.unshift(...events);
721
+ this.persistQueue(this.queue);
722
+ }
723
+ }
724
+ /**
725
+ * Get current queue length
726
+ */
727
+ get length() {
728
+ return this.queue.length;
729
+ }
730
+ /**
731
+ * Clear the queue
732
+ */
733
+ clear() {
734
+ this.queue = [];
735
+ this.persistQueue([]);
736
+ }
737
+ /**
738
+ * Stop the flush timer and cleanup handlers
739
+ */
740
+ destroy() {
741
+ if (this.flushTimer) {
742
+ clearInterval(this.flushTimer);
743
+ this.flushTimer = null;
744
+ }
745
+ // Remove unload handlers
746
+ if (typeof window !== 'undefined') {
747
+ if (this.boundBeforeUnload) {
748
+ window.removeEventListener('beforeunload', this.boundBeforeUnload);
749
+ }
750
+ if (this.boundVisibilityChange) {
751
+ window.removeEventListener('visibilitychange', this.boundVisibilityChange);
752
+ }
753
+ if (this.boundPageHide) {
754
+ window.removeEventListener('pagehide', this.boundPageHide);
755
+ }
756
+ }
757
+ }
758
+ /**
759
+ * Start auto-flush timer
760
+ */
761
+ startFlushTimer() {
762
+ if (this.flushTimer) {
763
+ clearInterval(this.flushTimer);
764
+ }
765
+ this.flushTimer = setInterval(() => {
766
+ this.flush();
767
+ }, this.config.flushInterval);
768
+ }
769
+ /**
770
+ * Setup page unload handlers
771
+ */
772
+ setupUnloadHandlers() {
773
+ if (typeof window === 'undefined')
774
+ return;
775
+ // Flush on page unload
776
+ this.boundBeforeUnload = () => this.flushSync();
777
+ window.addEventListener('beforeunload', this.boundBeforeUnload);
778
+ // Flush when page becomes hidden
779
+ this.boundVisibilityChange = () => {
780
+ if (document.visibilityState === 'hidden') {
781
+ this.flushSync();
782
+ }
783
+ };
784
+ window.addEventListener('visibilitychange', this.boundVisibilityChange);
785
+ // Flush on page hide (iOS Safari)
786
+ this.boundPageHide = () => this.flushSync();
787
+ window.addEventListener('pagehide', this.boundPageHide);
788
+ }
789
+ /**
790
+ * Persist queue to localStorage
791
+ */
792
+ persistQueue(events) {
793
+ try {
794
+ setLocalStorage(this.config.storageKey, JSON.stringify(events));
795
+ }
796
+ catch {
797
+ // Ignore storage errors
798
+ }
799
+ }
800
+ /**
801
+ * Restore queue from localStorage
802
+ */
803
+ restoreQueue() {
804
+ try {
805
+ const stored = getLocalStorage(this.config.storageKey);
806
+ if (stored) {
807
+ const events = JSON.parse(stored);
808
+ if (Array.isArray(events) && events.length > 0) {
809
+ this.queue = events;
810
+ logger.debug(`Restored ${events.length} events from storage`);
811
+ }
812
+ }
813
+ }
814
+ catch {
815
+ // Ignore parse errors
816
+ }
817
+ }
818
+ }
819
+
820
+ /**
821
+ * Clianta SDK - Plugin Base
822
+ * @see SDK_VERSION in core/config.ts
823
+ */
824
+ /**
825
+ * Base class for plugins
826
+ */
827
+ class BasePlugin {
828
+ constructor() {
829
+ this.tracker = null;
830
+ }
831
+ init(tracker) {
832
+ this.tracker = tracker;
833
+ }
834
+ destroy() {
835
+ this.tracker = null;
836
+ }
837
+ track(eventType, eventName, properties) {
838
+ if (this.tracker) {
839
+ this.tracker.track(eventType, eventName, properties);
840
+ }
841
+ }
842
+ }
843
+
844
+ /**
845
+ * Clianta SDK - Page View Plugin
846
+ * @see SDK_VERSION in core/config.ts
847
+ */
848
+ /**
849
+ * Page View Plugin - Tracks page views
850
+ */
851
+ class PageViewPlugin extends BasePlugin {
852
+ constructor() {
853
+ super(...arguments);
854
+ this.name = 'pageView';
855
+ this.originalPushState = null;
856
+ this.originalReplaceState = null;
857
+ this.popstateHandler = null;
858
+ }
859
+ init(tracker) {
860
+ super.init(tracker);
861
+ // Track initial page view
862
+ this.trackPageView();
863
+ // Track SPA navigation (History API)
864
+ if (typeof window !== 'undefined') {
865
+ // Store originals for cleanup
866
+ this.originalPushState = history.pushState;
867
+ this.originalReplaceState = history.replaceState;
868
+ // Intercept pushState and replaceState
869
+ const self = this;
870
+ history.pushState = function (...args) {
871
+ self.originalPushState.apply(history, args);
872
+ self.trackPageView();
873
+ };
874
+ history.replaceState = function (...args) {
875
+ self.originalReplaceState.apply(history, args);
876
+ self.trackPageView();
877
+ };
878
+ // Handle back/forward navigation
879
+ this.popstateHandler = () => this.trackPageView();
880
+ window.addEventListener('popstate', this.popstateHandler);
881
+ }
882
+ }
883
+ destroy() {
884
+ // Restore original history methods
885
+ if (this.originalPushState) {
886
+ history.pushState = this.originalPushState;
887
+ this.originalPushState = null;
888
+ }
889
+ if (this.originalReplaceState) {
890
+ history.replaceState = this.originalReplaceState;
891
+ this.originalReplaceState = null;
892
+ }
893
+ // Remove popstate listener
894
+ if (this.popstateHandler && typeof window !== 'undefined') {
895
+ window.removeEventListener('popstate', this.popstateHandler);
896
+ this.popstateHandler = null;
897
+ }
898
+ super.destroy();
899
+ }
900
+ trackPageView() {
901
+ if (typeof window === 'undefined' || typeof document === 'undefined')
902
+ return;
903
+ this.track('page_view', 'Page Viewed', {
904
+ title: document.title,
905
+ path: window.location.pathname,
906
+ search: window.location.search,
907
+ hash: window.location.hash,
908
+ referrer: document.referrer || 'direct',
909
+ viewport: `${window.innerWidth}x${window.innerHeight}`,
910
+ });
911
+ }
912
+ }
913
+
914
+ /**
915
+ * Clianta SDK - Scroll Depth Plugin
916
+ * @see SDK_VERSION in core/config.ts
917
+ */
918
+ /**
919
+ * Scroll Depth Plugin - Tracks scroll milestones
920
+ */
921
+ class ScrollPlugin extends BasePlugin {
922
+ constructor() {
923
+ super(...arguments);
924
+ this.name = 'scroll';
925
+ this.milestonesReached = new Set();
926
+ this.maxScrollDepth = 0;
927
+ this.pageLoadTime = 0;
928
+ this.scrollTimeout = null;
929
+ this.boundHandler = null;
930
+ /** SPA navigation support */
931
+ this.originalPushState = null;
932
+ this.originalReplaceState = null;
933
+ this.popstateHandler = null;
934
+ }
935
+ init(tracker) {
936
+ super.init(tracker);
937
+ this.pageLoadTime = Date.now();
938
+ if (typeof window !== 'undefined') {
939
+ this.boundHandler = this.handleScroll.bind(this);
940
+ window.addEventListener('scroll', this.boundHandler, { passive: true });
941
+ // Setup SPA navigation reset
942
+ this.setupNavigationReset();
943
+ }
944
+ }
945
+ destroy() {
946
+ if (this.boundHandler && typeof window !== 'undefined') {
947
+ window.removeEventListener('scroll', this.boundHandler);
948
+ }
949
+ if (this.scrollTimeout) {
950
+ clearTimeout(this.scrollTimeout);
951
+ }
952
+ // Restore original history methods
953
+ if (this.originalPushState) {
954
+ history.pushState = this.originalPushState;
955
+ this.originalPushState = null;
956
+ }
957
+ if (this.originalReplaceState) {
958
+ history.replaceState = this.originalReplaceState;
959
+ this.originalReplaceState = null;
960
+ }
961
+ // Remove popstate listener
962
+ if (this.popstateHandler && typeof window !== 'undefined') {
963
+ window.removeEventListener('popstate', this.popstateHandler);
964
+ this.popstateHandler = null;
965
+ }
966
+ super.destroy();
967
+ }
968
+ /**
969
+ * Reset scroll tracking for SPA navigation
970
+ */
971
+ resetForNavigation() {
972
+ this.milestonesReached.clear();
973
+ this.maxScrollDepth = 0;
974
+ this.pageLoadTime = Date.now();
975
+ }
976
+ /**
977
+ * Setup History API interception for SPA navigation
978
+ */
979
+ setupNavigationReset() {
980
+ if (typeof window === 'undefined')
981
+ return;
982
+ // Store originals for cleanup
983
+ this.originalPushState = history.pushState;
984
+ this.originalReplaceState = history.replaceState;
985
+ // Intercept pushState and replaceState
986
+ const self = this;
987
+ history.pushState = function (...args) {
988
+ self.originalPushState.apply(history, args);
989
+ self.resetForNavigation();
990
+ };
991
+ history.replaceState = function (...args) {
992
+ self.originalReplaceState.apply(history, args);
993
+ self.resetForNavigation();
994
+ };
995
+ // Handle back/forward navigation
996
+ this.popstateHandler = () => this.resetForNavigation();
997
+ window.addEventListener('popstate', this.popstateHandler);
998
+ }
999
+ handleScroll() {
1000
+ // Debounce scroll tracking
1001
+ if (this.scrollTimeout) {
1002
+ clearTimeout(this.scrollTimeout);
1003
+ }
1004
+ this.scrollTimeout = setTimeout(() => this.trackScrollDepth(), 150);
1005
+ }
1006
+ trackScrollDepth() {
1007
+ if (typeof window === 'undefined' || typeof document === 'undefined')
1008
+ return;
1009
+ const windowHeight = window.innerHeight;
1010
+ const documentHeight = document.documentElement.scrollHeight;
1011
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
1012
+ const scrollableHeight = documentHeight - windowHeight;
1013
+ // Guard against divide-by-zero on short pages
1014
+ if (scrollableHeight <= 0)
1015
+ return;
1016
+ const scrollPercent = Math.floor((scrollTop / scrollableHeight) * 100);
1017
+ // Clamp to valid range
1018
+ const clampedPercent = Math.max(0, Math.min(100, scrollPercent));
1019
+ // Update max scroll depth
1020
+ if (clampedPercent > this.maxScrollDepth) {
1021
+ this.maxScrollDepth = clampedPercent;
1022
+ }
1023
+ // Track milestones
1024
+ for (const milestone of SCROLL_MILESTONES) {
1025
+ if (clampedPercent >= milestone && !this.milestonesReached.has(milestone)) {
1026
+ this.milestonesReached.add(milestone);
1027
+ this.track('scroll_depth', `Scrolled ${milestone}%`, {
1028
+ depth: milestone,
1029
+ maxDepth: this.maxScrollDepth,
1030
+ timeToReach: Date.now() - this.pageLoadTime,
1031
+ });
1032
+ }
1033
+ }
1034
+ }
1035
+ }
1036
+
1037
+ /**
1038
+ * Clianta SDK - Form Tracking Plugin
1039
+ * @see SDK_VERSION in core/config.ts
1040
+ */
1041
+ /**
1042
+ * Form Tracking Plugin - Auto-tracks form views, interactions, and submissions
1043
+ */
1044
+ class FormsPlugin extends BasePlugin {
1045
+ constructor() {
1046
+ super(...arguments);
1047
+ this.name = 'forms';
1048
+ this.trackedForms = new WeakSet();
1049
+ this.formInteractions = new Set();
1050
+ this.observer = null;
1051
+ this.listeners = [];
1052
+ }
1053
+ init(tracker) {
1054
+ super.init(tracker);
1055
+ if (typeof document === 'undefined')
1056
+ return;
1057
+ // Track existing forms
1058
+ this.trackAllForms();
1059
+ // Watch for dynamically added forms
1060
+ if (typeof MutationObserver !== 'undefined') {
1061
+ this.observer = new MutationObserver(() => this.trackAllForms());
1062
+ this.observer.observe(document.body, { childList: true, subtree: true });
1063
+ }
1064
+ }
1065
+ destroy() {
1066
+ if (this.observer) {
1067
+ this.observer.disconnect();
1068
+ this.observer = null;
1069
+ }
1070
+ // Remove all tracked event listeners
1071
+ for (const { element, event, handler } of this.listeners) {
1072
+ element.removeEventListener(event, handler);
1073
+ }
1074
+ this.listeners = [];
1075
+ super.destroy();
1076
+ }
1077
+ /**
1078
+ * Track event listener for cleanup
1079
+ */
1080
+ addListener(element, event, handler) {
1081
+ element.addEventListener(event, handler);
1082
+ this.listeners.push({ element, event, handler });
1083
+ }
1084
+ trackAllForms() {
1085
+ document.querySelectorAll('form').forEach((form) => {
1086
+ this.setupFormTracking(form);
1087
+ });
1088
+ }
1089
+ setupFormTracking(form) {
1090
+ if (this.trackedForms.has(form))
1091
+ return;
1092
+ this.trackedForms.add(form);
1093
+ const formId = form.id || form.name || `form-${Math.random().toString(36).substr(2, 9)}`;
1094
+ // Track form view
1095
+ this.track('form_view', 'Form Viewed', {
1096
+ formId,
1097
+ action: form.action,
1098
+ method: form.method,
1099
+ fieldCount: form.elements.length,
1100
+ });
1101
+ // Track field interactions
1102
+ Array.from(form.elements).forEach((field) => {
1103
+ if (field instanceof HTMLInputElement ||
1104
+ field instanceof HTMLSelectElement ||
1105
+ field instanceof HTMLTextAreaElement) {
1106
+ if (!field.name || field.type === 'submit' || field.type === 'button')
1107
+ return;
1108
+ ['focus', 'blur', 'change'].forEach((eventType) => {
1109
+ const handler = () => {
1110
+ const key = `${formId}-${field.name}-${eventType}`;
1111
+ if (!this.formInteractions.has(key)) {
1112
+ this.formInteractions.add(key);
1113
+ this.track('form_interaction', 'Form Field Interaction', {
1114
+ formId,
1115
+ fieldName: field.name,
1116
+ fieldType: field.type,
1117
+ interactionType: eventType,
1118
+ });
1119
+ }
1120
+ };
1121
+ this.addListener(field, eventType, handler);
1122
+ });
1123
+ }
1124
+ });
1125
+ // Track form submission
1126
+ const submitHandler = () => {
1127
+ this.track('form_submit', 'Form Submitted', {
1128
+ formId,
1129
+ action: form.action,
1130
+ method: form.method,
1131
+ });
1132
+ // Auto-identify if email field found
1133
+ this.autoIdentify(form);
1134
+ };
1135
+ this.addListener(form, 'submit', submitHandler);
1136
+ }
1137
+ autoIdentify(form) {
1138
+ const emailField = form.querySelector('input[type="email"], input[name*="email"]');
1139
+ if (!emailField?.value || !this.tracker)
1140
+ return;
1141
+ const email = emailField.value;
1142
+ const traits = {};
1143
+ // Capture common fields
1144
+ const firstNameField = form.querySelector('[name*="first"], [name*="fname"]');
1145
+ const lastNameField = form.querySelector('[name*="last"], [name*="lname"]');
1146
+ const companyField = form.querySelector('[name*="company"], [name*="organization"]');
1147
+ const phoneField = form.querySelector('[type="tel"], [name*="phone"]');
1148
+ if (firstNameField?.value)
1149
+ traits.firstName = firstNameField.value;
1150
+ if (lastNameField?.value)
1151
+ traits.lastName = lastNameField.value;
1152
+ if (companyField?.value)
1153
+ traits.company = companyField.value;
1154
+ if (phoneField?.value)
1155
+ traits.phone = phoneField.value;
1156
+ this.tracker.identify(email, traits);
1157
+ }
1158
+ }
1159
+
1160
+ /**
1161
+ * Clianta SDK - Click Tracking Plugin
1162
+ * @see SDK_VERSION in core/config.ts
1163
+ */
1164
+ /**
1165
+ * Click Tracking Plugin - Tracks button and CTA clicks
1166
+ */
1167
+ class ClicksPlugin extends BasePlugin {
1168
+ constructor() {
1169
+ super(...arguments);
1170
+ this.name = 'clicks';
1171
+ this.boundHandler = null;
1172
+ }
1173
+ init(tracker) {
1174
+ super.init(tracker);
1175
+ if (typeof document !== 'undefined') {
1176
+ this.boundHandler = this.handleClick.bind(this);
1177
+ document.addEventListener('click', this.boundHandler, true);
1178
+ }
1179
+ }
1180
+ destroy() {
1181
+ if (this.boundHandler && typeof document !== 'undefined') {
1182
+ document.removeEventListener('click', this.boundHandler, true);
1183
+ }
1184
+ super.destroy();
1185
+ }
1186
+ handleClick(e) {
1187
+ const target = e.target;
1188
+ if (!target || !isTrackableClickElement(target))
1189
+ return;
1190
+ const buttonText = getElementText(target, 100);
1191
+ const elementInfo = getElementInfo(target);
1192
+ this.track('button_click', 'Button Clicked', {
1193
+ buttonText,
1194
+ elementType: target.tagName.toLowerCase(),
1195
+ elementId: elementInfo.id,
1196
+ elementClass: elementInfo.className,
1197
+ href: target.href || undefined,
1198
+ });
1199
+ }
1200
+ }
1201
+
1202
+ /**
1203
+ * Clianta SDK - Engagement Plugin
1204
+ * @see SDK_VERSION in core/config.ts
1205
+ */
1206
+ /**
1207
+ * Engagement Plugin - Tracks user engagement and time on page
1208
+ */
1209
+ class EngagementPlugin extends BasePlugin {
1210
+ constructor() {
1211
+ super(...arguments);
1212
+ this.name = 'engagement';
1213
+ this.pageLoadTime = 0;
1214
+ this.engagementStartTime = 0;
1215
+ this.isEngaged = false;
1216
+ this.engagementTimeout = null;
1217
+ this.boundMarkEngaged = null;
1218
+ this.boundTrackTimeOnPage = null;
1219
+ this.boundVisibilityHandler = null;
1220
+ }
1221
+ init(tracker) {
1222
+ super.init(tracker);
1223
+ this.pageLoadTime = Date.now();
1224
+ this.engagementStartTime = Date.now();
1225
+ if (typeof document === 'undefined' || typeof window === 'undefined')
1226
+ return;
1227
+ // Setup engagement detection
1228
+ this.boundMarkEngaged = this.markEngaged.bind(this);
1229
+ this.boundTrackTimeOnPage = this.trackTimeOnPage.bind(this);
1230
+ this.boundVisibilityHandler = () => {
1231
+ if (document.visibilityState === 'hidden') {
1232
+ this.trackTimeOnPage();
1233
+ }
1234
+ else {
1235
+ // Reset engagement timer when page becomes visible again
1236
+ this.engagementStartTime = Date.now();
1237
+ }
1238
+ };
1239
+ ['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
1240
+ document.addEventListener(event, this.boundMarkEngaged, { passive: true });
1241
+ });
1242
+ // Track time on page before unload
1243
+ window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
1244
+ document.addEventListener('visibilitychange', this.boundVisibilityHandler);
1245
+ }
1246
+ destroy() {
1247
+ if (this.boundMarkEngaged && typeof document !== 'undefined') {
1248
+ ['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
1249
+ document.removeEventListener(event, this.boundMarkEngaged);
1250
+ });
1251
+ }
1252
+ if (this.boundTrackTimeOnPage && typeof window !== 'undefined') {
1253
+ window.removeEventListener('beforeunload', this.boundTrackTimeOnPage);
1254
+ }
1255
+ if (this.boundVisibilityHandler && typeof document !== 'undefined') {
1256
+ document.removeEventListener('visibilitychange', this.boundVisibilityHandler);
1257
+ }
1258
+ if (this.engagementTimeout) {
1259
+ clearTimeout(this.engagementTimeout);
1260
+ }
1261
+ super.destroy();
1262
+ }
1263
+ markEngaged() {
1264
+ if (!this.isEngaged) {
1265
+ this.isEngaged = true;
1266
+ this.track('engagement', 'User Engaged', {
1267
+ timeToEngage: Date.now() - this.pageLoadTime,
1268
+ });
1269
+ }
1270
+ // Reset engagement timeout
1271
+ if (this.engagementTimeout) {
1272
+ clearTimeout(this.engagementTimeout);
1273
+ }
1274
+ this.engagementTimeout = setTimeout(() => {
1275
+ this.isEngaged = false;
1276
+ }, 30000); // 30 seconds of inactivity
1277
+ }
1278
+ trackTimeOnPage() {
1279
+ const timeSpent = Math.floor((Date.now() - this.engagementStartTime) / 1000);
1280
+ if (timeSpent > 0) {
1281
+ this.track('time_on_page', 'Time Spent', {
1282
+ seconds: timeSpent,
1283
+ engaged: this.isEngaged,
1284
+ });
1285
+ }
1286
+ }
1287
+ }
1288
+
1289
+ /**
1290
+ * Clianta SDK - Downloads Plugin
1291
+ * @see SDK_VERSION in core/config.ts
1292
+ */
1293
+ /**
1294
+ * Downloads Plugin - Tracks file downloads
1295
+ */
1296
+ class DownloadsPlugin extends BasePlugin {
1297
+ constructor() {
1298
+ super(...arguments);
1299
+ this.name = 'downloads';
1300
+ this.trackedDownloads = new Set();
1301
+ this.boundHandler = null;
1302
+ /** SPA navigation support */
1303
+ this.originalPushState = null;
1304
+ this.originalReplaceState = null;
1305
+ this.popstateHandler = null;
1306
+ }
1307
+ init(tracker) {
1308
+ super.init(tracker);
1309
+ if (typeof document !== 'undefined') {
1310
+ this.boundHandler = this.handleClick.bind(this);
1311
+ document.addEventListener('click', this.boundHandler, true);
1312
+ // Setup SPA navigation reset
1313
+ this.setupNavigationReset();
1314
+ }
1315
+ }
1316
+ destroy() {
1317
+ if (this.boundHandler && typeof document !== 'undefined') {
1318
+ document.removeEventListener('click', this.boundHandler, true);
1319
+ }
1320
+ // Restore original history methods
1321
+ if (this.originalPushState) {
1322
+ history.pushState = this.originalPushState;
1323
+ this.originalPushState = null;
1324
+ }
1325
+ if (this.originalReplaceState) {
1326
+ history.replaceState = this.originalReplaceState;
1327
+ this.originalReplaceState = null;
1328
+ }
1329
+ // Remove popstate listener
1330
+ if (this.popstateHandler && typeof window !== 'undefined') {
1331
+ window.removeEventListener('popstate', this.popstateHandler);
1332
+ this.popstateHandler = null;
1333
+ }
1334
+ super.destroy();
1335
+ }
1336
+ /**
1337
+ * Reset download tracking for SPA navigation
1338
+ */
1339
+ resetForNavigation() {
1340
+ this.trackedDownloads.clear();
1341
+ }
1342
+ /**
1343
+ * Setup History API interception for SPA navigation
1344
+ */
1345
+ setupNavigationReset() {
1346
+ if (typeof window === 'undefined')
1347
+ return;
1348
+ // Store originals for cleanup
1349
+ this.originalPushState = history.pushState;
1350
+ this.originalReplaceState = history.replaceState;
1351
+ // Intercept pushState and replaceState
1352
+ const self = this;
1353
+ history.pushState = function (...args) {
1354
+ self.originalPushState.apply(history, args);
1355
+ self.resetForNavigation();
1356
+ };
1357
+ history.replaceState = function (...args) {
1358
+ self.originalReplaceState.apply(history, args);
1359
+ self.resetForNavigation();
1360
+ };
1361
+ // Handle back/forward navigation
1362
+ this.popstateHandler = () => this.resetForNavigation();
1363
+ window.addEventListener('popstate', this.popstateHandler);
1364
+ }
1365
+ handleClick(e) {
1366
+ const link = e.target.closest('a');
1367
+ if (!link || !link.href)
1368
+ return;
1369
+ const url = link.href;
1370
+ // Check if it's a download link
1371
+ if (!isDownloadUrl(url))
1372
+ return;
1373
+ // Avoid tracking the same download multiple times
1374
+ if (this.trackedDownloads.has(url))
1375
+ return;
1376
+ this.trackedDownloads.add(url);
1377
+ this.track('download', 'File Download', {
1378
+ url,
1379
+ filename: getFilenameFromUrl(url),
1380
+ fileType: getFileExtension(url),
1381
+ linkText: getElementText(link, 100),
1382
+ });
1383
+ }
1384
+ }
1385
+
1386
+ /**
1387
+ * Clianta SDK - Exit Intent Plugin
1388
+ * @see SDK_VERSION in core/config.ts
1389
+ */
1390
+ /**
1391
+ * Exit Intent Plugin - Detects when user intends to leave the page
1392
+ */
1393
+ class ExitIntentPlugin extends BasePlugin {
1394
+ constructor() {
1395
+ super(...arguments);
1396
+ this.name = 'exitIntent';
1397
+ this.exitIntentShown = false;
1398
+ this.pageLoadTime = 0;
1399
+ this.boundHandler = null;
1400
+ }
1401
+ init(tracker) {
1402
+ super.init(tracker);
1403
+ this.pageLoadTime = Date.now();
1404
+ // Skip on mobile (no mouse events)
1405
+ if (isMobile())
1406
+ return;
1407
+ if (typeof document !== 'undefined') {
1408
+ this.boundHandler = this.handleMouseLeave.bind(this);
1409
+ document.addEventListener('mouseleave', this.boundHandler);
1410
+ }
1411
+ }
1412
+ destroy() {
1413
+ if (this.boundHandler && typeof document !== 'undefined') {
1414
+ document.removeEventListener('mouseleave', this.boundHandler);
1415
+ }
1416
+ super.destroy();
1417
+ }
1418
+ handleMouseLeave(e) {
1419
+ // Only trigger when mouse leaves from the top of the page
1420
+ if (e.clientY > 0 || this.exitIntentShown)
1421
+ return;
1422
+ this.exitIntentShown = true;
1423
+ this.track('exit_intent', 'Exit Intent Detected', {
1424
+ timeOnPage: Date.now() - this.pageLoadTime,
1425
+ });
1426
+ }
1427
+ }
1428
+
1429
+ /**
1430
+ * Clianta SDK - Error Tracking Plugin
1431
+ * @see SDK_VERSION in core/config.ts
1432
+ */
1433
+ /**
1434
+ * Error Tracking Plugin - Tracks JavaScript errors
1435
+ */
1436
+ class ErrorsPlugin extends BasePlugin {
1437
+ constructor() {
1438
+ super(...arguments);
1439
+ this.name = 'errors';
1440
+ this.boundErrorHandler = null;
1441
+ this.boundRejectionHandler = null;
1442
+ }
1443
+ init(tracker) {
1444
+ super.init(tracker);
1445
+ if (typeof window !== 'undefined') {
1446
+ this.boundErrorHandler = this.handleError.bind(this);
1447
+ this.boundRejectionHandler = this.handleRejection.bind(this);
1448
+ window.addEventListener('error', this.boundErrorHandler);
1449
+ window.addEventListener('unhandledrejection', this.boundRejectionHandler);
1450
+ }
1451
+ }
1452
+ destroy() {
1453
+ if (typeof window !== 'undefined') {
1454
+ if (this.boundErrorHandler) {
1455
+ window.removeEventListener('error', this.boundErrorHandler);
1456
+ }
1457
+ if (this.boundRejectionHandler) {
1458
+ window.removeEventListener('unhandledrejection', this.boundRejectionHandler);
1459
+ }
1460
+ }
1461
+ super.destroy();
1462
+ }
1463
+ handleError(e) {
1464
+ this.track('error', 'JavaScript Error', {
1465
+ message: e.message,
1466
+ filename: e.filename,
1467
+ line: e.lineno,
1468
+ column: e.colno,
1469
+ stack: e.error?.stack?.substring(0, 500),
1470
+ });
1471
+ }
1472
+ handleRejection(e) {
1473
+ this.track('error', 'Unhandled Promise Rejection', {
1474
+ reason: String(e.reason).substring(0, 200),
1475
+ });
1476
+ }
1477
+ }
1478
+
1479
+ /**
1480
+ * Clianta SDK - Performance Plugin
1481
+ * @see SDK_VERSION in core/config.ts
1482
+ */
1483
+ /**
1484
+ * Performance Plugin - Tracks page performance and Web Vitals
1485
+ */
1486
+ class PerformancePlugin extends BasePlugin {
1487
+ constructor() {
1488
+ super(...arguments);
1489
+ this.name = 'performance';
1490
+ this.boundLoadHandler = null;
1491
+ this.observers = [];
1492
+ this.boundClsVisibilityHandler = null;
1493
+ }
1494
+ init(tracker) {
1495
+ super.init(tracker);
1496
+ if (typeof window !== 'undefined') {
1497
+ // Track performance after page load
1498
+ this.boundLoadHandler = () => {
1499
+ // Delay to ensure all metrics are available
1500
+ setTimeout(() => this.trackPerformance(), 100);
1501
+ };
1502
+ window.addEventListener('load', this.boundLoadHandler);
1503
+ }
1504
+ }
1505
+ destroy() {
1506
+ if (this.boundLoadHandler && typeof window !== 'undefined') {
1507
+ window.removeEventListener('load', this.boundLoadHandler);
1508
+ }
1509
+ for (const observer of this.observers) {
1510
+ observer.disconnect();
1511
+ }
1512
+ this.observers = [];
1513
+ if (this.boundClsVisibilityHandler && typeof window !== 'undefined') {
1514
+ window.removeEventListener('visibilitychange', this.boundClsVisibilityHandler);
1515
+ }
1516
+ super.destroy();
1517
+ }
1518
+ trackPerformance() {
1519
+ if (typeof performance === 'undefined')
1520
+ return;
1521
+ // Use modern Navigation Timing API (PerformanceNavigationTiming)
1522
+ const entries = performance.getEntriesByType('navigation');
1523
+ if (entries.length > 0) {
1524
+ const navTiming = entries[0];
1525
+ const loadTime = Math.round(navTiming.loadEventEnd - navTiming.startTime);
1526
+ const domReady = Math.round(navTiming.domContentLoadedEventEnd - navTiming.startTime);
1527
+ const ttfb = Math.round(navTiming.responseStart - navTiming.requestStart);
1528
+ const domInteractive = Math.round(navTiming.domInteractive - navTiming.startTime);
1529
+ this.track('performance', 'Page Performance', {
1530
+ loadTime,
1531
+ domReady,
1532
+ ttfb, // Time to First Byte
1533
+ domInteractive,
1534
+ // Additional modern metrics
1535
+ dns: Math.round(navTiming.domainLookupEnd - navTiming.domainLookupStart),
1536
+ connection: Math.round(navTiming.connectEnd - navTiming.connectStart),
1537
+ transferSize: navTiming.transferSize,
1538
+ });
1539
+ }
1540
+ else {
1541
+ // Fallback for older browsers using deprecated API
1542
+ const timing = performance.timing;
1543
+ if (!timing)
1544
+ return;
1545
+ const loadTime = timing.loadEventEnd - timing.navigationStart;
1546
+ const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;
1547
+ const ttfb = timing.responseStart - timing.navigationStart;
1548
+ const domInteractive = timing.domInteractive - timing.navigationStart;
1549
+ this.track('performance', 'Page Performance', {
1550
+ loadTime,
1551
+ domReady,
1552
+ ttfb,
1553
+ domInteractive,
1554
+ });
1555
+ }
1556
+ // Track Web Vitals if available
1557
+ this.trackWebVitals();
1558
+ }
1559
+ trackWebVitals() {
1560
+ // LCP (Largest Contentful Paint)
1561
+ if ('PerformanceObserver' in window) {
1562
+ try {
1563
+ const lcpObserver = new PerformanceObserver((entryList) => {
1564
+ const entries = entryList.getEntries();
1565
+ const lastEntry = entries[entries.length - 1];
1566
+ if (lastEntry) {
1567
+ this.track('performance', 'Web Vital - LCP', {
1568
+ metric: 'LCP',
1569
+ value: Math.round(lastEntry.startTime),
1570
+ });
1571
+ }
1572
+ });
1573
+ lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
1574
+ this.observers.push(lcpObserver);
1575
+ }
1576
+ catch {
1577
+ // LCP not supported
1578
+ }
1579
+ // FID (First Input Delay)
1580
+ try {
1581
+ const fidObserver = new PerformanceObserver((entryList) => {
1582
+ const entries = entryList.getEntries();
1583
+ const firstEntry = entries[0];
1584
+ if (firstEntry) {
1585
+ this.track('performance', 'Web Vital - FID', {
1586
+ metric: 'FID',
1587
+ value: Math.round(firstEntry.processingStart - firstEntry.startTime),
1588
+ });
1589
+ }
1590
+ });
1591
+ fidObserver.observe({ type: 'first-input', buffered: true });
1592
+ this.observers.push(fidObserver);
1593
+ }
1594
+ catch {
1595
+ // FID not supported
1596
+ }
1597
+ // CLS (Cumulative Layout Shift)
1598
+ try {
1599
+ let clsValue = 0;
1600
+ const clsObserver = new PerformanceObserver((entryList) => {
1601
+ const entries = entryList.getEntries();
1602
+ entries.forEach((entry) => {
1603
+ if (!entry.hadRecentInput) {
1604
+ clsValue += entry.value || 0;
1605
+ }
1606
+ });
1607
+ });
1608
+ clsObserver.observe({ type: 'layout-shift', buffered: true });
1609
+ this.observers.push(clsObserver);
1610
+ // Report CLS after page is hidden
1611
+ this.boundClsVisibilityHandler = () => {
1612
+ if (document.visibilityState === 'hidden' && clsValue > 0) {
1613
+ this.track('performance', 'Web Vital - CLS', {
1614
+ metric: 'CLS',
1615
+ value: Math.round(clsValue * 1000) / 1000,
1616
+ });
1617
+ }
1618
+ };
1619
+ window.addEventListener('visibilitychange', this.boundClsVisibilityHandler, { once: true });
1620
+ }
1621
+ catch {
1622
+ // CLS not supported
1623
+ }
1624
+ }
1625
+ }
1626
+ }
1627
+
1628
+ /**
1629
+ * Clianta Tracking SDK - Popup Forms Plugin
1630
+ * @see SDK_VERSION in core/config.ts
1631
+ *
1632
+ * Auto-loads and displays lead capture popups based on triggers
1633
+ */
1634
+ /**
1635
+ * Popup Forms Plugin - Fetches and displays lead capture forms
1636
+ */
1637
+ class PopupFormsPlugin extends BasePlugin {
1638
+ constructor() {
1639
+ super(...arguments);
1640
+ this.name = 'popupForms';
1641
+ this.forms = [];
1642
+ this.shownForms = new Set();
1643
+ this.scrollHandler = null;
1644
+ this.exitHandler = null;
1645
+ }
1646
+ async init(tracker) {
1647
+ super.init(tracker);
1648
+ if (typeof window === 'undefined')
1649
+ return;
1650
+ // Load shown forms from storage
1651
+ this.loadShownForms();
1652
+ // Fetch active forms
1653
+ await this.fetchForms();
1654
+ // Setup triggers
1655
+ this.setupTriggers();
1656
+ }
1657
+ destroy() {
1658
+ this.removeTriggers();
1659
+ super.destroy();
1660
+ }
1661
+ loadShownForms() {
1662
+ try {
1663
+ const stored = localStorage.getItem('clianta_shown_forms');
1664
+ if (stored) {
1665
+ const data = JSON.parse(stored);
1666
+ this.shownForms = new Set(data.forms || []);
1667
+ }
1668
+ }
1669
+ catch (e) {
1670
+ // Ignore storage errors
1671
+ }
1672
+ }
1673
+ saveShownForms() {
1674
+ try {
1675
+ localStorage.setItem('clianta_shown_forms', JSON.stringify({
1676
+ forms: Array.from(this.shownForms),
1677
+ timestamp: Date.now(),
1678
+ }));
1679
+ }
1680
+ catch (e) {
1681
+ // Ignore storage errors
1682
+ }
1683
+ }
1684
+ async fetchForms() {
1685
+ if (!this.tracker)
1686
+ return;
1687
+ const config = this.tracker.getConfig();
1688
+ const workspaceId = this.tracker.getWorkspaceId();
1689
+ const apiEndpoint = config.apiEndpoint || 'https://api.clianta.online';
1690
+ try {
1691
+ const url = encodeURIComponent(window.location.href);
1692
+ const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${workspaceId}?url=${url}`);
1693
+ if (!response.ok)
1694
+ return;
1695
+ const data = await response.json();
1696
+ if (data.success && Array.isArray(data.data)) {
1697
+ this.forms = data.data.filter((form) => this.shouldShowForm(form));
1698
+ }
1699
+ }
1700
+ catch (error) {
1701
+ console.error('[Clianta] Failed to fetch forms:', error);
1702
+ }
1703
+ }
1704
+ shouldShowForm(form) {
1705
+ // Check show frequency
1706
+ if (form.showFrequency === 'once_per_visitor') {
1707
+ if (this.shownForms.has(form._id))
1708
+ return false;
1709
+ }
1710
+ else if (form.showFrequency === 'once_per_session') {
1711
+ const sessionKey = `clianta_form_${form._id}_shown`;
1712
+ if (sessionStorage.getItem(sessionKey))
1713
+ return false;
1714
+ }
1715
+ return true;
1716
+ }
1717
+ setupTriggers() {
1718
+ this.forms.forEach(form => {
1719
+ switch (form.trigger.type) {
1720
+ case 'delay':
1721
+ setTimeout(() => this.showForm(form), (form.trigger.value || 5) * 1000);
1722
+ break;
1723
+ case 'scroll':
1724
+ this.setupScrollTrigger(form);
1725
+ break;
1726
+ case 'exit_intent':
1727
+ this.setupExitIntentTrigger(form);
1728
+ break;
1729
+ case 'click':
1730
+ this.setupClickTrigger(form);
1731
+ break;
1732
+ }
1733
+ });
1734
+ }
1735
+ setupScrollTrigger(form) {
1736
+ const threshold = form.trigger.value || 50;
1737
+ this.scrollHandler = () => {
1738
+ const scrollPercent = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
1739
+ if (scrollPercent >= threshold) {
1740
+ this.showForm(form);
1741
+ if (this.scrollHandler) {
1742
+ window.removeEventListener('scroll', this.scrollHandler);
1743
+ }
1744
+ }
1745
+ };
1746
+ window.addEventListener('scroll', this.scrollHandler, { passive: true });
1747
+ }
1748
+ setupExitIntentTrigger(form) {
1749
+ this.exitHandler = (e) => {
1750
+ if (e.clientY <= 0) {
1751
+ this.showForm(form);
1752
+ if (this.exitHandler) {
1753
+ document.removeEventListener('mouseout', this.exitHandler);
1754
+ }
1755
+ }
1756
+ };
1757
+ document.addEventListener('mouseout', this.exitHandler);
1758
+ }
1759
+ setupClickTrigger(form) {
1760
+ if (!form.trigger.selector)
1761
+ return;
1762
+ const elements = document.querySelectorAll(form.trigger.selector);
1763
+ elements.forEach(el => {
1764
+ el.addEventListener('click', () => this.showForm(form));
1765
+ });
1766
+ }
1767
+ removeTriggers() {
1768
+ if (this.scrollHandler) {
1769
+ window.removeEventListener('scroll', this.scrollHandler);
1770
+ }
1771
+ if (this.exitHandler) {
1772
+ document.removeEventListener('mouseout', this.exitHandler);
1773
+ }
1774
+ }
1775
+ async showForm(form) {
1776
+ // Check if already shown in this session
1777
+ if (!this.shouldShowForm(form))
1778
+ return;
1779
+ // Mark as shown
1780
+ this.shownForms.add(form._id);
1781
+ this.saveShownForms();
1782
+ sessionStorage.setItem(`clianta_form_${form._id}_shown`, 'true');
1783
+ // Track view
1784
+ await this.trackFormView(form._id);
1785
+ // Render form
1786
+ this.renderForm(form);
1787
+ }
1788
+ async trackFormView(formId) {
1789
+ if (!this.tracker)
1790
+ return;
1791
+ const config = this.tracker.getConfig();
1792
+ const apiEndpoint = config.apiEndpoint || 'https://api.clianta.online';
1793
+ try {
1794
+ await fetch(`${apiEndpoint}/api/public/lead-forms/${formId}/view`, {
1795
+ method: 'POST',
1796
+ headers: { 'Content-Type': 'application/json' },
1797
+ });
1798
+ }
1799
+ catch (e) {
1800
+ // Ignore tracking errors
1801
+ }
1802
+ }
1803
+ renderForm(form) {
1804
+ // Create overlay
1805
+ const overlay = document.createElement('div');
1806
+ overlay.id = `clianta-form-overlay-${form._id}`;
1807
+ overlay.style.cssText = `
1808
+ position: fixed;
1809
+ top: 0;
1810
+ left: 0;
1811
+ right: 0;
1812
+ bottom: 0;
1813
+ background: rgba(0, 0, 0, 0.5);
1814
+ z-index: 999998;
1815
+ display: flex;
1816
+ align-items: center;
1817
+ justify-content: center;
1818
+ opacity: 0;
1819
+ transition: opacity 0.3s ease;
1820
+ `;
1821
+ // Create form container
1822
+ const container = document.createElement('div');
1823
+ container.id = `clianta-form-${form._id}`;
1824
+ const style = form.style || {};
1825
+ container.style.cssText = `
1826
+ background: ${style.backgroundColor || '#FFFFFF'};
1827
+ border-radius: ${style.borderRadius || 12}px;
1828
+ padding: 24px;
1829
+ max-width: 400px;
1830
+ width: 90%;
1831
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
1832
+ transform: translateY(20px);
1833
+ opacity: 0;
1834
+ transition: all 0.3s ease;
1835
+ `;
1836
+ // Build form using safe DOM APIs (no innerHTML for user content)
1837
+ this.buildFormDOM(form, container);
1838
+ overlay.appendChild(container);
1839
+ document.body.appendChild(overlay);
1840
+ // Animate in
1841
+ requestAnimationFrame(() => {
1842
+ overlay.style.opacity = '1';
1843
+ container.style.transform = 'translateY(0)';
1844
+ container.style.opacity = '1';
1845
+ });
1846
+ // Setup event listeners
1847
+ this.setupFormEvents(form, overlay, container);
1848
+ }
1849
+ /**
1850
+ * Escape HTML to prevent XSS - used only for static structure
1851
+ */
1852
+ escapeHTML(str) {
1853
+ const div = document.createElement('div');
1854
+ div.textContent = str;
1855
+ return div.innerHTML;
1856
+ }
1857
+ /**
1858
+ * Build form using safe DOM APIs (prevents XSS)
1859
+ */
1860
+ buildFormDOM(form, container) {
1861
+ const style = form.style || {};
1862
+ const primaryColor = style.primaryColor || '#10B981';
1863
+ const textColor = style.textColor || '#18181B';
1864
+ // Close button
1865
+ const closeBtn = document.createElement('button');
1866
+ closeBtn.id = 'clianta-form-close';
1867
+ closeBtn.style.cssText = `
1868
+ position: absolute;
1869
+ top: 12px;
1870
+ right: 12px;
1871
+ background: none;
1872
+ border: none;
1873
+ font-size: 20px;
1874
+ cursor: pointer;
1875
+ color: #71717A;
1876
+ padding: 4px;
1877
+ `;
1878
+ closeBtn.textContent = '×';
1879
+ container.appendChild(closeBtn);
1880
+ // Headline
1881
+ const headline = document.createElement('h2');
1882
+ headline.style.cssText = `font-size: 20px; font-weight: 700; margin-bottom: 8px; color: ${this.escapeHTML(textColor)};`;
1883
+ headline.textContent = form.headline || 'Stay in touch';
1884
+ container.appendChild(headline);
1885
+ // Subheadline
1886
+ const subheadline = document.createElement('p');
1887
+ subheadline.style.cssText = 'font-size: 14px; color: #71717A; margin-bottom: 16px;';
1888
+ subheadline.textContent = form.subheadline || 'Get the latest updates';
1889
+ container.appendChild(subheadline);
1890
+ // Form element
1891
+ const formElement = document.createElement('form');
1892
+ formElement.id = 'clianta-form-element';
1893
+ // Build fields
1894
+ form.fields.forEach(field => {
1895
+ const fieldWrapper = document.createElement('div');
1896
+ fieldWrapper.style.marginBottom = '12px';
1897
+ if (field.type === 'checkbox') {
1898
+ // Checkbox layout
1899
+ const label = document.createElement('label');
1900
+ label.style.cssText = `display: flex; align-items: center; gap: 8px; font-size: 14px; color: ${this.escapeHTML(textColor)}; cursor: pointer;`;
1901
+ const input = document.createElement('input');
1902
+ input.type = 'checkbox';
1903
+ input.name = field.name;
1904
+ if (field.required)
1905
+ input.required = true;
1906
+ input.style.cssText = 'width: 16px; height: 16px;';
1907
+ label.appendChild(input);
1908
+ const labelText = document.createTextNode(field.label + ' ');
1909
+ label.appendChild(labelText);
1910
+ if (field.required) {
1911
+ const requiredMark = document.createElement('span');
1912
+ requiredMark.style.color = '#EF4444';
1913
+ requiredMark.textContent = '*';
1914
+ label.appendChild(requiredMark);
1915
+ }
1916
+ fieldWrapper.appendChild(label);
1917
+ }
1918
+ else {
1919
+ // Label
1920
+ const label = document.createElement('label');
1921
+ label.style.cssText = `display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${this.escapeHTML(textColor)};`;
1922
+ label.textContent = field.label + ' ';
1923
+ if (field.required) {
1924
+ const requiredMark = document.createElement('span');
1925
+ requiredMark.style.color = '#EF4444';
1926
+ requiredMark.textContent = '*';
1927
+ label.appendChild(requiredMark);
1928
+ }
1929
+ fieldWrapper.appendChild(label);
1930
+ // Input/Textarea/Select
1931
+ if (field.type === 'textarea') {
1932
+ const textarea = document.createElement('textarea');
1933
+ textarea.name = field.name;
1934
+ if (field.placeholder)
1935
+ textarea.placeholder = field.placeholder;
1936
+ if (field.required)
1937
+ textarea.required = true;
1938
+ 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;';
1939
+ fieldWrapper.appendChild(textarea);
1940
+ }
1941
+ else if (field.type === 'select') {
1942
+ const select = document.createElement('select');
1943
+ select.name = field.name;
1944
+ if (field.required)
1945
+ select.required = true;
1946
+ select.style.cssText = 'width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box; background: white; cursor: pointer;';
1947
+ // Add placeholder option
1948
+ if (field.placeholder) {
1949
+ const placeholderOption = document.createElement('option');
1950
+ placeholderOption.value = '';
1951
+ placeholderOption.textContent = field.placeholder;
1952
+ placeholderOption.disabled = true;
1953
+ placeholderOption.selected = true;
1954
+ select.appendChild(placeholderOption);
1955
+ }
1956
+ // Add options from field.options array if provided
1957
+ if (field.options && Array.isArray(field.options)) {
1958
+ field.options.forEach((opt) => {
1959
+ const option = document.createElement('option');
1960
+ if (typeof opt === 'string') {
1961
+ option.value = opt;
1962
+ option.textContent = opt;
1963
+ }
1964
+ else {
1965
+ option.value = opt.value;
1966
+ option.textContent = opt.label;
1967
+ }
1968
+ select.appendChild(option);
1969
+ });
1970
+ }
1971
+ fieldWrapper.appendChild(select);
1972
+ }
1973
+ else {
1974
+ const input = document.createElement('input');
1975
+ input.type = field.type;
1976
+ input.name = field.name;
1977
+ if (field.placeholder)
1978
+ input.placeholder = field.placeholder;
1979
+ if (field.required)
1980
+ input.required = true;
1981
+ input.style.cssText = 'width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box;';
1982
+ fieldWrapper.appendChild(input);
1983
+ }
1984
+ }
1985
+ formElement.appendChild(fieldWrapper);
1986
+ });
1987
+ // Submit button
1988
+ const submitBtn = document.createElement('button');
1989
+ submitBtn.type = 'submit';
1990
+ submitBtn.style.cssText = `
1991
+ width: 100%;
1992
+ padding: 10px 16px;
1993
+ background: ${this.escapeHTML(primaryColor)};
1994
+ color: white;
1995
+ border: none;
1996
+ border-radius: 6px;
1997
+ font-size: 14px;
1998
+ font-weight: 500;
1999
+ cursor: pointer;
2000
+ margin-top: 8px;
2001
+ `;
2002
+ submitBtn.textContent = form.submitButtonText || 'Subscribe';
2003
+ formElement.appendChild(submitBtn);
2004
+ container.appendChild(formElement);
2005
+ }
2006
+ setupFormEvents(form, overlay, container) {
2007
+ // Close button
2008
+ const closeBtn = container.querySelector('#clianta-form-close');
2009
+ if (closeBtn) {
2010
+ closeBtn.addEventListener('click', () => this.closeForm(form._id, overlay, container));
2011
+ }
2012
+ // Overlay click
2013
+ overlay.addEventListener('click', (e) => {
2014
+ if (e.target === overlay) {
2015
+ this.closeForm(form._id, overlay, container);
2016
+ }
2017
+ });
2018
+ // Form submit
2019
+ const formElement = container.querySelector('#clianta-form-element');
2020
+ if (formElement) {
2021
+ formElement.addEventListener('submit', async (e) => {
2022
+ e.preventDefault();
2023
+ await this.handleSubmit(form, formElement, container);
2024
+ });
2025
+ }
2026
+ }
2027
+ closeForm(formId, overlay, container) {
2028
+ container.style.transform = 'translateY(20px)';
2029
+ container.style.opacity = '0';
2030
+ overlay.style.opacity = '0';
2031
+ setTimeout(() => {
2032
+ overlay.remove();
2033
+ }, 300);
2034
+ }
2035
+ async handleSubmit(form, formElement, container) {
2036
+ if (!this.tracker)
2037
+ return;
2038
+ const config = this.tracker.getConfig();
2039
+ const apiEndpoint = config.apiEndpoint || 'https://api.clianta.online';
2040
+ const visitorId = this.tracker.getVisitorId();
2041
+ // Collect form data
2042
+ const formData = new FormData(formElement);
2043
+ const data = {};
2044
+ formData.forEach((value, key) => {
2045
+ data[key] = value;
2046
+ });
2047
+ // Disable submit button
2048
+ const submitBtn = formElement.querySelector('button[type="submit"]');
2049
+ if (submitBtn) {
2050
+ submitBtn.disabled = true;
2051
+ submitBtn.innerHTML = 'Submitting...';
2052
+ }
2053
+ try {
2054
+ const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${form._id}/submit`, {
2055
+ method: 'POST',
2056
+ headers: { 'Content-Type': 'application/json' },
2057
+ body: JSON.stringify({
2058
+ visitorId,
2059
+ data,
2060
+ url: window.location.href,
2061
+ }),
2062
+ });
2063
+ const result = await response.json();
2064
+ if (result.success) {
2065
+ // Show success message using safe DOM APIs
2066
+ container.innerHTML = '';
2067
+ const successWrapper = document.createElement('div');
2068
+ successWrapper.style.cssText = 'text-align: center; padding: 20px;';
2069
+ const iconWrapper = document.createElement('div');
2070
+ iconWrapper.style.cssText = 'width: 48px; height: 48px; background: #10B981; border-radius: 50%; margin: 0 auto 16px; display: flex; align-items: center; justify-content: center;';
2071
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2072
+ svg.setAttribute('width', '24');
2073
+ svg.setAttribute('height', '24');
2074
+ svg.setAttribute('viewBox', '0 0 24 24');
2075
+ svg.setAttribute('fill', 'none');
2076
+ svg.setAttribute('stroke', 'white');
2077
+ svg.setAttribute('stroke-width', '2');
2078
+ const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
2079
+ polyline.setAttribute('points', '20 6 9 17 4 12');
2080
+ svg.appendChild(polyline);
2081
+ iconWrapper.appendChild(svg);
2082
+ const message = document.createElement('p');
2083
+ message.style.cssText = 'font-size: 16px; font-weight: 500; color: #18181B;';
2084
+ message.textContent = form.successMessage || 'Thank you!';
2085
+ successWrapper.appendChild(iconWrapper);
2086
+ successWrapper.appendChild(message);
2087
+ container.appendChild(successWrapper);
2088
+ // Track identify
2089
+ if (data.email) {
2090
+ this.tracker?.identify(data.email, data);
2091
+ }
2092
+ // Redirect if configured
2093
+ if (form.redirectUrl) {
2094
+ setTimeout(() => {
2095
+ window.location.href = form.redirectUrl;
2096
+ }, 1500);
2097
+ }
2098
+ // Close after delay
2099
+ setTimeout(() => {
2100
+ const overlay = document.getElementById(`clianta-form-overlay-${form._id}`);
2101
+ if (overlay) {
2102
+ this.closeForm(form._id, overlay, container);
2103
+ }
2104
+ }, 2000);
2105
+ }
2106
+ }
2107
+ catch (error) {
2108
+ console.error('[Clianta] Form submit error:', error);
2109
+ if (submitBtn) {
2110
+ submitBtn.disabled = false;
2111
+ submitBtn.innerHTML = form.submitButtonText || 'Subscribe';
2112
+ }
2113
+ }
2114
+ }
2115
+ }
2116
+
2117
+ /**
2118
+ * Clianta SDK - Plugins Index
2119
+ * Version is defined in core/config.ts as SDK_VERSION
2120
+ */
2121
+ /**
2122
+ * Get plugin instance by name
2123
+ */
2124
+ function getPlugin(name) {
2125
+ switch (name) {
2126
+ case 'pageView':
2127
+ return new PageViewPlugin();
2128
+ case 'scroll':
2129
+ return new ScrollPlugin();
2130
+ case 'forms':
2131
+ return new FormsPlugin();
2132
+ case 'clicks':
2133
+ return new ClicksPlugin();
2134
+ case 'engagement':
2135
+ return new EngagementPlugin();
2136
+ case 'downloads':
2137
+ return new DownloadsPlugin();
2138
+ case 'exitIntent':
2139
+ return new ExitIntentPlugin();
2140
+ case 'errors':
2141
+ return new ErrorsPlugin();
2142
+ case 'performance':
2143
+ return new PerformancePlugin();
2144
+ case 'popupForms':
2145
+ return new PopupFormsPlugin();
2146
+ default:
2147
+ throw new Error(`Unknown plugin: ${name}`);
2148
+ }
2149
+ }
2150
+
2151
+ /**
2152
+ * Clianta SDK - Consent Storage
2153
+ * Handles persistence of consent state
2154
+ * @see SDK_VERSION in core/config.ts
2155
+ */
2156
+ const CONSENT_VERSION = 1;
2157
+ /**
2158
+ * Save consent state to storage
2159
+ */
2160
+ function saveConsent(state) {
2161
+ try {
2162
+ if (typeof localStorage === 'undefined')
2163
+ return false;
2164
+ const stored = {
2165
+ state,
2166
+ timestamp: Date.now(),
2167
+ version: CONSENT_VERSION,
2168
+ };
2169
+ localStorage.setItem(STORAGE_KEYS.CONSENT, JSON.stringify(stored));
2170
+ return true;
2171
+ }
2172
+ catch {
2173
+ return false;
2174
+ }
2175
+ }
2176
+ /**
2177
+ * Load consent state from storage
2178
+ */
2179
+ function loadConsent() {
2180
+ try {
2181
+ if (typeof localStorage === 'undefined')
2182
+ return null;
2183
+ const stored = localStorage.getItem(STORAGE_KEYS.CONSENT);
2184
+ if (!stored)
2185
+ return null;
2186
+ const parsed = JSON.parse(stored);
2187
+ // Validate version
2188
+ if (parsed.version !== CONSENT_VERSION) {
2189
+ clearConsent();
2190
+ return null;
2191
+ }
2192
+ return parsed;
2193
+ }
2194
+ catch {
2195
+ return null;
2196
+ }
2197
+ }
2198
+ /**
2199
+ * Clear consent state from storage
2200
+ */
2201
+ function clearConsent() {
2202
+ try {
2203
+ if (typeof localStorage === 'undefined')
2204
+ return false;
2205
+ localStorage.removeItem(STORAGE_KEYS.CONSENT);
2206
+ return true;
2207
+ }
2208
+ catch {
2209
+ return false;
2210
+ }
2211
+ }
2212
+ /**
2213
+ * Check if consent has been explicitly set
2214
+ */
2215
+ function hasStoredConsent() {
2216
+ return loadConsent() !== null;
2217
+ }
2218
+
2219
+ /**
2220
+ * Clianta SDK - Consent Manager
2221
+ * Manages consent state and event buffering for GDPR/CCPA compliance
2222
+ * @see SDK_VERSION in core/config.ts
2223
+ */
2224
+ /** Maximum events to buffer while waiting for consent */
2225
+ const MAX_BUFFER_SIZE = 100;
2226
+ /**
2227
+ * Manages user consent state for tracking
2228
+ */
2229
+ class ConsentManager {
2230
+ constructor(config = {}) {
2231
+ this.eventBuffer = [];
2232
+ this.callbacks = [];
2233
+ this.hasExplicitConsent = false;
2234
+ this.config = {
2235
+ defaultConsent: { analytics: true, marketing: false, personalization: false },
2236
+ waitForConsent: false,
2237
+ storageKey: 'mb_consent',
2238
+ ...config,
2239
+ };
2240
+ // Load stored consent or use default
2241
+ const stored = loadConsent();
2242
+ if (stored) {
2243
+ this.state = stored.state;
2244
+ this.hasExplicitConsent = true;
2245
+ logger.debug('Loaded stored consent:', this.state);
2246
+ }
2247
+ else {
2248
+ this.state = this.config.defaultConsent || { analytics: true };
2249
+ this.hasExplicitConsent = false;
2250
+ logger.debug('Using default consent:', this.state);
2251
+ }
2252
+ // Register callback if provided
2253
+ if (config.onConsentChange) {
2254
+ this.callbacks.push(config.onConsentChange);
2255
+ }
2256
+ }
2257
+ /**
2258
+ * Grant consent for specified categories
2259
+ */
2260
+ grant(categories) {
2261
+ const previous = { ...this.state };
2262
+ this.state = { ...this.state, ...categories };
2263
+ this.hasExplicitConsent = true;
2264
+ saveConsent(this.state);
2265
+ logger.info('Consent granted:', categories);
2266
+ this.notifyChange(previous);
2267
+ }
2268
+ /**
2269
+ * Revoke consent for specified categories
2270
+ */
2271
+ revoke(categories) {
2272
+ const previous = { ...this.state };
2273
+ for (const category of categories) {
2274
+ this.state[category] = false;
2275
+ }
2276
+ this.hasExplicitConsent = true;
2277
+ saveConsent(this.state);
2278
+ logger.info('Consent revoked:', categories);
2279
+ this.notifyChange(previous);
2280
+ }
2281
+ /**
2282
+ * Update entire consent state
2283
+ */
2284
+ update(state) {
2285
+ const previous = { ...this.state };
2286
+ this.state = { ...state };
2287
+ this.hasExplicitConsent = true;
2288
+ saveConsent(this.state);
2289
+ logger.info('Consent updated:', this.state);
2290
+ this.notifyChange(previous);
2291
+ }
2292
+ /**
2293
+ * Reset consent to default (clear stored consent)
2294
+ */
2295
+ reset() {
2296
+ const previous = { ...this.state };
2297
+ this.state = this.config.defaultConsent || { analytics: true };
2298
+ this.hasExplicitConsent = false;
2299
+ this.eventBuffer = [];
2300
+ clearConsent();
2301
+ logger.info('Consent reset to defaults');
2302
+ this.notifyChange(previous);
2303
+ }
2304
+ /**
2305
+ * Get current consent state
2306
+ */
2307
+ getState() {
2308
+ return { ...this.state };
2309
+ }
2310
+ /**
2311
+ * Check if a specific consent category is granted
2312
+ */
2313
+ hasConsent(category) {
2314
+ return this.state[category] === true;
2315
+ }
2316
+ /**
2317
+ * Check if analytics consent is granted (most common check)
2318
+ */
2319
+ canTrack() {
2320
+ // If waiting for consent and no explicit consent given, cannot track
2321
+ if (this.config.waitForConsent && !this.hasExplicitConsent) {
2322
+ return false;
2323
+ }
2324
+ return this.state.analytics === true;
2325
+ }
2326
+ /**
2327
+ * Check if explicit consent has been given
2328
+ */
2329
+ hasExplicit() {
2330
+ return this.hasExplicitConsent;
2331
+ }
2332
+ /**
2333
+ * Check if there's stored consent
2334
+ */
2335
+ hasStored() {
2336
+ return hasStoredConsent();
2337
+ }
2338
+ /**
2339
+ * Buffer an event (for waitForConsent mode)
2340
+ */
2341
+ bufferEvent(event) {
2342
+ // Prevent unbounded buffer growth
2343
+ if (this.eventBuffer.length >= MAX_BUFFER_SIZE) {
2344
+ logger.warn('Consent event buffer full, dropping oldest event');
2345
+ this.eventBuffer.shift();
2346
+ }
2347
+ this.eventBuffer.push(event);
2348
+ logger.debug('Event buffered (waiting for consent):', event.eventName);
2349
+ }
2350
+ /**
2351
+ * Get and clear buffered events
2352
+ */
2353
+ flushBuffer() {
2354
+ const events = [...this.eventBuffer];
2355
+ this.eventBuffer = [];
2356
+ if (events.length > 0) {
2357
+ logger.debug(`Flushing ${events.length} buffered events`);
2358
+ }
2359
+ return events;
2360
+ }
2361
+ /**
2362
+ * Get buffered event count
2363
+ */
2364
+ getBufferSize() {
2365
+ return this.eventBuffer.length;
2366
+ }
2367
+ /**
2368
+ * Register a consent change callback
2369
+ */
2370
+ onChange(callback) {
2371
+ this.callbacks.push(callback);
2372
+ // Return unsubscribe function
2373
+ return () => {
2374
+ const index = this.callbacks.indexOf(callback);
2375
+ if (index > -1) {
2376
+ this.callbacks.splice(index, 1);
2377
+ }
2378
+ };
2379
+ }
2380
+ /**
2381
+ * Notify all callbacks of consent change
2382
+ */
2383
+ notifyChange(previous) {
2384
+ for (const callback of this.callbacks) {
2385
+ try {
2386
+ callback(this.state, previous);
2387
+ }
2388
+ catch (error) {
2389
+ logger.error('Consent change callback error:', error);
2390
+ }
2391
+ }
2392
+ }
2393
+ }
2394
+
2395
+ /**
2396
+ * Clianta SDK - Event Triggers Manager
2397
+ * Manages event-driven automation and email notifications
2398
+ */
2399
+ /**
2400
+ * Event Triggers Manager
2401
+ * Handles event-driven automation based on CRM actions
2402
+ *
2403
+ * Similar to:
2404
+ * - Salesforce: Process Builder, Flow Automation
2405
+ * - HubSpot: Workflows, Email Sequences
2406
+ * - Pipedrive: Workflow Automation
2407
+ */
2408
+ class EventTriggersManager {
2409
+ constructor(apiEndpoint, workspaceId, authToken) {
2410
+ this.triggers = new Map();
2411
+ this.listeners = new Map();
2412
+ this.apiEndpoint = apiEndpoint;
2413
+ this.workspaceId = workspaceId;
2414
+ this.authToken = authToken;
2415
+ }
2416
+ /**
2417
+ * Set authentication token
2418
+ */
2419
+ setAuthToken(token) {
2420
+ this.authToken = token;
2421
+ }
2422
+ /**
2423
+ * Make authenticated API request
2424
+ */
2425
+ async request(endpoint, options = {}) {
2426
+ const url = `${this.apiEndpoint}${endpoint}`;
2427
+ const headers = {
2428
+ 'Content-Type': 'application/json',
2429
+ ...(options.headers || {}),
2430
+ };
2431
+ if (this.authToken) {
2432
+ headers['Authorization'] = `Bearer ${this.authToken}`;
2433
+ }
2434
+ try {
2435
+ const response = await fetch(url, {
2436
+ ...options,
2437
+ headers,
2438
+ });
2439
+ const data = await response.json();
2440
+ if (!response.ok) {
2441
+ return {
2442
+ success: false,
2443
+ error: data.message || 'Request failed',
2444
+ status: response.status,
2445
+ };
2446
+ }
2447
+ return {
2448
+ success: true,
2449
+ data: data.data || data,
2450
+ status: response.status,
2451
+ };
2452
+ }
2453
+ catch (error) {
2454
+ return {
2455
+ success: false,
2456
+ error: error instanceof Error ? error.message : 'Network error',
2457
+ status: 0,
2458
+ };
2459
+ }
2460
+ }
2461
+ // ============================================
2462
+ // TRIGGER MANAGEMENT
2463
+ // ============================================
2464
+ /**
2465
+ * Get all event triggers
2466
+ */
2467
+ async getTriggers() {
2468
+ return this.request(`/api/workspaces/${this.workspaceId}/triggers`);
2469
+ }
2470
+ /**
2471
+ * Get a single trigger by ID
2472
+ */
2473
+ async getTrigger(triggerId) {
2474
+ return this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`);
2475
+ }
2476
+ /**
2477
+ * Create a new event trigger
2478
+ */
2479
+ async createTrigger(trigger) {
2480
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers`, {
2481
+ method: 'POST',
2482
+ body: JSON.stringify(trigger),
2483
+ });
2484
+ // Cache the trigger locally if successful
2485
+ if (result.success && result.data?._id) {
2486
+ this.triggers.set(result.data._id, result.data);
2487
+ }
2488
+ return result;
2489
+ }
2490
+ /**
2491
+ * Update an existing trigger
2492
+ */
2493
+ async updateTrigger(triggerId, updates) {
2494
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2495
+ method: 'PUT',
2496
+ body: JSON.stringify(updates),
2497
+ });
2498
+ // Update cache if successful
2499
+ if (result.success && result.data?._id) {
2500
+ this.triggers.set(result.data._id, result.data);
2501
+ }
2502
+ return result;
2503
+ }
2504
+ /**
2505
+ * Delete a trigger
2506
+ */
2507
+ async deleteTrigger(triggerId) {
2508
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2509
+ method: 'DELETE',
2510
+ });
2511
+ // Remove from cache if successful
2512
+ if (result.success) {
2513
+ this.triggers.delete(triggerId);
2514
+ }
2515
+ return result;
2516
+ }
2517
+ /**
2518
+ * Activate a trigger
2519
+ */
2520
+ async activateTrigger(triggerId) {
2521
+ return this.updateTrigger(triggerId, { isActive: true });
2522
+ }
2523
+ /**
2524
+ * Deactivate a trigger
2525
+ */
2526
+ async deactivateTrigger(triggerId) {
2527
+ return this.updateTrigger(triggerId, { isActive: false });
2528
+ }
2529
+ // ============================================
2530
+ // EVENT HANDLING (CLIENT-SIDE)
2531
+ // ============================================
2532
+ /**
2533
+ * Register a local event listener for client-side triggers
2534
+ * This allows immediate client-side reactions to events
2535
+ */
2536
+ on(eventType, callback) {
2537
+ if (!this.listeners.has(eventType)) {
2538
+ this.listeners.set(eventType, new Set());
2539
+ }
2540
+ this.listeners.get(eventType).add(callback);
2541
+ logger.debug(`Event listener registered: ${eventType}`);
2542
+ }
2543
+ /**
2544
+ * Remove an event listener
2545
+ */
2546
+ off(eventType, callback) {
2547
+ const listeners = this.listeners.get(eventType);
2548
+ if (listeners) {
2549
+ listeners.delete(callback);
2550
+ }
2551
+ }
2552
+ /**
2553
+ * Emit an event (client-side only)
2554
+ * This will trigger any registered local listeners
2555
+ */
2556
+ emit(eventType, data) {
2557
+ logger.debug(`Event emitted: ${eventType}`, data);
2558
+ const listeners = this.listeners.get(eventType);
2559
+ if (listeners) {
2560
+ listeners.forEach(callback => {
2561
+ try {
2562
+ callback(data);
2563
+ }
2564
+ catch (error) {
2565
+ logger.error(`Error in event listener for ${eventType}:`, error);
2566
+ }
2567
+ });
2568
+ }
2569
+ }
2570
+ /**
2571
+ * Check if conditions are met for a trigger
2572
+ * Supports dynamic field evaluation including custom fields and nested paths
2573
+ */
2574
+ evaluateConditions(conditions, data) {
2575
+ if (!conditions || conditions.length === 0) {
2576
+ return true; // No conditions means always fire
2577
+ }
2578
+ return conditions.every(condition => {
2579
+ // Support dot notation for nested fields (e.g., 'customFields.industry')
2580
+ const fieldValue = condition.field.includes('.')
2581
+ ? this.getNestedValue(data, condition.field)
2582
+ : data[condition.field];
2583
+ const targetValue = condition.value;
2584
+ switch (condition.operator) {
2585
+ case 'equals':
2586
+ return fieldValue === targetValue;
2587
+ case 'not_equals':
2588
+ return fieldValue !== targetValue;
2589
+ case 'contains':
2590
+ return String(fieldValue).includes(String(targetValue));
2591
+ case 'greater_than':
2592
+ return Number(fieldValue) > Number(targetValue);
2593
+ case 'less_than':
2594
+ return Number(fieldValue) < Number(targetValue);
2595
+ case 'in':
2596
+ return Array.isArray(targetValue) && targetValue.includes(fieldValue);
2597
+ case 'not_in':
2598
+ return Array.isArray(targetValue) && !targetValue.includes(fieldValue);
2599
+ default:
2600
+ return false;
2601
+ }
2602
+ });
2603
+ }
2604
+ /**
2605
+ * Execute actions for a triggered event (client-side preview)
2606
+ * Note: Actual execution happens on the backend
2607
+ */
2608
+ async executeActions(trigger, data) {
2609
+ logger.info(`Executing actions for trigger: ${trigger.name}`);
2610
+ for (const action of trigger.actions) {
2611
+ try {
2612
+ await this.executeAction(action, data);
2613
+ }
2614
+ catch (error) {
2615
+ logger.error(`Failed to execute action:`, error);
2616
+ }
2617
+ }
2618
+ }
2619
+ /**
2620
+ * Execute a single action
2621
+ */
2622
+ async executeAction(action, data) {
2623
+ switch (action.type) {
2624
+ case 'send_email':
2625
+ await this.executeSendEmail(action, data);
2626
+ break;
2627
+ case 'webhook':
2628
+ await this.executeWebhook(action, data);
2629
+ break;
2630
+ case 'create_task':
2631
+ await this.executeCreateTask(action, data);
2632
+ break;
2633
+ case 'update_contact':
2634
+ await this.executeUpdateContact(action, data);
2635
+ break;
2636
+ default:
2637
+ logger.warn(`Unknown action type:`, action);
2638
+ }
2639
+ }
2640
+ /**
2641
+ * Execute send email action (via backend API)
2642
+ */
2643
+ async executeSendEmail(action, data) {
2644
+ logger.debug('Sending email:', action);
2645
+ const payload = {
2646
+ to: this.replaceVariables(action.to, data),
2647
+ subject: action.subject ? this.replaceVariables(action.subject, data) : undefined,
2648
+ body: action.body ? this.replaceVariables(action.body, data) : undefined,
2649
+ templateId: action.templateId,
2650
+ cc: action.cc,
2651
+ bcc: action.bcc,
2652
+ from: action.from,
2653
+ delayMinutes: action.delayMinutes,
2654
+ };
2655
+ await this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
2656
+ method: 'POST',
2657
+ body: JSON.stringify(payload),
2658
+ });
2659
+ }
2660
+ /**
2661
+ * Execute webhook action
2662
+ */
2663
+ async executeWebhook(action, data) {
2664
+ logger.debug('Calling webhook:', action.url);
2665
+ const body = action.body ? this.replaceVariables(action.body, data) : JSON.stringify(data);
2666
+ await fetch(action.url, {
2667
+ method: action.method,
2668
+ headers: {
2669
+ 'Content-Type': 'application/json',
2670
+ ...action.headers,
2671
+ },
2672
+ body,
2673
+ });
2674
+ }
2675
+ /**
2676
+ * Execute create task action
2677
+ */
2678
+ async executeCreateTask(action, data) {
2679
+ logger.debug('Creating task:', action.title);
2680
+ const dueDate = action.dueDays
2681
+ ? new Date(Date.now() + action.dueDays * 24 * 60 * 60 * 1000).toISOString()
2682
+ : undefined;
2683
+ await this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
2684
+ method: 'POST',
2685
+ body: JSON.stringify({
2686
+ title: this.replaceVariables(action.title, data),
2687
+ description: action.description ? this.replaceVariables(action.description, data) : undefined,
2688
+ priority: action.priority,
2689
+ dueDate,
2690
+ assignedTo: action.assignedTo,
2691
+ relatedContactId: typeof data.contactId === 'string' ? data.contactId : undefined,
2692
+ }),
2693
+ });
2694
+ }
2695
+ /**
2696
+ * Execute update contact action
2697
+ */
2698
+ async executeUpdateContact(action, data) {
2699
+ const contactId = data.contactId || data._id;
2700
+ if (!contactId) {
2701
+ logger.warn('Cannot update contact: no contactId in data');
2702
+ return;
2703
+ }
2704
+ logger.debug('Updating contact:', contactId);
2705
+ await this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2706
+ method: 'PUT',
2707
+ body: JSON.stringify(action.updates),
2708
+ });
2709
+ }
2710
+ /**
2711
+ * Replace variables in a string template
2712
+ * Supports syntax like {{contact.email}}, {{opportunity.value}}
2713
+ */
2714
+ replaceVariables(template, data) {
2715
+ return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
2716
+ const value = this.getNestedValue(data, path.trim());
2717
+ return value !== undefined ? String(value) : match;
2718
+ });
2719
+ }
2720
+ /**
2721
+ * Get nested value from object using dot notation
2722
+ * Supports dynamic field access including custom fields
2723
+ */
2724
+ getNestedValue(obj, path) {
2725
+ return path.split('.').reduce((current, key) => {
2726
+ return current !== null && current !== undefined && typeof current === 'object'
2727
+ ? current[key]
2728
+ : undefined;
2729
+ }, obj);
2730
+ }
2731
+ /**
2732
+ * Extract all available field paths from a data object
2733
+ * Useful for dynamic field discovery based on platform-specific attributes
2734
+ * @param obj - The data object to extract fields from
2735
+ * @param prefix - Internal use for nested paths
2736
+ * @param maxDepth - Maximum depth to traverse (default: 3)
2737
+ * @returns Array of field paths (e.g., ['email', 'contact.firstName', 'customFields.industry'])
2738
+ */
2739
+ extractAvailableFields(obj, prefix = '', maxDepth = 3) {
2740
+ if (maxDepth <= 0)
2741
+ return [];
2742
+ const fields = [];
2743
+ for (const key in obj) {
2744
+ if (!obj.hasOwnProperty(key))
2745
+ continue;
2746
+ const value = obj[key];
2747
+ const fieldPath = prefix ? `${prefix}.${key}` : key;
2748
+ fields.push(fieldPath);
2749
+ // Recursively traverse nested objects
2750
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
2751
+ const nestedFields = this.extractAvailableFields(value, fieldPath, maxDepth - 1);
2752
+ fields.push(...nestedFields);
2753
+ }
2754
+ }
2755
+ return fields;
2756
+ }
2757
+ /**
2758
+ * Get available fields from sample data
2759
+ * Helps with dynamic field detection for platform-specific attributes
2760
+ * @param sampleData - Sample data object to analyze
2761
+ * @returns Array of available field paths
2762
+ */
2763
+ getAvailableFields(sampleData) {
2764
+ return this.extractAvailableFields(sampleData);
2765
+ }
2766
+ // ============================================
2767
+ // HELPER METHODS FOR COMMON PATTERNS
2768
+ // ============================================
2769
+ /**
2770
+ * Create a simple email trigger
2771
+ * Helper method for common use case
2772
+ */
2773
+ async createEmailTrigger(config) {
2774
+ return this.createTrigger({
2775
+ name: config.name,
2776
+ eventType: config.eventType,
2777
+ conditions: config.conditions,
2778
+ actions: [
2779
+ {
2780
+ type: 'send_email',
2781
+ to: config.to,
2782
+ subject: config.subject,
2783
+ body: config.body,
2784
+ },
2785
+ ],
2786
+ isActive: true,
2787
+ });
2788
+ }
2789
+ /**
2790
+ * Create a task creation trigger
2791
+ */
2792
+ async createTaskTrigger(config) {
2793
+ return this.createTrigger({
2794
+ name: config.name,
2795
+ eventType: config.eventType,
2796
+ conditions: config.conditions,
2797
+ actions: [
2798
+ {
2799
+ type: 'create_task',
2800
+ title: config.taskTitle,
2801
+ description: config.taskDescription,
2802
+ priority: config.priority,
2803
+ dueDays: config.dueDays,
2804
+ },
2805
+ ],
2806
+ isActive: true,
2807
+ });
2808
+ }
2809
+ /**
2810
+ * Create a webhook trigger
2811
+ */
2812
+ async createWebhookTrigger(config) {
2813
+ return this.createTrigger({
2814
+ name: config.name,
2815
+ eventType: config.eventType,
2816
+ conditions: config.conditions,
2817
+ actions: [
2818
+ {
2819
+ type: 'webhook',
2820
+ url: config.webhookUrl,
2821
+ method: config.method || 'POST',
2822
+ },
2823
+ ],
2824
+ isActive: true,
2825
+ });
2826
+ }
2827
+ }
2828
+
2829
+ /**
2830
+ * Clianta SDK - CRM API Client
2831
+ * @see SDK_VERSION in core/config.ts
2832
+ */
2833
+ /**
2834
+ * CRM API Client for managing contacts and opportunities
2835
+ */
2836
+ class CRMClient {
2837
+ constructor(apiEndpoint, workspaceId, authToken, apiKey) {
2838
+ this.apiEndpoint = apiEndpoint;
2839
+ this.workspaceId = workspaceId;
2840
+ this.authToken = authToken;
2841
+ this.apiKey = apiKey;
2842
+ this.triggers = new EventTriggersManager(apiEndpoint, workspaceId, authToken);
2843
+ }
2844
+ /**
2845
+ * Set authentication token for API requests (user JWT)
2846
+ */
2847
+ setAuthToken(token) {
2848
+ this.authToken = token;
2849
+ this.apiKey = undefined;
2850
+ this.triggers.setAuthToken(token);
2851
+ }
2852
+ /**
2853
+ * Set workspace API key for server-to-server requests.
2854
+ * Use this instead of setAuthToken when integrating from an external app.
2855
+ */
2856
+ setApiKey(key) {
2857
+ this.apiKey = key;
2858
+ this.authToken = undefined;
2859
+ }
2860
+ /**
2861
+ * Validate required parameter exists
2862
+ * @throws {Error} if value is null/undefined or empty string
2863
+ */
2864
+ validateRequired(param, value, methodName) {
2865
+ if (value === null || value === undefined || value === '') {
2866
+ throw new Error(`[CRMClient.${methodName}] ${param} is required`);
2867
+ }
2868
+ }
2869
+ /**
2870
+ * Make authenticated API request
2871
+ */
2872
+ async request(endpoint, options = {}) {
2873
+ const url = `${this.apiEndpoint}${endpoint}`;
2874
+ const headers = {
2875
+ 'Content-Type': 'application/json',
2876
+ ...(options.headers || {}),
2877
+ };
2878
+ if (this.apiKey) {
2879
+ headers['X-Api-Key'] = this.apiKey;
2880
+ }
2881
+ else if (this.authToken) {
2882
+ headers['Authorization'] = `Bearer ${this.authToken}`;
2883
+ }
2884
+ try {
2885
+ const response = await fetch(url, {
2886
+ ...options,
2887
+ headers,
2888
+ });
2889
+ const data = await response.json();
2890
+ if (!response.ok) {
2891
+ return {
2892
+ success: false,
2893
+ error: data.message || 'Request failed',
2894
+ status: response.status,
2895
+ };
2896
+ }
2897
+ return {
2898
+ success: true,
2899
+ data: data.data || data,
2900
+ status: response.status,
2901
+ };
2902
+ }
2903
+ catch (error) {
2904
+ return {
2905
+ success: false,
2906
+ error: error instanceof Error ? error.message : 'Network error',
2907
+ status: 0,
2908
+ };
2909
+ }
2910
+ }
2911
+ // ============================================
2912
+ // INBOUND EVENTS API (API-key authenticated)
2913
+ // ============================================
2914
+ /**
2915
+ * Send an inbound event from an external app (e.g. user signup on client website).
2916
+ * Requires the client to be initialized with an API key via setApiKey() or the constructor.
2917
+ *
2918
+ * The contact is upserted in the CRM and matching workflow automations fire automatically.
2919
+ *
2920
+ * @example
2921
+ * const crm = new CRMClient('https://api.clianta.online', 'WORKSPACE_ID');
2922
+ * crm.setApiKey('mm_live_...');
2923
+ *
2924
+ * await crm.sendEvent({
2925
+ * event: 'user.registered',
2926
+ * contact: { email: 'alice@example.com', firstName: 'Alice' },
2927
+ * data: { plan: 'free', signupSource: 'homepage' },
2928
+ * });
2929
+ */
2930
+ async sendEvent(payload) {
2931
+ const url = `${this.apiEndpoint}/api/public/events`;
2932
+ const headers = { 'Content-Type': 'application/json' };
2933
+ if (this.apiKey) {
2934
+ headers['X-Api-Key'] = this.apiKey;
2935
+ }
2936
+ else if (this.authToken) {
2937
+ headers['Authorization'] = `Bearer ${this.authToken}`;
2938
+ }
2939
+ try {
2940
+ const response = await fetch(url, {
2941
+ method: 'POST',
2942
+ headers,
2943
+ body: JSON.stringify(payload),
2944
+ });
2945
+ const data = await response.json();
2946
+ if (!response.ok) {
2947
+ return {
2948
+ success: false,
2949
+ contactCreated: false,
2950
+ event: payload.event,
2951
+ error: data.error || 'Request failed',
2952
+ };
2953
+ }
2954
+ return {
2955
+ success: data.success,
2956
+ contactCreated: data.contactCreated,
2957
+ contactId: data.contactId,
2958
+ event: data.event,
2959
+ };
2960
+ }
2961
+ catch (error) {
2962
+ return {
2963
+ success: false,
2964
+ contactCreated: false,
2965
+ event: payload.event,
2966
+ error: error instanceof Error ? error.message : 'Network error',
2967
+ };
2968
+ }
2969
+ }
2970
+ // ============================================
2971
+ // CONTACTS API
2972
+ // ============================================
2973
+ /**
2974
+ * Get all contacts with pagination
2975
+ */
2976
+ async getContacts(params) {
2977
+ const queryParams = new URLSearchParams();
2978
+ if (params?.page)
2979
+ queryParams.set('page', params.page.toString());
2980
+ if (params?.limit)
2981
+ queryParams.set('limit', params.limit.toString());
2982
+ if (params?.search)
2983
+ queryParams.set('search', params.search);
2984
+ if (params?.status)
2985
+ queryParams.set('status', params.status);
2986
+ const query = queryParams.toString();
2987
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts${query ? `?${query}` : ''}`;
2988
+ return this.request(endpoint);
2989
+ }
2990
+ /**
2991
+ * Get a single contact by ID
2992
+ */
2993
+ async getContact(contactId) {
2994
+ this.validateRequired('contactId', contactId, 'getContact');
2995
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
2996
+ }
2997
+ /**
2998
+ * Create a new contact
2999
+ */
3000
+ async createContact(contact) {
3001
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts`, {
3002
+ method: 'POST',
3003
+ body: JSON.stringify(contact),
3004
+ });
3005
+ }
3006
+ /**
3007
+ * Update an existing contact
3008
+ */
3009
+ async updateContact(contactId, updates) {
3010
+ this.validateRequired('contactId', contactId, 'updateContact');
3011
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
3012
+ method: 'PUT',
3013
+ body: JSON.stringify(updates),
3014
+ });
3015
+ }
3016
+ /**
3017
+ * Delete a contact
3018
+ */
3019
+ async deleteContact(contactId) {
3020
+ this.validateRequired('contactId', contactId, 'deleteContact');
3021
+ return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
3022
+ method: 'DELETE',
3023
+ });
3024
+ }
3025
+ // ============================================
3026
+ // OPPORTUNITIES API
3027
+ // ============================================
3028
+ /**
3029
+ * Get all opportunities with pagination
3030
+ */
3031
+ async getOpportunities(params) {
3032
+ const queryParams = new URLSearchParams();
3033
+ if (params?.page)
3034
+ queryParams.set('page', params.page.toString());
3035
+ if (params?.limit)
3036
+ queryParams.set('limit', params.limit.toString());
3037
+ if (params?.pipelineId)
3038
+ queryParams.set('pipelineId', params.pipelineId);
3039
+ if (params?.stageId)
3040
+ queryParams.set('stageId', params.stageId);
3041
+ const query = queryParams.toString();
3042
+ const endpoint = `/api/workspaces/${this.workspaceId}/opportunities${query ? `?${query}` : ''}`;
3043
+ return this.request(endpoint);
3044
+ }
3045
+ /**
3046
+ * Get a single opportunity by ID
3047
+ */
3048
+ async getOpportunity(opportunityId) {
3049
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`);
3050
+ }
3051
+ /**
3052
+ * Create a new opportunity
3053
+ */
3054
+ async createOpportunity(opportunity) {
3055
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities`, {
3056
+ method: 'POST',
3057
+ body: JSON.stringify(opportunity),
3058
+ });
3059
+ }
3060
+ /**
3061
+ * Update an existing opportunity
3062
+ */
3063
+ async updateOpportunity(opportunityId, updates) {
3064
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
3065
+ method: 'PUT',
3066
+ body: JSON.stringify(updates),
3067
+ });
3068
+ }
3069
+ /**
3070
+ * Delete an opportunity
3071
+ */
3072
+ async deleteOpportunity(opportunityId) {
3073
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
3074
+ method: 'DELETE',
3075
+ });
3076
+ }
3077
+ /**
3078
+ * Move opportunity to a different stage
3079
+ */
3080
+ async moveOpportunity(opportunityId, stageId) {
3081
+ return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/move`, {
3082
+ method: 'POST',
3083
+ body: JSON.stringify({ stageId }),
3084
+ });
3085
+ }
3086
+ // ============================================
3087
+ // COMPANIES API
3088
+ // ============================================
3089
+ /**
3090
+ * Get all companies with pagination
3091
+ */
3092
+ async getCompanies(params) {
3093
+ const queryParams = new URLSearchParams();
3094
+ if (params?.page)
3095
+ queryParams.set('page', params.page.toString());
3096
+ if (params?.limit)
3097
+ queryParams.set('limit', params.limit.toString());
3098
+ if (params?.search)
3099
+ queryParams.set('search', params.search);
3100
+ if (params?.status)
3101
+ queryParams.set('status', params.status);
3102
+ if (params?.industry)
3103
+ queryParams.set('industry', params.industry);
3104
+ const query = queryParams.toString();
3105
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies${query ? `?${query}` : ''}`;
3106
+ return this.request(endpoint);
3107
+ }
3108
+ /**
3109
+ * Get a single company by ID
3110
+ */
3111
+ async getCompany(companyId) {
3112
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`);
3113
+ }
3114
+ /**
3115
+ * Create a new company
3116
+ */
3117
+ async createCompany(company) {
3118
+ return this.request(`/api/workspaces/${this.workspaceId}/companies`, {
3119
+ method: 'POST',
3120
+ body: JSON.stringify(company),
3121
+ });
3122
+ }
3123
+ /**
3124
+ * Update an existing company
3125
+ */
3126
+ async updateCompany(companyId, updates) {
3127
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
3128
+ method: 'PUT',
3129
+ body: JSON.stringify(updates),
3130
+ });
3131
+ }
3132
+ /**
3133
+ * Delete a company
3134
+ */
3135
+ async deleteCompany(companyId) {
3136
+ return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
3137
+ method: 'DELETE',
3138
+ });
3139
+ }
3140
+ /**
3141
+ * Get contacts belonging to a company
3142
+ */
3143
+ async getCompanyContacts(companyId, params) {
3144
+ const queryParams = new URLSearchParams();
3145
+ if (params?.page)
3146
+ queryParams.set('page', params.page.toString());
3147
+ if (params?.limit)
3148
+ queryParams.set('limit', params.limit.toString());
3149
+ const query = queryParams.toString();
3150
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/contacts${query ? `?${query}` : ''}`;
3151
+ return this.request(endpoint);
3152
+ }
3153
+ /**
3154
+ * Get deals/opportunities belonging to a company
3155
+ */
3156
+ async getCompanyDeals(companyId, params) {
3157
+ const queryParams = new URLSearchParams();
3158
+ if (params?.page)
3159
+ queryParams.set('page', params.page.toString());
3160
+ if (params?.limit)
3161
+ queryParams.set('limit', params.limit.toString());
3162
+ const query = queryParams.toString();
3163
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/deals${query ? `?${query}` : ''}`;
3164
+ return this.request(endpoint);
3165
+ }
3166
+ // ============================================
3167
+ // PIPELINES API
3168
+ // ============================================
3169
+ /**
3170
+ * Get all pipelines
3171
+ */
3172
+ async getPipelines() {
3173
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines`);
3174
+ }
3175
+ /**
3176
+ * Get a single pipeline by ID
3177
+ */
3178
+ async getPipeline(pipelineId) {
3179
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`);
3180
+ }
3181
+ /**
3182
+ * Create a new pipeline
3183
+ */
3184
+ async createPipeline(pipeline) {
3185
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines`, {
3186
+ method: 'POST',
3187
+ body: JSON.stringify(pipeline),
3188
+ });
3189
+ }
3190
+ /**
3191
+ * Update an existing pipeline
3192
+ */
3193
+ async updatePipeline(pipelineId, updates) {
3194
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
3195
+ method: 'PUT',
3196
+ body: JSON.stringify(updates),
3197
+ });
3198
+ }
3199
+ /**
3200
+ * Delete a pipeline
3201
+ */
3202
+ async deletePipeline(pipelineId) {
3203
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
3204
+ method: 'DELETE',
3205
+ });
3206
+ }
3207
+ // ============================================
3208
+ // TASKS API
3209
+ // ============================================
3210
+ /**
3211
+ * Get all tasks with pagination
3212
+ */
3213
+ async getTasks(params) {
3214
+ const queryParams = new URLSearchParams();
3215
+ if (params?.page)
3216
+ queryParams.set('page', params.page.toString());
3217
+ if (params?.limit)
3218
+ queryParams.set('limit', params.limit.toString());
3219
+ if (params?.status)
3220
+ queryParams.set('status', params.status);
3221
+ if (params?.priority)
3222
+ queryParams.set('priority', params.priority);
3223
+ if (params?.contactId)
3224
+ queryParams.set('contactId', params.contactId);
3225
+ if (params?.companyId)
3226
+ queryParams.set('companyId', params.companyId);
3227
+ if (params?.opportunityId)
3228
+ queryParams.set('opportunityId', params.opportunityId);
3229
+ const query = queryParams.toString();
3230
+ const endpoint = `/api/workspaces/${this.workspaceId}/tasks${query ? `?${query}` : ''}`;
3231
+ return this.request(endpoint);
3232
+ }
3233
+ /**
3234
+ * Get a single task by ID
3235
+ */
3236
+ async getTask(taskId) {
3237
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`);
3238
+ }
3239
+ /**
3240
+ * Create a new task
3241
+ */
3242
+ async createTask(task) {
3243
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
3244
+ method: 'POST',
3245
+ body: JSON.stringify(task),
3246
+ });
3247
+ }
3248
+ /**
3249
+ * Update an existing task
3250
+ */
3251
+ async updateTask(taskId, updates) {
3252
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
3253
+ method: 'PUT',
3254
+ body: JSON.stringify(updates),
3255
+ });
3256
+ }
3257
+ /**
3258
+ * Mark a task as completed
3259
+ */
3260
+ async completeTask(taskId) {
3261
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}/complete`, {
3262
+ method: 'PATCH',
3263
+ });
3264
+ }
3265
+ /**
3266
+ * Delete a task
3267
+ */
3268
+ async deleteTask(taskId) {
3269
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
3270
+ method: 'DELETE',
3271
+ });
3272
+ }
3273
+ // ============================================
3274
+ // ACTIVITIES API
3275
+ // ============================================
3276
+ /**
3277
+ * Get activities for a contact
3278
+ */
3279
+ async getContactActivities(contactId, params) {
3280
+ const queryParams = new URLSearchParams();
3281
+ if (params?.page)
3282
+ queryParams.set('page', params.page.toString());
3283
+ if (params?.limit)
3284
+ queryParams.set('limit', params.limit.toString());
3285
+ if (params?.type)
3286
+ queryParams.set('type', params.type);
3287
+ const query = queryParams.toString();
3288
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
3289
+ return this.request(endpoint);
3290
+ }
3291
+ /**
3292
+ * Get activities for an opportunity/deal
3293
+ */
3294
+ async getOpportunityActivities(opportunityId, params) {
3295
+ const queryParams = new URLSearchParams();
3296
+ if (params?.page)
3297
+ queryParams.set('page', params.page.toString());
3298
+ if (params?.limit)
3299
+ queryParams.set('limit', params.limit.toString());
3300
+ if (params?.type)
3301
+ queryParams.set('type', params.type);
3302
+ const query = queryParams.toString();
3303
+ const endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/activities${query ? `?${query}` : ''}`;
3304
+ return this.request(endpoint);
3305
+ }
3306
+ /**
3307
+ * Create a new activity
3308
+ */
3309
+ async createActivity(activity) {
3310
+ // Determine the correct endpoint based on related entity
3311
+ let endpoint;
3312
+ if (activity.opportunityId) {
3313
+ endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${activity.opportunityId}/activities`;
3314
+ }
3315
+ else if (activity.contactId) {
3316
+ endpoint = `/api/workspaces/${this.workspaceId}/contacts/${activity.contactId}/activities`;
3317
+ }
3318
+ else {
3319
+ endpoint = `/api/workspaces/${this.workspaceId}/activities`;
3320
+ }
3321
+ return this.request(endpoint, {
3322
+ method: 'POST',
3323
+ body: JSON.stringify(activity),
3324
+ });
3325
+ }
3326
+ /**
3327
+ * Update an existing activity
3328
+ */
3329
+ async updateActivity(activityId, updates) {
3330
+ return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3331
+ method: 'PATCH',
3332
+ body: JSON.stringify(updates),
3333
+ });
3334
+ }
3335
+ /**
3336
+ * Delete an activity
3337
+ */
3338
+ async deleteActivity(activityId) {
3339
+ return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3340
+ method: 'DELETE',
3341
+ });
3342
+ }
3343
+ /**
3344
+ * Log a call activity
3345
+ */
3346
+ async logCall(data) {
3347
+ return this.createActivity({
3348
+ type: 'call',
3349
+ title: `${data.direction === 'inbound' ? 'Inbound' : 'Outbound'} Call`,
3350
+ direction: data.direction,
3351
+ duration: data.duration,
3352
+ outcome: data.outcome,
3353
+ description: data.notes,
3354
+ contactId: data.contactId,
3355
+ opportunityId: data.opportunityId,
3356
+ });
3357
+ }
3358
+ /**
3359
+ * Log a meeting activity
3360
+ */
3361
+ async logMeeting(data) {
3362
+ return this.createActivity({
3363
+ type: 'meeting',
3364
+ title: data.title,
3365
+ duration: data.duration,
3366
+ outcome: data.outcome,
3367
+ description: data.notes,
3368
+ contactId: data.contactId,
3369
+ opportunityId: data.opportunityId,
3370
+ });
3371
+ }
3372
+ /**
3373
+ * Add a note to a contact or opportunity
3374
+ */
3375
+ async addNote(data) {
3376
+ return this.createActivity({
3377
+ type: 'note',
3378
+ title: 'Note',
3379
+ description: data.content,
3380
+ contactId: data.contactId,
3381
+ opportunityId: data.opportunityId,
3382
+ });
3383
+ }
3384
+ // ============================================
3385
+ // EMAIL TEMPLATES API
3386
+ // ============================================
3387
+ /**
3388
+ * Get all email templates
3389
+ */
3390
+ async getEmailTemplates(params) {
3391
+ const queryParams = new URLSearchParams();
3392
+ if (params?.page)
3393
+ queryParams.set('page', params.page.toString());
3394
+ if (params?.limit)
3395
+ queryParams.set('limit', params.limit.toString());
3396
+ const query = queryParams.toString();
3397
+ const endpoint = `/api/workspaces/${this.workspaceId}/email-templates${query ? `?${query}` : ''}`;
3398
+ return this.request(endpoint);
3399
+ }
3400
+ /**
3401
+ * Get a single email template by ID
3402
+ */
3403
+ async getEmailTemplate(templateId) {
3404
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`);
3405
+ }
3406
+ /**
3407
+ * Create a new email template
3408
+ */
3409
+ async createEmailTemplate(template) {
3410
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates`, {
3411
+ method: 'POST',
3412
+ body: JSON.stringify(template),
3413
+ });
3414
+ }
3415
+ /**
3416
+ * Update an email template
3417
+ */
3418
+ async updateEmailTemplate(templateId, updates) {
3419
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3420
+ method: 'PUT',
3421
+ body: JSON.stringify(updates),
3422
+ });
3423
+ }
3424
+ /**
3425
+ * Delete an email template
3426
+ */
3427
+ async deleteEmailTemplate(templateId) {
3428
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3429
+ method: 'DELETE',
3430
+ });
3431
+ }
3432
+ /**
3433
+ * Send an email using a template
3434
+ */
3435
+ async sendEmail(data) {
3436
+ return this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
3437
+ method: 'POST',
3438
+ body: JSON.stringify(data),
3439
+ });
3440
+ }
3441
+ // ============================================
3442
+ // EVENT TRIGGERS API (delegated to triggers manager)
3443
+ // ============================================
3444
+ /**
3445
+ * Get all event triggers
3446
+ */
3447
+ async getEventTriggers() {
3448
+ return this.triggers.getTriggers();
3449
+ }
3450
+ /**
3451
+ * Create a new event trigger
3452
+ */
3453
+ async createEventTrigger(trigger) {
3454
+ return this.triggers.createTrigger(trigger);
3455
+ }
3456
+ /**
3457
+ * Update an event trigger
3458
+ */
3459
+ async updateEventTrigger(triggerId, updates) {
3460
+ return this.triggers.updateTrigger(triggerId, updates);
3461
+ }
3462
+ /**
3463
+ * Delete an event trigger
3464
+ */
3465
+ async deleteEventTrigger(triggerId) {
3466
+ return this.triggers.deleteTrigger(triggerId);
3467
+ }
3468
+ }
3469
+
3470
+ /**
3471
+ * Clianta SDK - Main Tracker Class
3472
+ * @see SDK_VERSION in core/config.ts
3473
+ */
3474
+ /**
3475
+ * Main Clianta Tracker Class
3476
+ */
3477
+ class Tracker {
3478
+ constructor(workspaceId, userConfig = {}) {
3479
+ this.plugins = [];
3480
+ this.isInitialized = false;
3481
+ /** contactId after a successful identify() call */
3482
+ this.contactId = null;
3483
+ /** Pending identify retry on next flush */
3484
+ this.pendingIdentify = null;
3485
+ if (!workspaceId) {
3486
+ throw new Error('[Clianta] Workspace ID is required');
3487
+ }
3488
+ this.workspaceId = workspaceId;
3489
+ this.config = mergeConfig(userConfig);
3490
+ // Setup debug mode
3491
+ logger.enabled = this.config.debug;
3492
+ logger.info(`Initializing SDK v${SDK_VERSION}`, { workspaceId });
3493
+ // Initialize consent manager
3494
+ this.consentManager = new ConsentManager({
3495
+ ...this.config.consent,
3496
+ onConsentChange: (state, previous) => {
3497
+ this.onConsentChange(state, previous);
3498
+ },
3499
+ });
3500
+ // Initialize transport and queue
3501
+ this.transport = new Transport({ apiEndpoint: this.config.apiEndpoint });
3502
+ this.queue = new EventQueue(this.transport, {
3503
+ batchSize: this.config.batchSize,
3504
+ flushInterval: this.config.flushInterval,
3505
+ });
3506
+ // Get or create visitor and session IDs based on mode
3507
+ this.visitorId = this.createVisitorId();
3508
+ this.sessionId = this.createSessionId();
3509
+ logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
3510
+ // Initialize plugins
3511
+ this.initPlugins();
3512
+ this.isInitialized = true;
3513
+ logger.info('SDK initialized successfully');
3514
+ }
3515
+ /**
3516
+ * Create visitor ID based on storage mode
3517
+ */
3518
+ createVisitorId() {
3519
+ // Anonymous mode: use temporary ID until consent
3520
+ if (this.config.consent.anonymousMode && !this.consentManager.hasExplicit()) {
3521
+ const key = STORAGE_KEYS.VISITOR_ID + '_anon';
3522
+ let anonId = getSessionStorage(key);
3523
+ if (!anonId) {
3524
+ anonId = 'anon_' + generateUUID();
3525
+ setSessionStorage(key, anonId);
3526
+ }
3527
+ return anonId;
3528
+ }
3529
+ // Cookie-less mode: use sessionStorage only
3530
+ if (this.config.cookielessMode) {
3531
+ let visitorId = getSessionStorage(STORAGE_KEYS.VISITOR_ID);
3532
+ if (!visitorId) {
3533
+ visitorId = generateUUID();
3534
+ setSessionStorage(STORAGE_KEYS.VISITOR_ID, visitorId);
3535
+ }
3536
+ return visitorId;
3537
+ }
3538
+ // Normal mode
3539
+ return getOrCreateVisitorId(this.config.useCookies);
3540
+ }
3541
+ /**
3542
+ * Create session ID
3543
+ */
3544
+ createSessionId() {
3545
+ return getOrCreateSessionId(this.config.sessionTimeout);
3546
+ }
3547
+ /**
3548
+ * Handle consent state changes
3549
+ */
3550
+ onConsentChange(state, previous) {
3551
+ logger.debug('Consent changed:', { from: previous, to: state });
3552
+ // If analytics consent was just granted
3553
+ if (state.analytics && !previous.analytics) {
3554
+ // Upgrade from anonymous ID to persistent ID
3555
+ if (this.config.consent.anonymousMode) {
3556
+ this.visitorId = getOrCreateVisitorId(this.config.useCookies);
3557
+ logger.info('Upgraded from anonymous to persistent visitor ID');
3558
+ }
3559
+ // Flush buffered events
3560
+ const buffered = this.consentManager.flushBuffer();
3561
+ for (const event of buffered) {
3562
+ // Update event with new visitor ID
3563
+ event.visitorId = this.visitorId;
3564
+ this.queue.push(event);
3565
+ }
3566
+ }
3567
+ }
3568
+ /**
3569
+ * Initialize enabled plugins
3570
+ * Handles both sync and async plugin init methods
3571
+ */
3572
+ initPlugins() {
3573
+ const pluginsToLoad = this.config.plugins;
3574
+ // Skip pageView plugin if autoPageView is disabled
3575
+ const filteredPlugins = this.config.autoPageView
3576
+ ? pluginsToLoad
3577
+ : pluginsToLoad.filter((p) => p !== 'pageView');
3578
+ for (const pluginName of filteredPlugins) {
3579
+ try {
3580
+ const plugin = getPlugin(pluginName);
3581
+ // Handle both sync and async init (fire-and-forget for async)
3582
+ const result = plugin.init(this);
3583
+ if (result instanceof Promise) {
3584
+ result.catch((error) => {
3585
+ logger.error(`Async plugin init failed: ${pluginName}`, error);
3586
+ });
3587
+ }
3588
+ this.plugins.push(plugin);
3589
+ logger.debug(`Plugin loaded: ${pluginName}`);
3590
+ }
3591
+ catch (error) {
3592
+ logger.error(`Failed to load plugin: ${pluginName}`, error);
3593
+ }
3594
+ }
3595
+ }
3596
+ /**
3597
+ * Track a custom event
3598
+ */
3599
+ track(eventType, eventName, properties = {}) {
3600
+ if (!this.isInitialized) {
3601
+ logger.warn('SDK not initialized, event dropped');
3602
+ return;
3603
+ }
3604
+ const event = {
3605
+ workspaceId: this.workspaceId,
3606
+ visitorId: this.visitorId,
3607
+ sessionId: this.sessionId,
3608
+ eventType: eventType,
3609
+ eventName,
3610
+ url: typeof window !== 'undefined' ? window.location.href : '',
3611
+ referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
3612
+ properties: {
3613
+ ...properties,
3614
+ eventId: generateUUID(), // Unique ID for deduplication on retry
3615
+ },
3616
+ device: getDeviceInfo(),
3617
+ ...getUTMParams(),
3618
+ timestamp: new Date().toISOString(),
3619
+ sdkVersion: SDK_VERSION,
3620
+ };
3621
+ // Attach contactId if known (from a prior identify() call)
3622
+ if (this.contactId) {
3623
+ event.contactId = this.contactId;
3624
+ }
3625
+ // Check consent before tracking
3626
+ if (!this.consentManager.canTrack()) {
3627
+ // Buffer event for later if waitForConsent is enabled
3628
+ if (this.config.consent.waitForConsent) {
3629
+ this.consentManager.bufferEvent(event);
3630
+ return;
3631
+ }
3632
+ // Otherwise drop the event
3633
+ logger.debug('Event dropped (no consent):', eventName);
3634
+ return;
3635
+ }
3636
+ this.queue.push(event);
3637
+ logger.debug('Event tracked:', eventName, properties);
3638
+ }
3639
+ /**
3640
+ * Track a page view
3641
+ */
3642
+ page(name, properties = {}) {
3643
+ const pageName = name || (typeof document !== 'undefined' ? document.title : 'Page View');
3644
+ this.track('page_view', pageName, {
3645
+ ...properties,
3646
+ path: typeof window !== 'undefined' ? window.location.pathname : '',
3647
+ });
3648
+ }
3649
+ /**
3650
+ * Identify a visitor.
3651
+ * Links the anonymous visitorId to a CRM contact and returns the contactId.
3652
+ * All subsequent track() calls will include the contactId automatically.
3653
+ */
3654
+ async identify(email, traits = {}) {
3655
+ if (!email) {
3656
+ logger.warn('Email is required for identification');
3657
+ return null;
3658
+ }
3659
+ logger.info('Identifying visitor:', email);
3660
+ const result = await this.transport.sendIdentify({
3661
+ workspaceId: this.workspaceId,
3662
+ visitorId: this.visitorId,
3663
+ email,
3664
+ properties: traits,
3665
+ });
3666
+ if (result.success) {
3667
+ logger.info('Visitor identified successfully, contactId:', result.contactId);
3668
+ // Store contactId so all future track() calls include it
3669
+ this.contactId = result.contactId ?? null;
3670
+ this.pendingIdentify = null;
3671
+ return this.contactId;
3672
+ }
3673
+ else {
3674
+ logger.error('Failed to identify visitor:', result.error);
3675
+ // Store for retry on next flush
3676
+ this.pendingIdentify = { email, traits };
3677
+ return null;
3678
+ }
3679
+ }
3680
+ /**
3681
+ * Send a server-side inbound event via the API key endpoint.
3682
+ * Convenience proxy to CRMClient.sendEvent() — requires apiKey in config.
3683
+ */
3684
+ async sendEvent(payload) {
3685
+ const apiKey = this.config.apiKey;
3686
+ if (!apiKey) {
3687
+ logger.error('sendEvent() requires an apiKey in the SDK config');
3688
+ return { success: false, contactCreated: false, event: payload.event, error: 'No API key configured' };
3689
+ }
3690
+ const client = new CRMClient(this.config.apiEndpoint, this.workspaceId, undefined, apiKey);
3691
+ return client.sendEvent(payload);
3692
+ }
3693
+ /**
3694
+ * Retry pending identify call
3695
+ */
3696
+ async retryPendingIdentify() {
3697
+ if (!this.pendingIdentify)
3698
+ return;
3699
+ const { email, traits } = this.pendingIdentify;
3700
+ this.pendingIdentify = null;
3701
+ await this.identify(email, traits);
3702
+ }
3703
+ /**
3704
+ * Update consent state
3705
+ */
3706
+ consent(state) {
3707
+ this.consentManager.update(state);
3708
+ }
3709
+ /**
3710
+ * Get current consent state
3711
+ */
3712
+ getConsentState() {
3713
+ return this.consentManager.getState();
3714
+ }
3715
+ /**
3716
+ * Toggle debug mode
3717
+ */
3718
+ debug(enabled) {
3719
+ logger.enabled = enabled;
3720
+ logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
3721
+ }
3722
+ /**
3723
+ * Get visitor ID
3724
+ */
3725
+ getVisitorId() {
3726
+ return this.visitorId;
3727
+ }
3728
+ /**
3729
+ * Get session ID
3730
+ */
3731
+ getSessionId() {
3732
+ return this.sessionId;
3733
+ }
3734
+ /**
3735
+ * Get workspace ID
3736
+ */
3737
+ getWorkspaceId() {
3738
+ return this.workspaceId;
3739
+ }
3740
+ /**
3741
+ * Get current configuration
3742
+ */
3743
+ getConfig() {
3744
+ return { ...this.config };
3745
+ }
3746
+ /**
3747
+ * Force flush event queue
3748
+ */
3749
+ async flush() {
3750
+ await this.retryPendingIdentify();
3751
+ await this.queue.flush();
3752
+ }
3753
+ /**
3754
+ * Reset visitor and session (for logout)
3755
+ */
3756
+ reset() {
3757
+ logger.info('Resetting visitor data');
3758
+ resetIds(this.config.useCookies);
3759
+ this.visitorId = this.createVisitorId();
3760
+ this.sessionId = this.createSessionId();
3761
+ this.queue.clear();
3762
+ }
3763
+ /**
3764
+ * Delete all stored user data (GDPR right-to-erasure)
3765
+ */
3766
+ deleteData() {
3767
+ logger.info('Deleting all user data (GDPR request)');
3768
+ // Clear queue
3769
+ this.queue.clear();
3770
+ // Reset consent
3771
+ this.consentManager.reset();
3772
+ // Clear all stored IDs
3773
+ resetIds(this.config.useCookies);
3774
+ // Clear session storage items
3775
+ if (typeof sessionStorage !== 'undefined') {
3776
+ try {
3777
+ sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
3778
+ sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID + '_anon');
3779
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_ID);
3780
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_TIMESTAMP);
3781
+ }
3782
+ catch {
3783
+ // Ignore errors
3784
+ }
3785
+ }
3786
+ // Clear localStorage items
3787
+ if (typeof localStorage !== 'undefined') {
3788
+ try {
3789
+ localStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
3790
+ localStorage.removeItem(STORAGE_KEYS.CONSENT);
3791
+ localStorage.removeItem(STORAGE_KEYS.EVENT_QUEUE);
3792
+ }
3793
+ catch {
3794
+ // Ignore errors
3795
+ }
3796
+ }
3797
+ // Generate new IDs
3798
+ this.visitorId = this.createVisitorId();
3799
+ this.sessionId = this.createSessionId();
3800
+ logger.info('All user data deleted');
3801
+ }
3802
+ /**
3803
+ * Destroy tracker and cleanup
3804
+ */
3805
+ async destroy() {
3806
+ logger.info('Destroying tracker');
3807
+ // Flush any remaining events (await to ensure completion)
3808
+ await this.queue.flush();
3809
+ // Destroy plugins
3810
+ for (const plugin of this.plugins) {
3811
+ if (plugin.destroy) {
3812
+ plugin.destroy();
3813
+ }
3814
+ }
3815
+ this.plugins = [];
3816
+ // Destroy queue
3817
+ this.queue.destroy();
3818
+ this.isInitialized = false;
3819
+ }
3820
+ }
3821
+
3822
+ /**
3823
+ * Clianta SDK
3824
+ * Professional CRM and tracking SDK for lead generation
3825
+ * @see SDK_VERSION in core/config.ts
3826
+ */
3827
+ // Global instance cache
3828
+ let globalInstance = null;
3829
+ /**
3830
+ * Initialize or get the Clianta tracker instance
3831
+ *
3832
+ * @example
3833
+ * // Simple initialization
3834
+ * const tracker = clianta('your-workspace-id');
3835
+ *
3836
+ * @example
3837
+ * // With configuration
3838
+ * const tracker = clianta('your-workspace-id', {
3839
+ * debug: true,
3840
+ * plugins: ['pageView', 'forms', 'scroll'],
3841
+ * });
3842
+ *
3843
+ * @example
3844
+ * // With consent configuration
3845
+ * const tracker = clianta('your-workspace-id', {
3846
+ * consent: {
3847
+ * waitForConsent: true,
3848
+ * anonymousMode: true,
3849
+ * },
3850
+ * cookielessMode: true, // GDPR-friendly mode
3851
+ * });
3852
+ */
3853
+ function clianta(workspaceId, config) {
3854
+ // Return existing instance if same workspace
3855
+ if (globalInstance && globalInstance.getWorkspaceId() === workspaceId) {
3856
+ return globalInstance;
3857
+ }
3858
+ // Destroy existing instance if workspace changed
3859
+ if (globalInstance) {
3860
+ globalInstance.destroy();
3861
+ }
3862
+ // Create new instance
3863
+ globalInstance = new Tracker(workspaceId, config);
3864
+ return globalInstance;
3865
+ }
3866
+ // Attach to window for <script> usage
3867
+ if (typeof window !== 'undefined') {
3868
+ window.clianta = clianta;
3869
+ window.Clianta = {
3870
+ clianta,
3871
+ Tracker,
3872
+ CRMClient,
3873
+ ConsentManager,
3874
+ EventTriggersManager,
3875
+ };
3876
+ }
3877
+
3878
+ /**
3879
+ * Clianta SDK - Vue 3 Integration
3880
+ *
3881
+ * Provides plugin and composables for easy Vue 3 integration.
3882
+ */
3883
+ // Injection key for tracker
3884
+ const CliantaKey = Symbol('clianta');
3885
+ /**
3886
+ * Vue plugin for Clianta SDK
3887
+ *
3888
+ * @example
3889
+ * // In main.ts:
3890
+ * import { createApp } from 'vue';
3891
+ * import { CliantaPlugin } from '@clianta/sdk/vue';
3892
+ * import App from './App.vue';
3893
+ *
3894
+ * const app = createApp(App);
3895
+ * app.use(CliantaPlugin, {
3896
+ * projectId: 'your-project-id',
3897
+ * apiEndpoint: 'https://api.clianta.online',
3898
+ * debug: import.meta.env.DEV,
3899
+ * });
3900
+ * app.mount('#app');
3901
+ */
3902
+ const CliantaPlugin = {
3903
+ install(app, options) {
3904
+ if (!options?.projectId) {
3905
+ console.error('[Clianta] Missing projectId in plugin options');
3906
+ return;
3907
+ }
3908
+ const { projectId, ...config } = options;
3909
+ const tracker = clianta(projectId, config);
3910
+ // Provide tracker to all components
3911
+ app.provide(CliantaKey, vue.ref(tracker));
3912
+ // Add global property for Options API access
3913
+ app.config.globalProperties.$clianta = tracker;
3914
+ // Flush on app unmount
3915
+ app.mixin({
3916
+ beforeUnmount() {
3917
+ // Only flush on root component unmount
3918
+ if (this.$.parent === null) {
3919
+ tracker.flush();
3920
+ }
3921
+ },
3922
+ });
3923
+ },
3924
+ };
3925
+ /**
3926
+ * useClianta - Composable to access tracker
3927
+ *
3928
+ * @example
3929
+ * <script setup>
3930
+ * import { useClianta } from '@clianta/sdk/vue';
3931
+ *
3932
+ * const tracker = useClianta();
3933
+ * tracker.value?.track('button_click', 'CTA Button');
3934
+ * </script>
3935
+ */
3936
+ function useClianta() {
3937
+ const tracker = vue.inject(CliantaKey);
3938
+ if (!tracker) {
3939
+ console.warn('[Clianta] useClianta must be used within a component where CliantaPlugin is installed');
3940
+ return vue.ref(null);
3941
+ }
3942
+ return tracker;
3943
+ }
3944
+ /**
3945
+ * useCliantaTrack - Composable for tracking events
3946
+ *
3947
+ * @example
3948
+ * <script setup>
3949
+ * import { useCliantaTrack } from '@clianta/sdk/vue';
3950
+ *
3951
+ * const track = useCliantaTrack();
3952
+ * track('purchase', 'Order Completed', { orderId: '123' });
3953
+ * </script>
3954
+ */
3955
+ function useCliantaTrack() {
3956
+ const tracker = useClianta();
3957
+ return (eventType, eventName, properties) => {
3958
+ tracker.value?.track(eventType, eventName, properties);
3959
+ };
3960
+ }
3961
+ /**
3962
+ * useCliantaIdentify - Composable for identifying users
3963
+ *
3964
+ * @example
3965
+ * <script setup>
3966
+ * import { useCliantaIdentify } from '@clianta/sdk/vue';
3967
+ *
3968
+ * const identify = useCliantaIdentify();
3969
+ * identify('user@example.com', { name: 'John' });
3970
+ * </script>
3971
+ */
3972
+ function useCliantaIdentify() {
3973
+ const tracker = useClianta();
3974
+ return (email, traits) => {
3975
+ return tracker.value?.identify(email, traits);
3976
+ };
3977
+ }
3978
+ /**
3979
+ * useCliantaPageView - Composable for manual page view tracking
3980
+ *
3981
+ * @example
3982
+ * <script setup>
3983
+ * import { useCliantaPageView } from '@clianta/sdk/vue';
3984
+ * import { watch } from 'vue';
3985
+ * import { useRoute } from 'vue-router';
3986
+ *
3987
+ * const route = useRoute();
3988
+ * const trackPageView = useCliantaPageView();
3989
+ *
3990
+ * watch(() => route.path, () => {
3991
+ * trackPageView(route.name?.toString());
3992
+ * });
3993
+ * </script>
3994
+ */
3995
+ function useCliantaPageView() {
3996
+ const tracker = useClianta();
3997
+ return (name, properties) => {
3998
+ tracker.value?.page(name, properties);
3999
+ };
4000
+ }
4001
+ /**
4002
+ * useCliantaConsent - Composable for managing consent
4003
+ *
4004
+ * @example
4005
+ * <script setup>
4006
+ * import { useCliantaConsent } from '@clianta/sdk/vue';
4007
+ *
4008
+ * const { consent, getConsentState } = useCliantaConsent();
4009
+ * consent({ analytics: true, marketing: false });
4010
+ * </script>
4011
+ */
4012
+ function useCliantaConsent() {
4013
+ const tracker = useClianta();
4014
+ return {
4015
+ consent: (state) => {
4016
+ tracker.value?.consent(state);
4017
+ },
4018
+ getConsentState: () => tracker.value?.getConsentState(),
4019
+ };
4020
+ }
4021
+
4022
+ exports.CliantaPlugin = CliantaPlugin;
4023
+ exports.useClianta = useClianta;
4024
+ exports.useCliantaConsent = useCliantaConsent;
4025
+ exports.useCliantaIdentify = useCliantaIdentify;
4026
+ exports.useCliantaPageView = useCliantaPageView;
4027
+ exports.useCliantaTrack = useCliantaTrack;
4028
+ //# sourceMappingURL=vue.cjs.js.map