@clianta/sdk 1.0.0 → 1.1.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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Clianta SDK v1.0.0
2
+ * Clianta SDK v1.1.0
3
3
  * (c) 2026 Clianta
4
4
  * Released under the MIT License.
5
5
  */
@@ -11,10 +11,10 @@
11
11
 
12
12
  /**
13
13
  * Clianta SDK - Configuration
14
- * @version 1.0.0
14
+ * @see SDK_VERSION in core/config.ts
15
15
  */
16
16
  /** SDK Version */
17
- const SDK_VERSION = '1.0.0';
17
+ const SDK_VERSION = '1.1.0';
18
18
  /** Default API endpoint based on environment */
19
19
  const getDefaultApiEndpoint = () => {
20
20
  if (typeof window === 'undefined')
@@ -34,6 +34,7 @@
34
34
  'engagement',
35
35
  'downloads',
36
36
  'exitIntent',
37
+ 'popupForms',
37
38
  ];
38
39
  /** Default configuration values */
39
40
  const DEFAULT_CONFIG = {
@@ -48,9 +49,11 @@
48
49
  defaultConsent: { analytics: true, marketing: false, personalization: false },
49
50
  waitForConsent: false,
50
51
  storageKey: 'mb_consent',
52
+ anonymousMode: false,
51
53
  },
52
54
  cookieDomain: '',
53
55
  useCookies: false,
56
+ cookielessMode: false,
54
57
  };
55
58
  /** Storage keys */
56
59
  const STORAGE_KEYS = {
@@ -84,8 +87,8 @@
84
87
  }
85
88
 
86
89
  /**
87
- * MorrisB Tracking SDK - Debug Logger
88
- * @version 3.0.0
90
+ * Clianta SDK - Debug Logger
91
+ * @see SDK_VERSION in core/config.ts
89
92
  */
90
93
  const LOG_PREFIX = '[Clianta]';
91
94
  const LOG_STYLES = {
@@ -155,9 +158,9 @@
155
158
  const logger = createLogger(false);
156
159
 
157
160
  /**
158
- * MorrisB Tracking SDK - Transport Layer
161
+ * Clianta SDK - Transport Layer
159
162
  * Handles sending events to the backend with retry logic
160
- * @version 3.0.0
163
+ * @see SDK_VERSION in core/config.ts
161
164
  */
162
165
  const DEFAULT_TIMEOUT = 10000; // 10 seconds
163
166
  const DEFAULT_MAX_RETRIES = 3;
@@ -281,8 +284,8 @@
281
284
  }
282
285
 
283
286
  /**
284
- * MorrisB Tracking SDK - Utility Functions
285
- * @version 3.0.0
287
+ * Clianta SDK - Utility Functions
288
+ * @see SDK_VERSION in core/config.ts
286
289
  */
287
290
  // ============================================
288
291
  // UUID GENERATION
@@ -568,9 +571,9 @@
568
571
  }
569
572
 
570
573
  /**
571
- * MorrisB Tracking SDK - Event Queue
574
+ * Clianta SDK - Event Queue
572
575
  * Handles batching and flushing of events
573
- * @version 3.0.0
576
+ * @see SDK_VERSION in core/config.ts
574
577
  */
575
578
  const MAX_QUEUE_SIZE = 1000;
576
579
  /**
@@ -745,8 +748,8 @@
745
748
  }
746
749
 
747
750
  /**
748
- * MorrisB Tracking SDK - Plugin Base
749
- * @version 3.0.0
751
+ * Clianta SDK - Plugin Base
752
+ * @see SDK_VERSION in core/config.ts
750
753
  */
751
754
  /**
752
755
  * Base class for plugins
@@ -769,8 +772,8 @@
769
772
  }
770
773
 
771
774
  /**
772
- * MorrisB Tracking SDK - Page View Plugin
773
- * @version 3.0.0
775
+ * Clianta SDK - Page View Plugin
776
+ * @see SDK_VERSION in core/config.ts
774
777
  */
775
778
  /**
776
779
  * Page View Plugin - Tracks page views
@@ -818,8 +821,8 @@
818
821
  }
819
822
 
820
823
  /**
821
- * MorrisB Tracking SDK - Scroll Depth Plugin
822
- * @version 3.0.0
824
+ * Clianta SDK - Scroll Depth Plugin
825
+ * @see SDK_VERSION in core/config.ts
823
826
  */
824
827
  /**
825
828
  * Scroll Depth Plugin - Tracks scroll milestones
@@ -886,8 +889,8 @@
886
889
  }
887
890
 
888
891
  /**
889
- * MorrisB Tracking SDK - Form Tracking Plugin
890
- * @version 3.0.0
892
+ * Clianta SDK - Form Tracking Plugin
893
+ * @see SDK_VERSION in core/config.ts
891
894
  */
892
895
  /**
893
896
  * Form Tracking Plugin - Auto-tracks form views, interactions, and submissions
@@ -994,8 +997,8 @@
994
997
  }
995
998
 
996
999
  /**
997
- * MorrisB Tracking SDK - Click Tracking Plugin
998
- * @version 3.0.0
1000
+ * Clianta SDK - Click Tracking Plugin
1001
+ * @see SDK_VERSION in core/config.ts
999
1002
  */
1000
1003
  /**
1001
1004
  * Click Tracking Plugin - Tracks button and CTA clicks
@@ -1036,8 +1039,8 @@
1036
1039
  }
1037
1040
 
1038
1041
  /**
1039
- * MorrisB Tracking SDK - Engagement Plugin
1040
- * @version 3.0.0
1042
+ * Clianta SDK - Engagement Plugin
1043
+ * @see SDK_VERSION in core/config.ts
1041
1044
  */
1042
1045
  /**
1043
1046
  * Engagement Plugin - Tracks user engagement and time on page
@@ -1118,8 +1121,8 @@
1118
1121
  }
1119
1122
 
1120
1123
  /**
1121
- * MorrisB Tracking SDK - Downloads Plugin
1122
- * @version 3.0.0
1124
+ * Clianta SDK - Downloads Plugin
1125
+ * @see SDK_VERSION in core/config.ts
1123
1126
  */
1124
1127
  /**
1125
1128
  * Downloads Plugin - Tracks file downloads
@@ -1166,8 +1169,8 @@
1166
1169
  }
1167
1170
 
1168
1171
  /**
1169
- * MorrisB Tracking SDK - Exit Intent Plugin
1170
- * @version 3.0.0
1172
+ * Clianta SDK - Exit Intent Plugin
1173
+ * @see SDK_VERSION in core/config.ts
1171
1174
  */
1172
1175
  /**
1173
1176
  * Exit Intent Plugin - Detects when user intends to leave the page
@@ -1209,8 +1212,8 @@
1209
1212
  }
1210
1213
 
1211
1214
  /**
1212
- * MorrisB Tracking SDK - Error Tracking Plugin
1213
- * @version 3.0.0
1215
+ * Clianta SDK - Error Tracking Plugin
1216
+ * @see SDK_VERSION in core/config.ts
1214
1217
  */
1215
1218
  /**
1216
1219
  * Error Tracking Plugin - Tracks JavaScript errors
@@ -1259,8 +1262,8 @@
1259
1262
  }
1260
1263
 
1261
1264
  /**
1262
- * MorrisB Tracking SDK - Performance Plugin
1263
- * @version 3.0.0
1265
+ * Clianta SDK - Performance Plugin
1266
+ * @see SDK_VERSION in core/config.ts
1264
1267
  */
1265
1268
  /**
1266
1269
  * Performance Plugin - Tracks page performance and Web Vitals
@@ -1366,8 +1369,420 @@
1366
1369
  }
1367
1370
 
1368
1371
  /**
1369
- * MorrisB Tracking SDK - Plugins Index
1370
- * @version 3.0.0
1372
+ * Clianta Tracking SDK - Popup Forms Plugin
1373
+ * @see SDK_VERSION in core/config.ts
1374
+ *
1375
+ * Auto-loads and displays lead capture popups based on triggers
1376
+ */
1377
+ /**
1378
+ * Popup Forms Plugin - Fetches and displays lead capture forms
1379
+ */
1380
+ class PopupFormsPlugin extends BasePlugin {
1381
+ constructor() {
1382
+ super(...arguments);
1383
+ this.name = 'popupForms';
1384
+ this.forms = [];
1385
+ this.shownForms = new Set();
1386
+ this.scrollHandler = null;
1387
+ this.exitHandler = null;
1388
+ }
1389
+ async init(tracker) {
1390
+ super.init(tracker);
1391
+ if (typeof window === 'undefined')
1392
+ return;
1393
+ // Load shown forms from storage
1394
+ this.loadShownForms();
1395
+ // Fetch active forms
1396
+ await this.fetchForms();
1397
+ // Setup triggers
1398
+ this.setupTriggers();
1399
+ }
1400
+ destroy() {
1401
+ this.removeTriggers();
1402
+ super.destroy();
1403
+ }
1404
+ loadShownForms() {
1405
+ try {
1406
+ const stored = localStorage.getItem('clianta_shown_forms');
1407
+ if (stored) {
1408
+ const data = JSON.parse(stored);
1409
+ this.shownForms = new Set(data.forms || []);
1410
+ }
1411
+ }
1412
+ catch (e) {
1413
+ // Ignore storage errors
1414
+ }
1415
+ }
1416
+ saveShownForms() {
1417
+ try {
1418
+ localStorage.setItem('clianta_shown_forms', JSON.stringify({
1419
+ forms: Array.from(this.shownForms),
1420
+ timestamp: Date.now(),
1421
+ }));
1422
+ }
1423
+ catch (e) {
1424
+ // Ignore storage errors
1425
+ }
1426
+ }
1427
+ async fetchForms() {
1428
+ if (!this.tracker)
1429
+ return;
1430
+ const config = this.tracker.getConfig();
1431
+ const workspaceId = this.tracker.getWorkspaceId();
1432
+ const apiEndpoint = config.apiEndpoint || 'https://api.clianta.online';
1433
+ try {
1434
+ const url = encodeURIComponent(window.location.href);
1435
+ const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${workspaceId}?url=${url}`);
1436
+ if (!response.ok)
1437
+ return;
1438
+ const data = await response.json();
1439
+ if (data.success && Array.isArray(data.data)) {
1440
+ this.forms = data.data.filter((form) => this.shouldShowForm(form));
1441
+ }
1442
+ }
1443
+ catch (error) {
1444
+ console.error('[Clianta] Failed to fetch forms:', error);
1445
+ }
1446
+ }
1447
+ shouldShowForm(form) {
1448
+ // Check show frequency
1449
+ if (form.showFrequency === 'once_per_visitor') {
1450
+ if (this.shownForms.has(form._id))
1451
+ return false;
1452
+ }
1453
+ else if (form.showFrequency === 'once_per_session') {
1454
+ const sessionKey = `clianta_form_${form._id}_shown`;
1455
+ if (sessionStorage.getItem(sessionKey))
1456
+ return false;
1457
+ }
1458
+ return true;
1459
+ }
1460
+ setupTriggers() {
1461
+ this.forms.forEach(form => {
1462
+ switch (form.trigger.type) {
1463
+ case 'delay':
1464
+ setTimeout(() => this.showForm(form), (form.trigger.value || 5) * 1000);
1465
+ break;
1466
+ case 'scroll':
1467
+ this.setupScrollTrigger(form);
1468
+ break;
1469
+ case 'exit_intent':
1470
+ this.setupExitIntentTrigger(form);
1471
+ break;
1472
+ case 'click':
1473
+ this.setupClickTrigger(form);
1474
+ break;
1475
+ }
1476
+ });
1477
+ }
1478
+ setupScrollTrigger(form) {
1479
+ const threshold = form.trigger.value || 50;
1480
+ this.scrollHandler = () => {
1481
+ const scrollPercent = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
1482
+ if (scrollPercent >= threshold) {
1483
+ this.showForm(form);
1484
+ if (this.scrollHandler) {
1485
+ window.removeEventListener('scroll', this.scrollHandler);
1486
+ }
1487
+ }
1488
+ };
1489
+ window.addEventListener('scroll', this.scrollHandler, { passive: true });
1490
+ }
1491
+ setupExitIntentTrigger(form) {
1492
+ this.exitHandler = (e) => {
1493
+ if (e.clientY <= 0) {
1494
+ this.showForm(form);
1495
+ if (this.exitHandler) {
1496
+ document.removeEventListener('mouseout', this.exitHandler);
1497
+ }
1498
+ }
1499
+ };
1500
+ document.addEventListener('mouseout', this.exitHandler);
1501
+ }
1502
+ setupClickTrigger(form) {
1503
+ if (!form.trigger.selector)
1504
+ return;
1505
+ const elements = document.querySelectorAll(form.trigger.selector);
1506
+ elements.forEach(el => {
1507
+ el.addEventListener('click', () => this.showForm(form));
1508
+ });
1509
+ }
1510
+ removeTriggers() {
1511
+ if (this.scrollHandler) {
1512
+ window.removeEventListener('scroll', this.scrollHandler);
1513
+ }
1514
+ if (this.exitHandler) {
1515
+ document.removeEventListener('mouseout', this.exitHandler);
1516
+ }
1517
+ }
1518
+ async showForm(form) {
1519
+ // Check if already shown in this session
1520
+ if (!this.shouldShowForm(form))
1521
+ return;
1522
+ // Mark as shown
1523
+ this.shownForms.add(form._id);
1524
+ this.saveShownForms();
1525
+ sessionStorage.setItem(`clianta_form_${form._id}_shown`, 'true');
1526
+ // Track view
1527
+ await this.trackFormView(form._id);
1528
+ // Render form
1529
+ this.renderForm(form);
1530
+ }
1531
+ async trackFormView(formId) {
1532
+ if (!this.tracker)
1533
+ return;
1534
+ const config = this.tracker.getConfig();
1535
+ const apiEndpoint = config.apiEndpoint || 'https://api.clianta.online';
1536
+ try {
1537
+ await fetch(`${apiEndpoint}/api/public/lead-forms/${formId}/view`, {
1538
+ method: 'POST',
1539
+ headers: { 'Content-Type': 'application/json' },
1540
+ });
1541
+ }
1542
+ catch (e) {
1543
+ // Ignore tracking errors
1544
+ }
1545
+ }
1546
+ renderForm(form) {
1547
+ // Create overlay
1548
+ const overlay = document.createElement('div');
1549
+ overlay.id = `clianta-form-overlay-${form._id}`;
1550
+ overlay.style.cssText = `
1551
+ position: fixed;
1552
+ top: 0;
1553
+ left: 0;
1554
+ right: 0;
1555
+ bottom: 0;
1556
+ background: rgba(0, 0, 0, 0.5);
1557
+ z-index: 999998;
1558
+ display: flex;
1559
+ align-items: center;
1560
+ justify-content: center;
1561
+ opacity: 0;
1562
+ transition: opacity 0.3s ease;
1563
+ `;
1564
+ // Create form container
1565
+ const container = document.createElement('div');
1566
+ container.id = `clianta-form-${form._id}`;
1567
+ const style = form.style || {};
1568
+ container.style.cssText = `
1569
+ background: ${style.backgroundColor || '#FFFFFF'};
1570
+ border-radius: ${style.borderRadius || 12}px;
1571
+ padding: 24px;
1572
+ max-width: 400px;
1573
+ width: 90%;
1574
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
1575
+ transform: translateY(20px);
1576
+ opacity: 0;
1577
+ transition: all 0.3s ease;
1578
+ `;
1579
+ // Build form HTML
1580
+ container.innerHTML = this.buildFormHTML(form);
1581
+ overlay.appendChild(container);
1582
+ document.body.appendChild(overlay);
1583
+ // Animate in
1584
+ requestAnimationFrame(() => {
1585
+ overlay.style.opacity = '1';
1586
+ container.style.transform = 'translateY(0)';
1587
+ container.style.opacity = '1';
1588
+ });
1589
+ // Setup event listeners
1590
+ this.setupFormEvents(form, overlay, container);
1591
+ }
1592
+ buildFormHTML(form) {
1593
+ const style = form.style || {};
1594
+ const primaryColor = style.primaryColor || '#10B981';
1595
+ const textColor = style.textColor || '#18181B';
1596
+ let fieldsHTML = form.fields.map(field => {
1597
+ const requiredMark = field.required ? '<span style="color: #EF4444;">*</span>' : '';
1598
+ if (field.type === 'textarea') {
1599
+ return `
1600
+ <div style="margin-bottom: 12px;">
1601
+ <label style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${textColor};">
1602
+ ${field.label} ${requiredMark}
1603
+ </label>
1604
+ <textarea
1605
+ name="${field.name}"
1606
+ placeholder="${field.placeholder || ''}"
1607
+ ${field.required ? 'required' : ''}
1608
+ style="width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; resize: vertical; min-height: 80px;"
1609
+ ></textarea>
1610
+ </div>
1611
+ `;
1612
+ }
1613
+ else if (field.type === 'checkbox') {
1614
+ return `
1615
+ <div style="margin-bottom: 12px;">
1616
+ <label style="display: flex; align-items: center; gap: 8px; font-size: 14px; color: ${textColor}; cursor: pointer;">
1617
+ <input
1618
+ type="checkbox"
1619
+ name="${field.name}"
1620
+ ${field.required ? 'required' : ''}
1621
+ style="width: 16px; height: 16px;"
1622
+ />
1623
+ ${field.label} ${requiredMark}
1624
+ </label>
1625
+ </div>
1626
+ `;
1627
+ }
1628
+ else {
1629
+ return `
1630
+ <div style="margin-bottom: 12px;">
1631
+ <label style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${textColor};">
1632
+ ${field.label} ${requiredMark}
1633
+ </label>
1634
+ <input
1635
+ type="${field.type}"
1636
+ name="${field.name}"
1637
+ placeholder="${field.placeholder || ''}"
1638
+ ${field.required ? 'required' : ''}
1639
+ style="width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box;"
1640
+ />
1641
+ </div>
1642
+ `;
1643
+ }
1644
+ }).join('');
1645
+ return `
1646
+ <button id="clianta-form-close" style="
1647
+ position: absolute;
1648
+ top: 12px;
1649
+ right: 12px;
1650
+ background: none;
1651
+ border: none;
1652
+ font-size: 20px;
1653
+ cursor: pointer;
1654
+ color: #71717A;
1655
+ padding: 4px;
1656
+ ">&times;</button>
1657
+ <h2 style="font-size: 20px; font-weight: 700; margin-bottom: 8px; color: ${textColor};">
1658
+ ${form.headline || 'Stay in touch'}
1659
+ </h2>
1660
+ <p style="font-size: 14px; color: #71717A; margin-bottom: 16px;">
1661
+ ${form.subheadline || 'Get the latest updates'}
1662
+ </p>
1663
+ <form id="clianta-form-element">
1664
+ ${fieldsHTML}
1665
+ <button type="submit" style="
1666
+ width: 100%;
1667
+ padding: 10px 16px;
1668
+ background: ${primaryColor};
1669
+ color: white;
1670
+ border: none;
1671
+ border-radius: 6px;
1672
+ font-size: 14px;
1673
+ font-weight: 500;
1674
+ cursor: pointer;
1675
+ margin-top: 8px;
1676
+ ">
1677
+ ${form.submitButtonText || 'Subscribe'}
1678
+ </button>
1679
+ </form>
1680
+ `;
1681
+ }
1682
+ setupFormEvents(form, overlay, container) {
1683
+ // Close button
1684
+ const closeBtn = container.querySelector('#clianta-form-close');
1685
+ if (closeBtn) {
1686
+ closeBtn.addEventListener('click', () => this.closeForm(form._id, overlay, container));
1687
+ }
1688
+ // Overlay click
1689
+ overlay.addEventListener('click', (e) => {
1690
+ if (e.target === overlay) {
1691
+ this.closeForm(form._id, overlay, container);
1692
+ }
1693
+ });
1694
+ // Form submit
1695
+ const formElement = container.querySelector('#clianta-form-element');
1696
+ if (formElement) {
1697
+ formElement.addEventListener('submit', async (e) => {
1698
+ e.preventDefault();
1699
+ await this.handleSubmit(form, formElement, container);
1700
+ });
1701
+ }
1702
+ }
1703
+ closeForm(formId, overlay, container) {
1704
+ container.style.transform = 'translateY(20px)';
1705
+ container.style.opacity = '0';
1706
+ overlay.style.opacity = '0';
1707
+ setTimeout(() => {
1708
+ overlay.remove();
1709
+ }, 300);
1710
+ }
1711
+ async handleSubmit(form, formElement, container) {
1712
+ if (!this.tracker)
1713
+ return;
1714
+ const config = this.tracker.getConfig();
1715
+ const apiEndpoint = config.apiEndpoint || 'https://api.clianta.online';
1716
+ const visitorId = this.tracker.getVisitorId();
1717
+ // Collect form data
1718
+ const formData = new FormData(formElement);
1719
+ const data = {};
1720
+ formData.forEach((value, key) => {
1721
+ data[key] = value;
1722
+ });
1723
+ // Disable submit button
1724
+ const submitBtn = formElement.querySelector('button[type="submit"]');
1725
+ if (submitBtn) {
1726
+ submitBtn.disabled = true;
1727
+ submitBtn.innerHTML = 'Submitting...';
1728
+ }
1729
+ try {
1730
+ const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${form._id}/submit`, {
1731
+ method: 'POST',
1732
+ headers: { 'Content-Type': 'application/json' },
1733
+ body: JSON.stringify({
1734
+ visitorId,
1735
+ data,
1736
+ url: window.location.href,
1737
+ }),
1738
+ });
1739
+ const result = await response.json();
1740
+ if (result.success) {
1741
+ // Show success message
1742
+ container.innerHTML = `
1743
+ <div style="text-align: center; padding: 20px;">
1744
+ <div style="width: 48px; height: 48px; background: #10B981; border-radius: 50%; margin: 0 auto 16px; display: flex; align-items: center; justify-content: center;">
1745
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
1746
+ <polyline points="20 6 9 17 4 12"></polyline>
1747
+ </svg>
1748
+ </div>
1749
+ <p style="font-size: 16px; font-weight: 500; color: #18181B;">
1750
+ ${form.successMessage || 'Thank you!'}
1751
+ </p>
1752
+ </div>
1753
+ `;
1754
+ // Track identify
1755
+ if (data.email) {
1756
+ this.tracker?.identify(data.email, data);
1757
+ }
1758
+ // Redirect if configured
1759
+ if (form.redirectUrl) {
1760
+ setTimeout(() => {
1761
+ window.location.href = form.redirectUrl;
1762
+ }, 1500);
1763
+ }
1764
+ // Close after delay
1765
+ setTimeout(() => {
1766
+ const overlay = document.getElementById(`clianta-form-overlay-${form._id}`);
1767
+ if (overlay) {
1768
+ this.closeForm(form._id, overlay, container);
1769
+ }
1770
+ }, 2000);
1771
+ }
1772
+ }
1773
+ catch (error) {
1774
+ console.error('[Clianta] Form submit error:', error);
1775
+ if (submitBtn) {
1776
+ submitBtn.disabled = false;
1777
+ submitBtn.innerHTML = form.submitButtonText || 'Subscribe';
1778
+ }
1779
+ }
1780
+ }
1781
+ }
1782
+
1783
+ /**
1784
+ * Clianta SDK - Plugins Index
1785
+ * Version is defined in core/config.ts as SDK_VERSION
1371
1786
  */
1372
1787
  /**
1373
1788
  * Get plugin instance by name
@@ -1392,17 +1807,256 @@
1392
1807
  return new ErrorsPlugin();
1393
1808
  case 'performance':
1394
1809
  return new PerformancePlugin();
1810
+ case 'popupForms':
1811
+ return new PopupFormsPlugin();
1395
1812
  default:
1396
1813
  throw new Error(`Unknown plugin: ${name}`);
1397
1814
  }
1398
1815
  }
1399
1816
 
1400
1817
  /**
1401
- * MorrisB Tracking SDK - Main Tracker Class
1402
- * @version 3.0.0
1818
+ * Clianta SDK - Consent Storage
1819
+ * Handles persistence of consent state
1820
+ * @see SDK_VERSION in core/config.ts
1821
+ */
1822
+ const CONSENT_VERSION = 1;
1823
+ /**
1824
+ * Save consent state to storage
1825
+ */
1826
+ function saveConsent(state) {
1827
+ try {
1828
+ if (typeof localStorage === 'undefined')
1829
+ return false;
1830
+ const stored = {
1831
+ state,
1832
+ timestamp: Date.now(),
1833
+ version: CONSENT_VERSION,
1834
+ };
1835
+ localStorage.setItem(STORAGE_KEYS.CONSENT, JSON.stringify(stored));
1836
+ return true;
1837
+ }
1838
+ catch {
1839
+ return false;
1840
+ }
1841
+ }
1842
+ /**
1843
+ * Load consent state from storage
1844
+ */
1845
+ function loadConsent() {
1846
+ try {
1847
+ if (typeof localStorage === 'undefined')
1848
+ return null;
1849
+ const stored = localStorage.getItem(STORAGE_KEYS.CONSENT);
1850
+ if (!stored)
1851
+ return null;
1852
+ const parsed = JSON.parse(stored);
1853
+ // Validate version
1854
+ if (parsed.version !== CONSENT_VERSION) {
1855
+ clearConsent();
1856
+ return null;
1857
+ }
1858
+ return parsed;
1859
+ }
1860
+ catch {
1861
+ return null;
1862
+ }
1863
+ }
1864
+ /**
1865
+ * Clear consent state from storage
1866
+ */
1867
+ function clearConsent() {
1868
+ try {
1869
+ if (typeof localStorage === 'undefined')
1870
+ return false;
1871
+ localStorage.removeItem(STORAGE_KEYS.CONSENT);
1872
+ return true;
1873
+ }
1874
+ catch {
1875
+ return false;
1876
+ }
1877
+ }
1878
+ /**
1879
+ * Check if consent has been explicitly set
1880
+ */
1881
+ function hasStoredConsent() {
1882
+ return loadConsent() !== null;
1883
+ }
1884
+
1885
+ /**
1886
+ * Clianta SDK - Consent Manager
1887
+ * Manages consent state and event buffering for GDPR/CCPA compliance
1888
+ * @see SDK_VERSION in core/config.ts
1403
1889
  */
1404
1890
  /**
1405
- * Main MorrisB Tracker Class
1891
+ * Manages user consent state for tracking
1892
+ */
1893
+ class ConsentManager {
1894
+ constructor(config = {}) {
1895
+ this.eventBuffer = [];
1896
+ this.callbacks = [];
1897
+ this.hasExplicitConsent = false;
1898
+ this.config = {
1899
+ defaultConsent: { analytics: true, marketing: false, personalization: false },
1900
+ waitForConsent: false,
1901
+ storageKey: 'mb_consent',
1902
+ ...config,
1903
+ };
1904
+ // Load stored consent or use default
1905
+ const stored = loadConsent();
1906
+ if (stored) {
1907
+ this.state = stored.state;
1908
+ this.hasExplicitConsent = true;
1909
+ logger.debug('Loaded stored consent:', this.state);
1910
+ }
1911
+ else {
1912
+ this.state = this.config.defaultConsent || { analytics: true };
1913
+ this.hasExplicitConsent = false;
1914
+ logger.debug('Using default consent:', this.state);
1915
+ }
1916
+ // Register callback if provided
1917
+ if (config.onConsentChange) {
1918
+ this.callbacks.push(config.onConsentChange);
1919
+ }
1920
+ }
1921
+ /**
1922
+ * Grant consent for specified categories
1923
+ */
1924
+ grant(categories) {
1925
+ const previous = { ...this.state };
1926
+ this.state = { ...this.state, ...categories };
1927
+ this.hasExplicitConsent = true;
1928
+ saveConsent(this.state);
1929
+ logger.info('Consent granted:', categories);
1930
+ this.notifyChange(previous);
1931
+ }
1932
+ /**
1933
+ * Revoke consent for specified categories
1934
+ */
1935
+ revoke(categories) {
1936
+ const previous = { ...this.state };
1937
+ for (const category of categories) {
1938
+ this.state[category] = false;
1939
+ }
1940
+ this.hasExplicitConsent = true;
1941
+ saveConsent(this.state);
1942
+ logger.info('Consent revoked:', categories);
1943
+ this.notifyChange(previous);
1944
+ }
1945
+ /**
1946
+ * Update entire consent state
1947
+ */
1948
+ update(state) {
1949
+ const previous = { ...this.state };
1950
+ this.state = { ...state };
1951
+ this.hasExplicitConsent = true;
1952
+ saveConsent(this.state);
1953
+ logger.info('Consent updated:', this.state);
1954
+ this.notifyChange(previous);
1955
+ }
1956
+ /**
1957
+ * Reset consent to default (clear stored consent)
1958
+ */
1959
+ reset() {
1960
+ const previous = { ...this.state };
1961
+ this.state = this.config.defaultConsent || { analytics: true };
1962
+ this.hasExplicitConsent = false;
1963
+ this.eventBuffer = [];
1964
+ clearConsent();
1965
+ logger.info('Consent reset to defaults');
1966
+ this.notifyChange(previous);
1967
+ }
1968
+ /**
1969
+ * Get current consent state
1970
+ */
1971
+ getState() {
1972
+ return { ...this.state };
1973
+ }
1974
+ /**
1975
+ * Check if a specific consent category is granted
1976
+ */
1977
+ hasConsent(category) {
1978
+ return this.state[category] === true;
1979
+ }
1980
+ /**
1981
+ * Check if analytics consent is granted (most common check)
1982
+ */
1983
+ canTrack() {
1984
+ // If waiting for consent and no explicit consent given, cannot track
1985
+ if (this.config.waitForConsent && !this.hasExplicitConsent) {
1986
+ return false;
1987
+ }
1988
+ return this.state.analytics === true;
1989
+ }
1990
+ /**
1991
+ * Check if explicit consent has been given
1992
+ */
1993
+ hasExplicit() {
1994
+ return this.hasExplicitConsent;
1995
+ }
1996
+ /**
1997
+ * Check if there's stored consent
1998
+ */
1999
+ hasStored() {
2000
+ return hasStoredConsent();
2001
+ }
2002
+ /**
2003
+ * Buffer an event (for waitForConsent mode)
2004
+ */
2005
+ bufferEvent(event) {
2006
+ this.eventBuffer.push(event);
2007
+ logger.debug('Event buffered (waiting for consent):', event.eventName);
2008
+ }
2009
+ /**
2010
+ * Get and clear buffered events
2011
+ */
2012
+ flushBuffer() {
2013
+ const events = [...this.eventBuffer];
2014
+ this.eventBuffer = [];
2015
+ if (events.length > 0) {
2016
+ logger.debug(`Flushing ${events.length} buffered events`);
2017
+ }
2018
+ return events;
2019
+ }
2020
+ /**
2021
+ * Get buffered event count
2022
+ */
2023
+ getBufferSize() {
2024
+ return this.eventBuffer.length;
2025
+ }
2026
+ /**
2027
+ * Register a consent change callback
2028
+ */
2029
+ onChange(callback) {
2030
+ this.callbacks.push(callback);
2031
+ // Return unsubscribe function
2032
+ return () => {
2033
+ const index = this.callbacks.indexOf(callback);
2034
+ if (index > -1) {
2035
+ this.callbacks.splice(index, 1);
2036
+ }
2037
+ };
2038
+ }
2039
+ /**
2040
+ * Notify all callbacks of consent change
2041
+ */
2042
+ notifyChange(previous) {
2043
+ for (const callback of this.callbacks) {
2044
+ try {
2045
+ callback(this.state, previous);
2046
+ }
2047
+ catch (error) {
2048
+ logger.error('Consent change callback error:', error);
2049
+ }
2050
+ }
2051
+ }
2052
+ }
2053
+
2054
+ /**
2055
+ * Clianta SDK - Main Tracker Class
2056
+ * @see SDK_VERSION in core/config.ts
2057
+ */
2058
+ /**
2059
+ * Main Clianta Tracker Class
1406
2060
  */
1407
2061
  class Tracker {
1408
2062
  constructor(workspaceId, userConfig = {}) {
@@ -1416,21 +2070,81 @@
1416
2070
  // Setup debug mode
1417
2071
  logger.enabled = this.config.debug;
1418
2072
  logger.info(`Initializing SDK v${SDK_VERSION}`, { workspaceId });
2073
+ // Initialize consent manager
2074
+ this.consentManager = new ConsentManager({
2075
+ ...this.config.consent,
2076
+ onConsentChange: (state, previous) => {
2077
+ this.onConsentChange(state, previous);
2078
+ },
2079
+ });
1419
2080
  // Initialize transport and queue
1420
2081
  this.transport = new Transport({ apiEndpoint: this.config.apiEndpoint });
1421
2082
  this.queue = new EventQueue(this.transport, {
1422
2083
  batchSize: this.config.batchSize,
1423
2084
  flushInterval: this.config.flushInterval,
1424
2085
  });
1425
- // Get or create visitor and session IDs
1426
- this.visitorId = getOrCreateVisitorId(this.config.useCookies);
1427
- this.sessionId = getOrCreateSessionId(this.config.sessionTimeout);
2086
+ // Get or create visitor and session IDs based on mode
2087
+ this.visitorId = this.createVisitorId();
2088
+ this.sessionId = this.createSessionId();
1428
2089
  logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
1429
2090
  // Initialize plugins
1430
2091
  this.initPlugins();
1431
2092
  this.isInitialized = true;
1432
2093
  logger.info('SDK initialized successfully');
1433
2094
  }
2095
+ /**
2096
+ * Create visitor ID based on storage mode
2097
+ */
2098
+ createVisitorId() {
2099
+ // Anonymous mode: use temporary ID until consent
2100
+ if (this.config.consent.anonymousMode && !this.consentManager.hasExplicit()) {
2101
+ const key = STORAGE_KEYS.VISITOR_ID + '_anon';
2102
+ let anonId = getSessionStorage(key);
2103
+ if (!anonId) {
2104
+ anonId = 'anon_' + generateUUID();
2105
+ setSessionStorage(key, anonId);
2106
+ }
2107
+ return anonId;
2108
+ }
2109
+ // Cookie-less mode: use sessionStorage only
2110
+ if (this.config.cookielessMode) {
2111
+ let visitorId = getSessionStorage(STORAGE_KEYS.VISITOR_ID);
2112
+ if (!visitorId) {
2113
+ visitorId = generateUUID();
2114
+ setSessionStorage(STORAGE_KEYS.VISITOR_ID, visitorId);
2115
+ }
2116
+ return visitorId;
2117
+ }
2118
+ // Normal mode
2119
+ return getOrCreateVisitorId(this.config.useCookies);
2120
+ }
2121
+ /**
2122
+ * Create session ID
2123
+ */
2124
+ createSessionId() {
2125
+ return getOrCreateSessionId(this.config.sessionTimeout);
2126
+ }
2127
+ /**
2128
+ * Handle consent state changes
2129
+ */
2130
+ onConsentChange(state, previous) {
2131
+ logger.debug('Consent changed:', { from: previous, to: state });
2132
+ // If analytics consent was just granted
2133
+ if (state.analytics && !previous.analytics) {
2134
+ // Upgrade from anonymous ID to persistent ID
2135
+ if (this.config.consent.anonymousMode) {
2136
+ this.visitorId = getOrCreateVisitorId(this.config.useCookies);
2137
+ logger.info('Upgraded from anonymous to persistent visitor ID');
2138
+ }
2139
+ // Flush buffered events
2140
+ const buffered = this.consentManager.flushBuffer();
2141
+ for (const event of buffered) {
2142
+ // Update event with new visitor ID
2143
+ event.visitorId = this.visitorId;
2144
+ this.queue.push(event);
2145
+ }
2146
+ }
2147
+ }
1434
2148
  /**
1435
2149
  * Initialize enabled plugins
1436
2150
  */
@@ -1474,6 +2188,17 @@
1474
2188
  timestamp: new Date().toISOString(),
1475
2189
  sdkVersion: SDK_VERSION,
1476
2190
  };
2191
+ // Check consent before tracking
2192
+ if (!this.consentManager.canTrack()) {
2193
+ // Buffer event for later if waitForConsent is enabled
2194
+ if (this.config.consent.waitForConsent) {
2195
+ this.consentManager.bufferEvent(event);
2196
+ return;
2197
+ }
2198
+ // Otherwise drop the event
2199
+ logger.debug('Event dropped (no consent):', eventName);
2200
+ return;
2201
+ }
1477
2202
  this.queue.push(event);
1478
2203
  logger.debug('Event tracked:', eventName, properties);
1479
2204
  }
@@ -1513,11 +2238,13 @@
1513
2238
  * Update consent state
1514
2239
  */
1515
2240
  consent(state) {
1516
- logger.info('Consent updated:', state);
1517
- // TODO: Implement consent management
1518
- // - Store consent state
1519
- // - Enable/disable tracking based on consent
1520
- // - Notify plugins
2241
+ this.consentManager.update(state);
2242
+ }
2243
+ /**
2244
+ * Get current consent state
2245
+ */
2246
+ getConsentState() {
2247
+ return this.consentManager.getState();
1521
2248
  }
1522
2249
  /**
1523
2250
  * Toggle debug mode
@@ -1562,10 +2289,49 @@
1562
2289
  reset() {
1563
2290
  logger.info('Resetting visitor data');
1564
2291
  resetIds(this.config.useCookies);
1565
- this.visitorId = getOrCreateVisitorId(this.config.useCookies);
1566
- this.sessionId = getOrCreateSessionId(this.config.sessionTimeout);
2292
+ this.visitorId = this.createVisitorId();
2293
+ this.sessionId = this.createSessionId();
1567
2294
  this.queue.clear();
1568
2295
  }
2296
+ /**
2297
+ * Delete all stored user data (GDPR right-to-erasure)
2298
+ */
2299
+ deleteData() {
2300
+ logger.info('Deleting all user data (GDPR request)');
2301
+ // Clear queue
2302
+ this.queue.clear();
2303
+ // Reset consent
2304
+ this.consentManager.reset();
2305
+ // Clear all stored IDs
2306
+ resetIds(this.config.useCookies);
2307
+ // Clear session storage items
2308
+ if (typeof sessionStorage !== 'undefined') {
2309
+ try {
2310
+ sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
2311
+ sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID + '_anon');
2312
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_ID);
2313
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_TIMESTAMP);
2314
+ }
2315
+ catch {
2316
+ // Ignore errors
2317
+ }
2318
+ }
2319
+ // Clear localStorage items
2320
+ if (typeof localStorage !== 'undefined') {
2321
+ try {
2322
+ localStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
2323
+ localStorage.removeItem(STORAGE_KEYS.CONSENT);
2324
+ localStorage.removeItem(STORAGE_KEYS.EVENT_QUEUE);
2325
+ }
2326
+ catch {
2327
+ // Ignore errors
2328
+ }
2329
+ }
2330
+ // Generate new IDs
2331
+ this.visitorId = this.createVisitorId();
2332
+ this.sessionId = this.createSessionId();
2333
+ logger.info('All user data deleted');
2334
+ }
1569
2335
  /**
1570
2336
  * Destroy tracker and cleanup
1571
2337
  */
@@ -1588,7 +2354,7 @@
1588
2354
 
1589
2355
  /**
1590
2356
  * Clianta SDK - CRM API Client
1591
- * @version 1.0.0
2357
+ * @see SDK_VERSION in core/config.ts
1592
2358
  */
1593
2359
  /**
1594
2360
  * CRM API Client for managing contacts and opportunities
@@ -1762,7 +2528,7 @@
1762
2528
  /**
1763
2529
  * Clianta SDK
1764
2530
  * Professional CRM and tracking SDK for lead generation
1765
- * @version 1.0.0
2531
+ * @see SDK_VERSION in core/config.ts
1766
2532
  */
1767
2533
  // Global instance cache
1768
2534
  let globalInstance = null;
@@ -1779,6 +2545,16 @@
1779
2545
  * debug: true,
1780
2546
  * plugins: ['pageView', 'forms', 'scroll'],
1781
2547
  * });
2548
+ *
2549
+ * @example
2550
+ * // With consent configuration
2551
+ * const tracker = clianta('your-workspace-id', {
2552
+ * consent: {
2553
+ * waitForConsent: true,
2554
+ * anonymousMode: true,
2555
+ * },
2556
+ * cookielessMode: true, // GDPR-friendly mode
2557
+ * });
1782
2558
  */
1783
2559
  function clianta(workspaceId, config) {
1784
2560
  // Return existing instance if same workspace
@@ -1800,10 +2576,12 @@
1800
2576
  clianta,
1801
2577
  Tracker,
1802
2578
  CRMClient,
2579
+ ConsentManager,
1803
2580
  };
1804
2581
  }
1805
2582
 
1806
2583
  exports.CRMClient = CRMClient;
2584
+ exports.ConsentManager = ConsentManager;
1807
2585
  exports.SDK_VERSION = SDK_VERSION;
1808
2586
  exports.Tracker = Tracker;
1809
2587
  exports.clianta = clianta;