@clianta/sdk 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/dist/clianta.cjs.js +571 -50
- package/dist/clianta.cjs.js.map +1 -1
- package/dist/clianta.esm.js +571 -50
- package/dist/clianta.esm.js.map +1 -1
- package/dist/clianta.umd.js +571 -50
- package/dist/clianta.umd.js.map +1 -1
- package/dist/clianta.umd.min.js +2 -2
- package/dist/clianta.umd.min.js.map +1 -1
- package/dist/index.d.ts +244 -5
- package/dist/react.cjs.js +572 -51
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.esm.js +572 -51
- package/dist/react.esm.js.map +1 -1
- package/package.json +1 -1
package/dist/react.cjs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Clianta SDK v1.
|
|
2
|
+
* Clianta SDK v1.2.0
|
|
3
3
|
* (c) 2026 Clianta
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -13,7 +13,7 @@ var react = require('react');
|
|
|
13
13
|
* @see SDK_VERSION in core/config.ts
|
|
14
14
|
*/
|
|
15
15
|
/** SDK Version */
|
|
16
|
-
const SDK_VERSION = '1.
|
|
16
|
+
const SDK_VERSION = '1.2.0';
|
|
17
17
|
/** Default API endpoint based on environment */
|
|
18
18
|
const getDefaultApiEndpoint = () => {
|
|
19
19
|
if (typeof window === 'undefined')
|
|
@@ -577,14 +577,20 @@ function getDeviceInfo() {
|
|
|
577
577
|
* @see SDK_VERSION in core/config.ts
|
|
578
578
|
*/
|
|
579
579
|
const MAX_QUEUE_SIZE = 1000;
|
|
580
|
+
/** Rate limit: max events per window */
|
|
581
|
+
const RATE_LIMIT_MAX_EVENTS = 100;
|
|
582
|
+
/** Rate limit window in ms (1 minute) */
|
|
583
|
+
const RATE_LIMIT_WINDOW_MS = 60000;
|
|
580
584
|
/**
|
|
581
|
-
* Event queue with batching, persistence, and auto-flush
|
|
585
|
+
* Event queue with batching, persistence, rate limiting, and auto-flush
|
|
582
586
|
*/
|
|
583
587
|
class EventQueue {
|
|
584
588
|
constructor(transport, config = {}) {
|
|
585
589
|
this.queue = [];
|
|
586
590
|
this.flushTimer = null;
|
|
587
591
|
this.isFlushing = false;
|
|
592
|
+
/** Rate limiting: timestamps of recent events */
|
|
593
|
+
this.eventTimestamps = [];
|
|
588
594
|
this.transport = transport;
|
|
589
595
|
this.config = {
|
|
590
596
|
batchSize: config.batchSize ?? 10,
|
|
@@ -603,6 +609,11 @@ class EventQueue {
|
|
|
603
609
|
* Add an event to the queue
|
|
604
610
|
*/
|
|
605
611
|
push(event) {
|
|
612
|
+
// Rate limiting check
|
|
613
|
+
if (!this.checkRateLimit()) {
|
|
614
|
+
logger.warn('Rate limit exceeded, event dropped:', event.eventName);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
606
617
|
// Don't exceed max queue size
|
|
607
618
|
if (this.queue.length >= this.config.maxQueueSize) {
|
|
608
619
|
logger.warn('Queue full, dropping oldest event');
|
|
@@ -615,6 +626,22 @@ class EventQueue {
|
|
|
615
626
|
this.flush();
|
|
616
627
|
}
|
|
617
628
|
}
|
|
629
|
+
/**
|
|
630
|
+
* Check and enforce rate limiting
|
|
631
|
+
* @returns true if event is allowed, false if rate limited
|
|
632
|
+
*/
|
|
633
|
+
checkRateLimit() {
|
|
634
|
+
const now = Date.now();
|
|
635
|
+
// Remove timestamps outside the window
|
|
636
|
+
this.eventTimestamps = this.eventTimestamps.filter(ts => now - ts < RATE_LIMIT_WINDOW_MS);
|
|
637
|
+
// Check if under limit
|
|
638
|
+
if (this.eventTimestamps.length >= RATE_LIMIT_MAX_EVENTS) {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
// Record this event
|
|
642
|
+
this.eventTimestamps.push(now);
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
618
645
|
/**
|
|
619
646
|
* Flush the queue (send all events)
|
|
620
647
|
*/
|
|
@@ -623,9 +650,10 @@ class EventQueue {
|
|
|
623
650
|
return;
|
|
624
651
|
}
|
|
625
652
|
this.isFlushing = true;
|
|
653
|
+
// Atomically take snapshot of current queue length to avoid race condition
|
|
654
|
+
const count = this.queue.length;
|
|
655
|
+
const events = this.queue.splice(0, count);
|
|
626
656
|
try {
|
|
627
|
-
// Take all events from queue
|
|
628
|
-
const events = this.queue.splice(0, this.queue.length);
|
|
629
657
|
logger.debug(`Flushing ${events.length} events`);
|
|
630
658
|
// Clear persisted queue
|
|
631
659
|
this.persistQueue([]);
|
|
@@ -783,6 +811,9 @@ class PageViewPlugin extends BasePlugin {
|
|
|
783
811
|
constructor() {
|
|
784
812
|
super(...arguments);
|
|
785
813
|
this.name = 'pageView';
|
|
814
|
+
this.originalPushState = null;
|
|
815
|
+
this.originalReplaceState = null;
|
|
816
|
+
this.popstateHandler = null;
|
|
786
817
|
}
|
|
787
818
|
init(tracker) {
|
|
788
819
|
super.init(tracker);
|
|
@@ -790,22 +821,40 @@ class PageViewPlugin extends BasePlugin {
|
|
|
790
821
|
this.trackPageView();
|
|
791
822
|
// Track SPA navigation (History API)
|
|
792
823
|
if (typeof window !== 'undefined') {
|
|
824
|
+
// Store originals for cleanup
|
|
825
|
+
this.originalPushState = history.pushState;
|
|
826
|
+
this.originalReplaceState = history.replaceState;
|
|
793
827
|
// Intercept pushState and replaceState
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
this.trackPageView();
|
|
828
|
+
const self = this;
|
|
829
|
+
history.pushState = function (...args) {
|
|
830
|
+
self.originalPushState.apply(history, args);
|
|
831
|
+
self.trackPageView();
|
|
799
832
|
};
|
|
800
|
-
history.replaceState = (...args)
|
|
801
|
-
originalReplaceState.apply(history, args);
|
|
802
|
-
|
|
833
|
+
history.replaceState = function (...args) {
|
|
834
|
+
self.originalReplaceState.apply(history, args);
|
|
835
|
+
self.trackPageView();
|
|
803
836
|
};
|
|
804
837
|
// Handle back/forward navigation
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
838
|
+
this.popstateHandler = () => this.trackPageView();
|
|
839
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
destroy() {
|
|
843
|
+
// Restore original history methods
|
|
844
|
+
if (this.originalPushState) {
|
|
845
|
+
history.pushState = this.originalPushState;
|
|
846
|
+
this.originalPushState = null;
|
|
847
|
+
}
|
|
848
|
+
if (this.originalReplaceState) {
|
|
849
|
+
history.replaceState = this.originalReplaceState;
|
|
850
|
+
this.originalReplaceState = null;
|
|
851
|
+
}
|
|
852
|
+
// Remove popstate listener
|
|
853
|
+
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
854
|
+
window.removeEventListener('popstate', this.popstateHandler);
|
|
855
|
+
this.popstateHandler = null;
|
|
808
856
|
}
|
|
857
|
+
super.destroy();
|
|
809
858
|
}
|
|
810
859
|
trackPageView() {
|
|
811
860
|
if (typeof window === 'undefined' || typeof document === 'undefined')
|
|
@@ -868,7 +917,11 @@ class ScrollPlugin extends BasePlugin {
|
|
|
868
917
|
const windowHeight = window.innerHeight;
|
|
869
918
|
const documentHeight = document.documentElement.scrollHeight;
|
|
870
919
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
871
|
-
const
|
|
920
|
+
const scrollableHeight = documentHeight - windowHeight;
|
|
921
|
+
// Guard against divide-by-zero on short pages
|
|
922
|
+
if (scrollableHeight <= 0)
|
|
923
|
+
return;
|
|
924
|
+
const scrollPercent = Math.floor((scrollTop / scrollableHeight) * 100);
|
|
872
925
|
// Clamp to valid range
|
|
873
926
|
const clampedPercent = Math.max(0, Math.min(100, scrollPercent));
|
|
874
927
|
// Update max scroll depth
|
|
@@ -1287,20 +1340,41 @@ class PerformancePlugin extends BasePlugin {
|
|
|
1287
1340
|
trackPerformance() {
|
|
1288
1341
|
if (typeof performance === 'undefined')
|
|
1289
1342
|
return;
|
|
1290
|
-
// Use Navigation Timing API
|
|
1291
|
-
const
|
|
1292
|
-
if (
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1343
|
+
// Use modern Navigation Timing API (PerformanceNavigationTiming)
|
|
1344
|
+
const entries = performance.getEntriesByType('navigation');
|
|
1345
|
+
if (entries.length > 0) {
|
|
1346
|
+
const navTiming = entries[0];
|
|
1347
|
+
const loadTime = Math.round(navTiming.loadEventEnd - navTiming.startTime);
|
|
1348
|
+
const domReady = Math.round(navTiming.domContentLoadedEventEnd - navTiming.startTime);
|
|
1349
|
+
const ttfb = Math.round(navTiming.responseStart - navTiming.requestStart);
|
|
1350
|
+
const domInteractive = Math.round(navTiming.domInteractive - navTiming.startTime);
|
|
1351
|
+
this.track('performance', 'Page Performance', {
|
|
1352
|
+
loadTime,
|
|
1353
|
+
domReady,
|
|
1354
|
+
ttfb, // Time to First Byte
|
|
1355
|
+
domInteractive,
|
|
1356
|
+
// Additional modern metrics
|
|
1357
|
+
dns: Math.round(navTiming.domainLookupEnd - navTiming.domainLookupStart),
|
|
1358
|
+
connection: Math.round(navTiming.connectEnd - navTiming.connectStart),
|
|
1359
|
+
transferSize: navTiming.transferSize,
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
else {
|
|
1363
|
+
// Fallback for older browsers using deprecated API
|
|
1364
|
+
const timing = performance.timing;
|
|
1365
|
+
if (!timing)
|
|
1366
|
+
return;
|
|
1367
|
+
const loadTime = timing.loadEventEnd - timing.navigationStart;
|
|
1368
|
+
const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;
|
|
1369
|
+
const ttfb = timing.responseStart - timing.navigationStart;
|
|
1370
|
+
const domInteractive = timing.domInteractive - timing.navigationStart;
|
|
1371
|
+
this.track('performance', 'Page Performance', {
|
|
1372
|
+
loadTime,
|
|
1373
|
+
domReady,
|
|
1374
|
+
ttfb,
|
|
1375
|
+
domInteractive,
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1304
1378
|
// Track Web Vitals if available
|
|
1305
1379
|
this.trackWebVitals();
|
|
1306
1380
|
}
|
|
@@ -1577,8 +1651,8 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1577
1651
|
opacity: 0;
|
|
1578
1652
|
transition: all 0.3s ease;
|
|
1579
1653
|
`;
|
|
1580
|
-
// Build form
|
|
1581
|
-
|
|
1654
|
+
// Build form using safe DOM APIs (no innerHTML for user content)
|
|
1655
|
+
this.buildFormDOM(form, container);
|
|
1582
1656
|
overlay.appendChild(container);
|
|
1583
1657
|
document.body.appendChild(overlay);
|
|
1584
1658
|
// Animate in
|
|
@@ -1590,6 +1664,131 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1590
1664
|
// Setup event listeners
|
|
1591
1665
|
this.setupFormEvents(form, overlay, container);
|
|
1592
1666
|
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Escape HTML to prevent XSS - used only for static structure
|
|
1669
|
+
*/
|
|
1670
|
+
escapeHTML(str) {
|
|
1671
|
+
const div = document.createElement('div');
|
|
1672
|
+
div.textContent = str;
|
|
1673
|
+
return div.innerHTML;
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Build form using safe DOM APIs (prevents XSS)
|
|
1677
|
+
*/
|
|
1678
|
+
buildFormDOM(form, container) {
|
|
1679
|
+
const style = form.style || {};
|
|
1680
|
+
const primaryColor = style.primaryColor || '#10B981';
|
|
1681
|
+
const textColor = style.textColor || '#18181B';
|
|
1682
|
+
// Close button
|
|
1683
|
+
const closeBtn = document.createElement('button');
|
|
1684
|
+
closeBtn.id = 'clianta-form-close';
|
|
1685
|
+
closeBtn.style.cssText = `
|
|
1686
|
+
position: absolute;
|
|
1687
|
+
top: 12px;
|
|
1688
|
+
right: 12px;
|
|
1689
|
+
background: none;
|
|
1690
|
+
border: none;
|
|
1691
|
+
font-size: 20px;
|
|
1692
|
+
cursor: pointer;
|
|
1693
|
+
color: #71717A;
|
|
1694
|
+
padding: 4px;
|
|
1695
|
+
`;
|
|
1696
|
+
closeBtn.textContent = '×';
|
|
1697
|
+
container.appendChild(closeBtn);
|
|
1698
|
+
// Headline
|
|
1699
|
+
const headline = document.createElement('h2');
|
|
1700
|
+
headline.style.cssText = `font-size: 20px; font-weight: 700; margin-bottom: 8px; color: ${this.escapeHTML(textColor)};`;
|
|
1701
|
+
headline.textContent = form.headline || 'Stay in touch';
|
|
1702
|
+
container.appendChild(headline);
|
|
1703
|
+
// Subheadline
|
|
1704
|
+
const subheadline = document.createElement('p');
|
|
1705
|
+
subheadline.style.cssText = 'font-size: 14px; color: #71717A; margin-bottom: 16px;';
|
|
1706
|
+
subheadline.textContent = form.subheadline || 'Get the latest updates';
|
|
1707
|
+
container.appendChild(subheadline);
|
|
1708
|
+
// Form element
|
|
1709
|
+
const formElement = document.createElement('form');
|
|
1710
|
+
formElement.id = 'clianta-form-element';
|
|
1711
|
+
// Build fields
|
|
1712
|
+
form.fields.forEach(field => {
|
|
1713
|
+
const fieldWrapper = document.createElement('div');
|
|
1714
|
+
fieldWrapper.style.marginBottom = '12px';
|
|
1715
|
+
if (field.type === 'checkbox') {
|
|
1716
|
+
// Checkbox layout
|
|
1717
|
+
const label = document.createElement('label');
|
|
1718
|
+
label.style.cssText = `display: flex; align-items: center; gap: 8px; font-size: 14px; color: ${this.escapeHTML(textColor)}; cursor: pointer;`;
|
|
1719
|
+
const input = document.createElement('input');
|
|
1720
|
+
input.type = 'checkbox';
|
|
1721
|
+
input.name = field.name;
|
|
1722
|
+
if (field.required)
|
|
1723
|
+
input.required = true;
|
|
1724
|
+
input.style.cssText = 'width: 16px; height: 16px;';
|
|
1725
|
+
label.appendChild(input);
|
|
1726
|
+
const labelText = document.createTextNode(field.label + ' ');
|
|
1727
|
+
label.appendChild(labelText);
|
|
1728
|
+
if (field.required) {
|
|
1729
|
+
const requiredMark = document.createElement('span');
|
|
1730
|
+
requiredMark.style.color = '#EF4444';
|
|
1731
|
+
requiredMark.textContent = '*';
|
|
1732
|
+
label.appendChild(requiredMark);
|
|
1733
|
+
}
|
|
1734
|
+
fieldWrapper.appendChild(label);
|
|
1735
|
+
}
|
|
1736
|
+
else {
|
|
1737
|
+
// Label
|
|
1738
|
+
const label = document.createElement('label');
|
|
1739
|
+
label.style.cssText = `display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${this.escapeHTML(textColor)};`;
|
|
1740
|
+
label.textContent = field.label + ' ';
|
|
1741
|
+
if (field.required) {
|
|
1742
|
+
const requiredMark = document.createElement('span');
|
|
1743
|
+
requiredMark.style.color = '#EF4444';
|
|
1744
|
+
requiredMark.textContent = '*';
|
|
1745
|
+
label.appendChild(requiredMark);
|
|
1746
|
+
}
|
|
1747
|
+
fieldWrapper.appendChild(label);
|
|
1748
|
+
// Input/Textarea
|
|
1749
|
+
if (field.type === 'textarea') {
|
|
1750
|
+
const textarea = document.createElement('textarea');
|
|
1751
|
+
textarea.name = field.name;
|
|
1752
|
+
if (field.placeholder)
|
|
1753
|
+
textarea.placeholder = field.placeholder;
|
|
1754
|
+
if (field.required)
|
|
1755
|
+
textarea.required = true;
|
|
1756
|
+
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;';
|
|
1757
|
+
fieldWrapper.appendChild(textarea);
|
|
1758
|
+
}
|
|
1759
|
+
else {
|
|
1760
|
+
const input = document.createElement('input');
|
|
1761
|
+
input.type = field.type;
|
|
1762
|
+
input.name = field.name;
|
|
1763
|
+
if (field.placeholder)
|
|
1764
|
+
input.placeholder = field.placeholder;
|
|
1765
|
+
if (field.required)
|
|
1766
|
+
input.required = true;
|
|
1767
|
+
input.style.cssText = 'width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box;';
|
|
1768
|
+
fieldWrapper.appendChild(input);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
formElement.appendChild(fieldWrapper);
|
|
1772
|
+
});
|
|
1773
|
+
// Submit button
|
|
1774
|
+
const submitBtn = document.createElement('button');
|
|
1775
|
+
submitBtn.type = 'submit';
|
|
1776
|
+
submitBtn.style.cssText = `
|
|
1777
|
+
width: 100%;
|
|
1778
|
+
padding: 10px 16px;
|
|
1779
|
+
background: ${this.escapeHTML(primaryColor)};
|
|
1780
|
+
color: white;
|
|
1781
|
+
border: none;
|
|
1782
|
+
border-radius: 6px;
|
|
1783
|
+
font-size: 14px;
|
|
1784
|
+
font-weight: 500;
|
|
1785
|
+
cursor: pointer;
|
|
1786
|
+
margin-top: 8px;
|
|
1787
|
+
`;
|
|
1788
|
+
submitBtn.textContent = form.submitButtonText || 'Subscribe';
|
|
1789
|
+
formElement.appendChild(submitBtn);
|
|
1790
|
+
container.appendChild(formElement);
|
|
1791
|
+
}
|
|
1593
1792
|
buildFormHTML(form) {
|
|
1594
1793
|
const style = form.style || {};
|
|
1595
1794
|
const primaryColor = style.primaryColor || '#10B981';
|
|
@@ -1739,19 +1938,29 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1739
1938
|
});
|
|
1740
1939
|
const result = await response.json();
|
|
1741
1940
|
if (result.success) {
|
|
1742
|
-
// Show success message
|
|
1743
|
-
container.innerHTML =
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1941
|
+
// Show success message using safe DOM APIs
|
|
1942
|
+
container.innerHTML = '';
|
|
1943
|
+
const successWrapper = document.createElement('div');
|
|
1944
|
+
successWrapper.style.cssText = 'text-align: center; padding: 20px;';
|
|
1945
|
+
const iconWrapper = document.createElement('div');
|
|
1946
|
+
iconWrapper.style.cssText = 'width: 48px; height: 48px; background: #10B981; border-radius: 50%; margin: 0 auto 16px; display: flex; align-items: center; justify-content: center;';
|
|
1947
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
1948
|
+
svg.setAttribute('width', '24');
|
|
1949
|
+
svg.setAttribute('height', '24');
|
|
1950
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
1951
|
+
svg.setAttribute('fill', 'none');
|
|
1952
|
+
svg.setAttribute('stroke', 'white');
|
|
1953
|
+
svg.setAttribute('stroke-width', '2');
|
|
1954
|
+
const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
|
1955
|
+
polyline.setAttribute('points', '20 6 9 17 4 12');
|
|
1956
|
+
svg.appendChild(polyline);
|
|
1957
|
+
iconWrapper.appendChild(svg);
|
|
1958
|
+
const message = document.createElement('p');
|
|
1959
|
+
message.style.cssText = 'font-size: 16px; font-weight: 500; color: #18181B;';
|
|
1960
|
+
message.textContent = form.successMessage || 'Thank you!';
|
|
1961
|
+
successWrapper.appendChild(iconWrapper);
|
|
1962
|
+
successWrapper.appendChild(message);
|
|
1963
|
+
container.appendChild(successWrapper);
|
|
1755
1964
|
// Track identify
|
|
1756
1965
|
if (data.email) {
|
|
1757
1966
|
this.tracker?.identify(data.email, data);
|
|
@@ -1888,6 +2097,8 @@ function hasStoredConsent() {
|
|
|
1888
2097
|
* Manages consent state and event buffering for GDPR/CCPA compliance
|
|
1889
2098
|
* @see SDK_VERSION in core/config.ts
|
|
1890
2099
|
*/
|
|
2100
|
+
/** Maximum events to buffer while waiting for consent */
|
|
2101
|
+
const MAX_BUFFER_SIZE = 100;
|
|
1891
2102
|
/**
|
|
1892
2103
|
* Manages user consent state for tracking
|
|
1893
2104
|
*/
|
|
@@ -2004,6 +2215,11 @@ class ConsentManager {
|
|
|
2004
2215
|
* Buffer an event (for waitForConsent mode)
|
|
2005
2216
|
*/
|
|
2006
2217
|
bufferEvent(event) {
|
|
2218
|
+
// Prevent unbounded buffer growth
|
|
2219
|
+
if (this.eventBuffer.length >= MAX_BUFFER_SIZE) {
|
|
2220
|
+
logger.warn('Consent event buffer full, dropping oldest event');
|
|
2221
|
+
this.eventBuffer.shift();
|
|
2222
|
+
}
|
|
2007
2223
|
this.eventBuffer.push(event);
|
|
2008
2224
|
logger.debug('Event buffered (waiting for consent):', event.eventName);
|
|
2009
2225
|
}
|
|
@@ -2148,6 +2364,7 @@ class Tracker {
|
|
|
2148
2364
|
}
|
|
2149
2365
|
/**
|
|
2150
2366
|
* Initialize enabled plugins
|
|
2367
|
+
* Handles both sync and async plugin init methods
|
|
2151
2368
|
*/
|
|
2152
2369
|
initPlugins() {
|
|
2153
2370
|
const pluginsToLoad = this.config.plugins;
|
|
@@ -2158,7 +2375,13 @@ class Tracker {
|
|
|
2158
2375
|
for (const pluginName of filteredPlugins) {
|
|
2159
2376
|
try {
|
|
2160
2377
|
const plugin = getPlugin(pluginName);
|
|
2161
|
-
|
|
2378
|
+
// Handle both sync and async init (fire-and-forget for async)
|
|
2379
|
+
const result = plugin.init(this);
|
|
2380
|
+
if (result instanceof Promise) {
|
|
2381
|
+
result.catch((error) => {
|
|
2382
|
+
logger.error(`Async plugin init failed: ${pluginName}`, error);
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2162
2385
|
this.plugins.push(plugin);
|
|
2163
2386
|
logger.debug(`Plugin loaded: ${pluginName}`);
|
|
2164
2387
|
}
|
|
@@ -2336,10 +2559,10 @@ class Tracker {
|
|
|
2336
2559
|
/**
|
|
2337
2560
|
* Destroy tracker and cleanup
|
|
2338
2561
|
*/
|
|
2339
|
-
destroy() {
|
|
2562
|
+
async destroy() {
|
|
2340
2563
|
logger.info('Destroying tracker');
|
|
2341
|
-
// Flush any remaining events
|
|
2342
|
-
this.queue.flush();
|
|
2564
|
+
// Flush any remaining events (await to ensure completion)
|
|
2565
|
+
await this.queue.flush();
|
|
2343
2566
|
// Destroy plugins
|
|
2344
2567
|
for (const plugin of this.plugins) {
|
|
2345
2568
|
if (plugin.destroy) {
|
|
@@ -2524,6 +2747,304 @@ class CRMClient {
|
|
|
2524
2747
|
body: JSON.stringify({ stageId }),
|
|
2525
2748
|
});
|
|
2526
2749
|
}
|
|
2750
|
+
// ============================================
|
|
2751
|
+
// COMPANIES API
|
|
2752
|
+
// ============================================
|
|
2753
|
+
/**
|
|
2754
|
+
* Get all companies with pagination
|
|
2755
|
+
*/
|
|
2756
|
+
async getCompanies(params) {
|
|
2757
|
+
const queryParams = new URLSearchParams();
|
|
2758
|
+
if (params?.page)
|
|
2759
|
+
queryParams.set('page', params.page.toString());
|
|
2760
|
+
if (params?.limit)
|
|
2761
|
+
queryParams.set('limit', params.limit.toString());
|
|
2762
|
+
if (params?.search)
|
|
2763
|
+
queryParams.set('search', params.search);
|
|
2764
|
+
if (params?.status)
|
|
2765
|
+
queryParams.set('status', params.status);
|
|
2766
|
+
if (params?.industry)
|
|
2767
|
+
queryParams.set('industry', params.industry);
|
|
2768
|
+
const query = queryParams.toString();
|
|
2769
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/companies${query ? `?${query}` : ''}`;
|
|
2770
|
+
return this.request(endpoint);
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* Get a single company by ID
|
|
2774
|
+
*/
|
|
2775
|
+
async getCompany(companyId) {
|
|
2776
|
+
return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`);
|
|
2777
|
+
}
|
|
2778
|
+
/**
|
|
2779
|
+
* Create a new company
|
|
2780
|
+
*/
|
|
2781
|
+
async createCompany(company) {
|
|
2782
|
+
return this.request(`/api/workspaces/${this.workspaceId}/companies`, {
|
|
2783
|
+
method: 'POST',
|
|
2784
|
+
body: JSON.stringify(company),
|
|
2785
|
+
});
|
|
2786
|
+
}
|
|
2787
|
+
/**
|
|
2788
|
+
* Update an existing company
|
|
2789
|
+
*/
|
|
2790
|
+
async updateCompany(companyId, updates) {
|
|
2791
|
+
return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
|
|
2792
|
+
method: 'PUT',
|
|
2793
|
+
body: JSON.stringify(updates),
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
/**
|
|
2797
|
+
* Delete a company
|
|
2798
|
+
*/
|
|
2799
|
+
async deleteCompany(companyId) {
|
|
2800
|
+
return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
|
|
2801
|
+
method: 'DELETE',
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2804
|
+
/**
|
|
2805
|
+
* Get contacts belonging to a company
|
|
2806
|
+
*/
|
|
2807
|
+
async getCompanyContacts(companyId, params) {
|
|
2808
|
+
const queryParams = new URLSearchParams();
|
|
2809
|
+
if (params?.page)
|
|
2810
|
+
queryParams.set('page', params.page.toString());
|
|
2811
|
+
if (params?.limit)
|
|
2812
|
+
queryParams.set('limit', params.limit.toString());
|
|
2813
|
+
const query = queryParams.toString();
|
|
2814
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/contacts${query ? `?${query}` : ''}`;
|
|
2815
|
+
return this.request(endpoint);
|
|
2816
|
+
}
|
|
2817
|
+
/**
|
|
2818
|
+
* Get deals/opportunities belonging to a company
|
|
2819
|
+
*/
|
|
2820
|
+
async getCompanyDeals(companyId, params) {
|
|
2821
|
+
const queryParams = new URLSearchParams();
|
|
2822
|
+
if (params?.page)
|
|
2823
|
+
queryParams.set('page', params.page.toString());
|
|
2824
|
+
if (params?.limit)
|
|
2825
|
+
queryParams.set('limit', params.limit.toString());
|
|
2826
|
+
const query = queryParams.toString();
|
|
2827
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/deals${query ? `?${query}` : ''}`;
|
|
2828
|
+
return this.request(endpoint);
|
|
2829
|
+
}
|
|
2830
|
+
// ============================================
|
|
2831
|
+
// PIPELINES API
|
|
2832
|
+
// ============================================
|
|
2833
|
+
/**
|
|
2834
|
+
* Get all pipelines
|
|
2835
|
+
*/
|
|
2836
|
+
async getPipelines() {
|
|
2837
|
+
return this.request(`/api/workspaces/${this.workspaceId}/pipelines`);
|
|
2838
|
+
}
|
|
2839
|
+
/**
|
|
2840
|
+
* Get a single pipeline by ID
|
|
2841
|
+
*/
|
|
2842
|
+
async getPipeline(pipelineId) {
|
|
2843
|
+
return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`);
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Create a new pipeline
|
|
2847
|
+
*/
|
|
2848
|
+
async createPipeline(pipeline) {
|
|
2849
|
+
return this.request(`/api/workspaces/${this.workspaceId}/pipelines`, {
|
|
2850
|
+
method: 'POST',
|
|
2851
|
+
body: JSON.stringify(pipeline),
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2854
|
+
/**
|
|
2855
|
+
* Update an existing pipeline
|
|
2856
|
+
*/
|
|
2857
|
+
async updatePipeline(pipelineId, updates) {
|
|
2858
|
+
return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
|
|
2859
|
+
method: 'PUT',
|
|
2860
|
+
body: JSON.stringify(updates),
|
|
2861
|
+
});
|
|
2862
|
+
}
|
|
2863
|
+
/**
|
|
2864
|
+
* Delete a pipeline
|
|
2865
|
+
*/
|
|
2866
|
+
async deletePipeline(pipelineId) {
|
|
2867
|
+
return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
|
|
2868
|
+
method: 'DELETE',
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
// ============================================
|
|
2872
|
+
// TASKS API
|
|
2873
|
+
// ============================================
|
|
2874
|
+
/**
|
|
2875
|
+
* Get all tasks with pagination
|
|
2876
|
+
*/
|
|
2877
|
+
async getTasks(params) {
|
|
2878
|
+
const queryParams = new URLSearchParams();
|
|
2879
|
+
if (params?.page)
|
|
2880
|
+
queryParams.set('page', params.page.toString());
|
|
2881
|
+
if (params?.limit)
|
|
2882
|
+
queryParams.set('limit', params.limit.toString());
|
|
2883
|
+
if (params?.status)
|
|
2884
|
+
queryParams.set('status', params.status);
|
|
2885
|
+
if (params?.priority)
|
|
2886
|
+
queryParams.set('priority', params.priority);
|
|
2887
|
+
if (params?.contactId)
|
|
2888
|
+
queryParams.set('contactId', params.contactId);
|
|
2889
|
+
if (params?.companyId)
|
|
2890
|
+
queryParams.set('companyId', params.companyId);
|
|
2891
|
+
if (params?.opportunityId)
|
|
2892
|
+
queryParams.set('opportunityId', params.opportunityId);
|
|
2893
|
+
const query = queryParams.toString();
|
|
2894
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/tasks${query ? `?${query}` : ''}`;
|
|
2895
|
+
return this.request(endpoint);
|
|
2896
|
+
}
|
|
2897
|
+
/**
|
|
2898
|
+
* Get a single task by ID
|
|
2899
|
+
*/
|
|
2900
|
+
async getTask(taskId) {
|
|
2901
|
+
return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`);
|
|
2902
|
+
}
|
|
2903
|
+
/**
|
|
2904
|
+
* Create a new task
|
|
2905
|
+
*/
|
|
2906
|
+
async createTask(task) {
|
|
2907
|
+
return this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
|
|
2908
|
+
method: 'POST',
|
|
2909
|
+
body: JSON.stringify(task),
|
|
2910
|
+
});
|
|
2911
|
+
}
|
|
2912
|
+
/**
|
|
2913
|
+
* Update an existing task
|
|
2914
|
+
*/
|
|
2915
|
+
async updateTask(taskId, updates) {
|
|
2916
|
+
return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
|
|
2917
|
+
method: 'PUT',
|
|
2918
|
+
body: JSON.stringify(updates),
|
|
2919
|
+
});
|
|
2920
|
+
}
|
|
2921
|
+
/**
|
|
2922
|
+
* Mark a task as completed
|
|
2923
|
+
*/
|
|
2924
|
+
async completeTask(taskId) {
|
|
2925
|
+
return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}/complete`, {
|
|
2926
|
+
method: 'PATCH',
|
|
2927
|
+
});
|
|
2928
|
+
}
|
|
2929
|
+
/**
|
|
2930
|
+
* Delete a task
|
|
2931
|
+
*/
|
|
2932
|
+
async deleteTask(taskId) {
|
|
2933
|
+
return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
|
|
2934
|
+
method: 'DELETE',
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2937
|
+
// ============================================
|
|
2938
|
+
// ACTIVITIES API
|
|
2939
|
+
// ============================================
|
|
2940
|
+
/**
|
|
2941
|
+
* Get activities for a contact
|
|
2942
|
+
*/
|
|
2943
|
+
async getContactActivities(contactId, params) {
|
|
2944
|
+
const queryParams = new URLSearchParams();
|
|
2945
|
+
if (params?.page)
|
|
2946
|
+
queryParams.set('page', params.page.toString());
|
|
2947
|
+
if (params?.limit)
|
|
2948
|
+
queryParams.set('limit', params.limit.toString());
|
|
2949
|
+
if (params?.type)
|
|
2950
|
+
queryParams.set('type', params.type);
|
|
2951
|
+
const query = queryParams.toString();
|
|
2952
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
|
|
2953
|
+
return this.request(endpoint);
|
|
2954
|
+
}
|
|
2955
|
+
/**
|
|
2956
|
+
* Get activities for an opportunity/deal
|
|
2957
|
+
*/
|
|
2958
|
+
async getOpportunityActivities(opportunityId, params) {
|
|
2959
|
+
const queryParams = new URLSearchParams();
|
|
2960
|
+
if (params?.page)
|
|
2961
|
+
queryParams.set('page', params.page.toString());
|
|
2962
|
+
if (params?.limit)
|
|
2963
|
+
queryParams.set('limit', params.limit.toString());
|
|
2964
|
+
if (params?.type)
|
|
2965
|
+
queryParams.set('type', params.type);
|
|
2966
|
+
const query = queryParams.toString();
|
|
2967
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/activities${query ? `?${query}` : ''}`;
|
|
2968
|
+
return this.request(endpoint);
|
|
2969
|
+
}
|
|
2970
|
+
/**
|
|
2971
|
+
* Create a new activity
|
|
2972
|
+
*/
|
|
2973
|
+
async createActivity(activity) {
|
|
2974
|
+
// Determine the correct endpoint based on related entity
|
|
2975
|
+
let endpoint;
|
|
2976
|
+
if (activity.opportunityId) {
|
|
2977
|
+
endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${activity.opportunityId}/activities`;
|
|
2978
|
+
}
|
|
2979
|
+
else if (activity.contactId) {
|
|
2980
|
+
endpoint = `/api/workspaces/${this.workspaceId}/contacts/${activity.contactId}/activities`;
|
|
2981
|
+
}
|
|
2982
|
+
else {
|
|
2983
|
+
endpoint = `/api/workspaces/${this.workspaceId}/activities`;
|
|
2984
|
+
}
|
|
2985
|
+
return this.request(endpoint, {
|
|
2986
|
+
method: 'POST',
|
|
2987
|
+
body: JSON.stringify(activity),
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
/**
|
|
2991
|
+
* Update an existing activity
|
|
2992
|
+
*/
|
|
2993
|
+
async updateActivity(activityId, updates) {
|
|
2994
|
+
return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
|
|
2995
|
+
method: 'PATCH',
|
|
2996
|
+
body: JSON.stringify(updates),
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
/**
|
|
3000
|
+
* Delete an activity
|
|
3001
|
+
*/
|
|
3002
|
+
async deleteActivity(activityId) {
|
|
3003
|
+
return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
|
|
3004
|
+
method: 'DELETE',
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
/**
|
|
3008
|
+
* Log a call activity
|
|
3009
|
+
*/
|
|
3010
|
+
async logCall(data) {
|
|
3011
|
+
return this.createActivity({
|
|
3012
|
+
type: 'call',
|
|
3013
|
+
title: `${data.direction === 'inbound' ? 'Inbound' : 'Outbound'} Call`,
|
|
3014
|
+
direction: data.direction,
|
|
3015
|
+
duration: data.duration,
|
|
3016
|
+
outcome: data.outcome,
|
|
3017
|
+
description: data.notes,
|
|
3018
|
+
contactId: data.contactId,
|
|
3019
|
+
opportunityId: data.opportunityId,
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
3022
|
+
/**
|
|
3023
|
+
* Log a meeting activity
|
|
3024
|
+
*/
|
|
3025
|
+
async logMeeting(data) {
|
|
3026
|
+
return this.createActivity({
|
|
3027
|
+
type: 'meeting',
|
|
3028
|
+
title: data.title,
|
|
3029
|
+
duration: data.duration,
|
|
3030
|
+
outcome: data.outcome,
|
|
3031
|
+
description: data.notes,
|
|
3032
|
+
contactId: data.contactId,
|
|
3033
|
+
opportunityId: data.opportunityId,
|
|
3034
|
+
});
|
|
3035
|
+
}
|
|
3036
|
+
/**
|
|
3037
|
+
* Add a note to a contact or opportunity
|
|
3038
|
+
*/
|
|
3039
|
+
async addNote(data) {
|
|
3040
|
+
return this.createActivity({
|
|
3041
|
+
type: 'note',
|
|
3042
|
+
title: 'Note',
|
|
3043
|
+
description: data.content,
|
|
3044
|
+
contactId: data.contactId,
|
|
3045
|
+
opportunityId: data.opportunityId,
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
2527
3048
|
}
|
|
2528
3049
|
|
|
2529
3050
|
/**
|
|
@@ -2642,7 +3163,7 @@ function useClianta() {
|
|
|
2642
3163
|
return react.useContext(CliantaContext);
|
|
2643
3164
|
}
|
|
2644
3165
|
/**
|
|
2645
|
-
*
|
|
3166
|
+
* useCliantaTrack - Convenience hook for tracking events
|
|
2646
3167
|
*
|
|
2647
3168
|
* @example
|
|
2648
3169
|
* const track = useCliantaTrack();
|