@clianta/sdk 1.1.1 → 1.3.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 +69 -0
- package/README.md +71 -1
- package/dist/clianta.cjs.js +1393 -225
- package/dist/clianta.cjs.js.map +1 -1
- package/dist/clianta.esm.js +1393 -226
- package/dist/clianta.esm.js.map +1 -1
- package/dist/clianta.umd.js +1393 -225
- 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 +596 -6
- package/dist/react.cjs.js +1393 -226
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.esm.js +1393 -226
- package/dist/react.esm.js.map +1 -1
- package/dist/vue.cjs.js +3900 -0
- package/dist/vue.cjs.js.map +1 -0
- package/dist/vue.d.ts +201 -0
- package/dist/vue.esm.js +3893 -0
- package/dist/vue.esm.js.map +1 -0
- package/package.json +16 -3
package/dist/clianta.umd.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Clianta SDK v1.
|
|
2
|
+
* Clianta SDK v1.3.0
|
|
3
3
|
* (c) 2026 Clianta
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @see SDK_VERSION in core/config.ts
|
|
15
15
|
*/
|
|
16
16
|
/** SDK Version */
|
|
17
|
-
const SDK_VERSION = '1.
|
|
17
|
+
const SDK_VERSION = '1.3.0';
|
|
18
18
|
/** Default API endpoint based on environment */
|
|
19
19
|
const getDefaultApiEndpoint = () => {
|
|
20
20
|
if (typeof window === 'undefined')
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
'engagement',
|
|
35
35
|
'downloads',
|
|
36
36
|
'exitIntent',
|
|
37
|
-
'popupForms',
|
|
38
37
|
];
|
|
39
38
|
/** Default configuration values */
|
|
40
39
|
const DEFAULT_CONFIG = {
|
|
@@ -578,14 +577,24 @@
|
|
|
578
577
|
* @see SDK_VERSION in core/config.ts
|
|
579
578
|
*/
|
|
580
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;
|
|
581
584
|
/**
|
|
582
|
-
* Event queue with batching, persistence, and auto-flush
|
|
585
|
+
* Event queue with batching, persistence, rate limiting, and auto-flush
|
|
583
586
|
*/
|
|
584
587
|
class EventQueue {
|
|
585
588
|
constructor(transport, config = {}) {
|
|
586
589
|
this.queue = [];
|
|
587
590
|
this.flushTimer = null;
|
|
588
591
|
this.isFlushing = false;
|
|
592
|
+
/** Rate limiting: timestamps of recent events */
|
|
593
|
+
this.eventTimestamps = [];
|
|
594
|
+
/** Unload handler references for cleanup */
|
|
595
|
+
this.boundBeforeUnload = null;
|
|
596
|
+
this.boundVisibilityChange = null;
|
|
597
|
+
this.boundPageHide = null;
|
|
589
598
|
this.transport = transport;
|
|
590
599
|
this.config = {
|
|
591
600
|
batchSize: config.batchSize ?? 10,
|
|
@@ -604,6 +613,11 @@
|
|
|
604
613
|
* Add an event to the queue
|
|
605
614
|
*/
|
|
606
615
|
push(event) {
|
|
616
|
+
// Rate limiting check
|
|
617
|
+
if (!this.checkRateLimit()) {
|
|
618
|
+
logger.warn('Rate limit exceeded, event dropped:', event.eventName);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
607
621
|
// Don't exceed max queue size
|
|
608
622
|
if (this.queue.length >= this.config.maxQueueSize) {
|
|
609
623
|
logger.warn('Queue full, dropping oldest event');
|
|
@@ -616,6 +630,22 @@
|
|
|
616
630
|
this.flush();
|
|
617
631
|
}
|
|
618
632
|
}
|
|
633
|
+
/**
|
|
634
|
+
* Check and enforce rate limiting
|
|
635
|
+
* @returns true if event is allowed, false if rate limited
|
|
636
|
+
*/
|
|
637
|
+
checkRateLimit() {
|
|
638
|
+
const now = Date.now();
|
|
639
|
+
// Remove timestamps outside the window
|
|
640
|
+
this.eventTimestamps = this.eventTimestamps.filter(ts => now - ts < RATE_LIMIT_WINDOW_MS);
|
|
641
|
+
// Check if under limit
|
|
642
|
+
if (this.eventTimestamps.length >= RATE_LIMIT_MAX_EVENTS) {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
// Record this event
|
|
646
|
+
this.eventTimestamps.push(now);
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
619
649
|
/**
|
|
620
650
|
* Flush the queue (send all events)
|
|
621
651
|
*/
|
|
@@ -624,9 +654,10 @@
|
|
|
624
654
|
return;
|
|
625
655
|
}
|
|
626
656
|
this.isFlushing = true;
|
|
657
|
+
// Atomically take snapshot of current queue length to avoid race condition
|
|
658
|
+
const count = this.queue.length;
|
|
659
|
+
const events = this.queue.splice(0, count);
|
|
627
660
|
try {
|
|
628
|
-
// Take all events from queue
|
|
629
|
-
const events = this.queue.splice(0, this.queue.length);
|
|
630
661
|
logger.debug(`Flushing ${events.length} events`);
|
|
631
662
|
// Clear persisted queue
|
|
632
663
|
this.persistQueue([]);
|
|
@@ -678,13 +709,25 @@
|
|
|
678
709
|
this.persistQueue([]);
|
|
679
710
|
}
|
|
680
711
|
/**
|
|
681
|
-
* Stop the flush timer
|
|
712
|
+
* Stop the flush timer and cleanup handlers
|
|
682
713
|
*/
|
|
683
714
|
destroy() {
|
|
684
715
|
if (this.flushTimer) {
|
|
685
716
|
clearInterval(this.flushTimer);
|
|
686
717
|
this.flushTimer = null;
|
|
687
718
|
}
|
|
719
|
+
// Remove unload handlers
|
|
720
|
+
if (typeof window !== 'undefined') {
|
|
721
|
+
if (this.boundBeforeUnload) {
|
|
722
|
+
window.removeEventListener('beforeunload', this.boundBeforeUnload);
|
|
723
|
+
}
|
|
724
|
+
if (this.boundVisibilityChange) {
|
|
725
|
+
window.removeEventListener('visibilitychange', this.boundVisibilityChange);
|
|
726
|
+
}
|
|
727
|
+
if (this.boundPageHide) {
|
|
728
|
+
window.removeEventListener('pagehide', this.boundPageHide);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
688
731
|
}
|
|
689
732
|
/**
|
|
690
733
|
* Start auto-flush timer
|
|
@@ -704,19 +747,18 @@
|
|
|
704
747
|
if (typeof window === 'undefined')
|
|
705
748
|
return;
|
|
706
749
|
// Flush on page unload
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
});
|
|
750
|
+
this.boundBeforeUnload = () => this.flushSync();
|
|
751
|
+
window.addEventListener('beforeunload', this.boundBeforeUnload);
|
|
710
752
|
// Flush when page becomes hidden
|
|
711
|
-
|
|
753
|
+
this.boundVisibilityChange = () => {
|
|
712
754
|
if (document.visibilityState === 'hidden') {
|
|
713
755
|
this.flushSync();
|
|
714
756
|
}
|
|
715
|
-
}
|
|
757
|
+
};
|
|
758
|
+
window.addEventListener('visibilitychange', this.boundVisibilityChange);
|
|
716
759
|
// Flush on page hide (iOS Safari)
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
});
|
|
760
|
+
this.boundPageHide = () => this.flushSync();
|
|
761
|
+
window.addEventListener('pagehide', this.boundPageHide);
|
|
720
762
|
}
|
|
721
763
|
/**
|
|
722
764
|
* Persist queue to localStorage
|
|
@@ -784,6 +826,9 @@
|
|
|
784
826
|
constructor() {
|
|
785
827
|
super(...arguments);
|
|
786
828
|
this.name = 'pageView';
|
|
829
|
+
this.originalPushState = null;
|
|
830
|
+
this.originalReplaceState = null;
|
|
831
|
+
this.popstateHandler = null;
|
|
787
832
|
}
|
|
788
833
|
init(tracker) {
|
|
789
834
|
super.init(tracker);
|
|
@@ -791,22 +836,40 @@
|
|
|
791
836
|
this.trackPageView();
|
|
792
837
|
// Track SPA navigation (History API)
|
|
793
838
|
if (typeof window !== 'undefined') {
|
|
839
|
+
// Store originals for cleanup
|
|
840
|
+
this.originalPushState = history.pushState;
|
|
841
|
+
this.originalReplaceState = history.replaceState;
|
|
794
842
|
// Intercept pushState and replaceState
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
this.trackPageView();
|
|
843
|
+
const self = this;
|
|
844
|
+
history.pushState = function (...args) {
|
|
845
|
+
self.originalPushState.apply(history, args);
|
|
846
|
+
self.trackPageView();
|
|
800
847
|
};
|
|
801
|
-
history.replaceState = (...args)
|
|
802
|
-
originalReplaceState.apply(history, args);
|
|
803
|
-
|
|
848
|
+
history.replaceState = function (...args) {
|
|
849
|
+
self.originalReplaceState.apply(history, args);
|
|
850
|
+
self.trackPageView();
|
|
804
851
|
};
|
|
805
852
|
// Handle back/forward navigation
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
853
|
+
this.popstateHandler = () => this.trackPageView();
|
|
854
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
destroy() {
|
|
858
|
+
// Restore original history methods
|
|
859
|
+
if (this.originalPushState) {
|
|
860
|
+
history.pushState = this.originalPushState;
|
|
861
|
+
this.originalPushState = null;
|
|
862
|
+
}
|
|
863
|
+
if (this.originalReplaceState) {
|
|
864
|
+
history.replaceState = this.originalReplaceState;
|
|
865
|
+
this.originalReplaceState = null;
|
|
809
866
|
}
|
|
867
|
+
// Remove popstate listener
|
|
868
|
+
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
869
|
+
window.removeEventListener('popstate', this.popstateHandler);
|
|
870
|
+
this.popstateHandler = null;
|
|
871
|
+
}
|
|
872
|
+
super.destroy();
|
|
810
873
|
}
|
|
811
874
|
trackPageView() {
|
|
812
875
|
if (typeof window === 'undefined' || typeof document === 'undefined')
|
|
@@ -838,6 +901,10 @@
|
|
|
838
901
|
this.pageLoadTime = 0;
|
|
839
902
|
this.scrollTimeout = null;
|
|
840
903
|
this.boundHandler = null;
|
|
904
|
+
/** SPA navigation support */
|
|
905
|
+
this.originalPushState = null;
|
|
906
|
+
this.originalReplaceState = null;
|
|
907
|
+
this.popstateHandler = null;
|
|
841
908
|
}
|
|
842
909
|
init(tracker) {
|
|
843
910
|
super.init(tracker);
|
|
@@ -845,6 +912,8 @@
|
|
|
845
912
|
if (typeof window !== 'undefined') {
|
|
846
913
|
this.boundHandler = this.handleScroll.bind(this);
|
|
847
914
|
window.addEventListener('scroll', this.boundHandler, { passive: true });
|
|
915
|
+
// Setup SPA navigation reset
|
|
916
|
+
this.setupNavigationReset();
|
|
848
917
|
}
|
|
849
918
|
}
|
|
850
919
|
destroy() {
|
|
@@ -854,8 +923,53 @@
|
|
|
854
923
|
if (this.scrollTimeout) {
|
|
855
924
|
clearTimeout(this.scrollTimeout);
|
|
856
925
|
}
|
|
926
|
+
// Restore original history methods
|
|
927
|
+
if (this.originalPushState) {
|
|
928
|
+
history.pushState = this.originalPushState;
|
|
929
|
+
this.originalPushState = null;
|
|
930
|
+
}
|
|
931
|
+
if (this.originalReplaceState) {
|
|
932
|
+
history.replaceState = this.originalReplaceState;
|
|
933
|
+
this.originalReplaceState = null;
|
|
934
|
+
}
|
|
935
|
+
// Remove popstate listener
|
|
936
|
+
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
937
|
+
window.removeEventListener('popstate', this.popstateHandler);
|
|
938
|
+
this.popstateHandler = null;
|
|
939
|
+
}
|
|
857
940
|
super.destroy();
|
|
858
941
|
}
|
|
942
|
+
/**
|
|
943
|
+
* Reset scroll tracking for SPA navigation
|
|
944
|
+
*/
|
|
945
|
+
resetForNavigation() {
|
|
946
|
+
this.milestonesReached.clear();
|
|
947
|
+
this.maxScrollDepth = 0;
|
|
948
|
+
this.pageLoadTime = Date.now();
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Setup History API interception for SPA navigation
|
|
952
|
+
*/
|
|
953
|
+
setupNavigationReset() {
|
|
954
|
+
if (typeof window === 'undefined')
|
|
955
|
+
return;
|
|
956
|
+
// Store originals for cleanup
|
|
957
|
+
this.originalPushState = history.pushState;
|
|
958
|
+
this.originalReplaceState = history.replaceState;
|
|
959
|
+
// Intercept pushState and replaceState
|
|
960
|
+
const self = this;
|
|
961
|
+
history.pushState = function (...args) {
|
|
962
|
+
self.originalPushState.apply(history, args);
|
|
963
|
+
self.resetForNavigation();
|
|
964
|
+
};
|
|
965
|
+
history.replaceState = function (...args) {
|
|
966
|
+
self.originalReplaceState.apply(history, args);
|
|
967
|
+
self.resetForNavigation();
|
|
968
|
+
};
|
|
969
|
+
// Handle back/forward navigation
|
|
970
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
971
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
972
|
+
}
|
|
859
973
|
handleScroll() {
|
|
860
974
|
// Debounce scroll tracking
|
|
861
975
|
if (this.scrollTimeout) {
|
|
@@ -869,7 +983,11 @@
|
|
|
869
983
|
const windowHeight = window.innerHeight;
|
|
870
984
|
const documentHeight = document.documentElement.scrollHeight;
|
|
871
985
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
872
|
-
const
|
|
986
|
+
const scrollableHeight = documentHeight - windowHeight;
|
|
987
|
+
// Guard against divide-by-zero on short pages
|
|
988
|
+
if (scrollableHeight <= 0)
|
|
989
|
+
return;
|
|
990
|
+
const scrollPercent = Math.floor((scrollTop / scrollableHeight) * 100);
|
|
873
991
|
// Clamp to valid range
|
|
874
992
|
const clampedPercent = Math.max(0, Math.min(100, scrollPercent));
|
|
875
993
|
// Update max scroll depth
|
|
@@ -904,6 +1022,7 @@
|
|
|
904
1022
|
this.trackedForms = new WeakSet();
|
|
905
1023
|
this.formInteractions = new Set();
|
|
906
1024
|
this.observer = null;
|
|
1025
|
+
this.listeners = [];
|
|
907
1026
|
}
|
|
908
1027
|
init(tracker) {
|
|
909
1028
|
super.init(tracker);
|
|
@@ -922,8 +1041,20 @@
|
|
|
922
1041
|
this.observer.disconnect();
|
|
923
1042
|
this.observer = null;
|
|
924
1043
|
}
|
|
1044
|
+
// Remove all tracked event listeners
|
|
1045
|
+
for (const { element, event, handler } of this.listeners) {
|
|
1046
|
+
element.removeEventListener(event, handler);
|
|
1047
|
+
}
|
|
1048
|
+
this.listeners = [];
|
|
925
1049
|
super.destroy();
|
|
926
1050
|
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Track event listener for cleanup
|
|
1053
|
+
*/
|
|
1054
|
+
addListener(element, event, handler) {
|
|
1055
|
+
element.addEventListener(event, handler);
|
|
1056
|
+
this.listeners.push({ element, event, handler });
|
|
1057
|
+
}
|
|
927
1058
|
trackAllForms() {
|
|
928
1059
|
document.querySelectorAll('form').forEach((form) => {
|
|
929
1060
|
this.setupFormTracking(form);
|
|
@@ -949,7 +1080,7 @@
|
|
|
949
1080
|
if (!field.name || field.type === 'submit' || field.type === 'button')
|
|
950
1081
|
return;
|
|
951
1082
|
['focus', 'blur', 'change'].forEach((eventType) => {
|
|
952
|
-
|
|
1083
|
+
const handler = () => {
|
|
953
1084
|
const key = `${formId}-${field.name}-${eventType}`;
|
|
954
1085
|
if (!this.formInteractions.has(key)) {
|
|
955
1086
|
this.formInteractions.add(key);
|
|
@@ -960,12 +1091,13 @@
|
|
|
960
1091
|
interactionType: eventType,
|
|
961
1092
|
});
|
|
962
1093
|
}
|
|
963
|
-
}
|
|
1094
|
+
};
|
|
1095
|
+
this.addListener(field, eventType, handler);
|
|
964
1096
|
});
|
|
965
1097
|
}
|
|
966
1098
|
});
|
|
967
1099
|
// Track form submission
|
|
968
|
-
|
|
1100
|
+
const submitHandler = () => {
|
|
969
1101
|
this.track('form_submit', 'Form Submitted', {
|
|
970
1102
|
formId,
|
|
971
1103
|
action: form.action,
|
|
@@ -973,7 +1105,8 @@
|
|
|
973
1105
|
});
|
|
974
1106
|
// Auto-identify if email field found
|
|
975
1107
|
this.autoIdentify(form);
|
|
976
|
-
}
|
|
1108
|
+
};
|
|
1109
|
+
this.addListener(form, 'submit', submitHandler);
|
|
977
1110
|
}
|
|
978
1111
|
autoIdentify(form) {
|
|
979
1112
|
const emailField = form.querySelector('input[type="email"], input[name*="email"]');
|
|
@@ -1057,6 +1190,7 @@
|
|
|
1057
1190
|
this.engagementTimeout = null;
|
|
1058
1191
|
this.boundMarkEngaged = null;
|
|
1059
1192
|
this.boundTrackTimeOnPage = null;
|
|
1193
|
+
this.boundVisibilityHandler = null;
|
|
1060
1194
|
}
|
|
1061
1195
|
init(tracker) {
|
|
1062
1196
|
super.init(tracker);
|
|
@@ -1067,12 +1201,7 @@
|
|
|
1067
1201
|
// Setup engagement detection
|
|
1068
1202
|
this.boundMarkEngaged = this.markEngaged.bind(this);
|
|
1069
1203
|
this.boundTrackTimeOnPage = this.trackTimeOnPage.bind(this);
|
|
1070
|
-
|
|
1071
|
-
document.addEventListener(event, this.boundMarkEngaged, { passive: true });
|
|
1072
|
-
});
|
|
1073
|
-
// Track time on page before unload
|
|
1074
|
-
window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
|
|
1075
|
-
window.addEventListener('visibilitychange', () => {
|
|
1204
|
+
this.boundVisibilityHandler = () => {
|
|
1076
1205
|
if (document.visibilityState === 'hidden') {
|
|
1077
1206
|
this.trackTimeOnPage();
|
|
1078
1207
|
}
|
|
@@ -1080,7 +1209,13 @@
|
|
|
1080
1209
|
// Reset engagement timer when page becomes visible again
|
|
1081
1210
|
this.engagementStartTime = Date.now();
|
|
1082
1211
|
}
|
|
1212
|
+
};
|
|
1213
|
+
['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
|
|
1214
|
+
document.addEventListener(event, this.boundMarkEngaged, { passive: true });
|
|
1083
1215
|
});
|
|
1216
|
+
// Track time on page before unload
|
|
1217
|
+
window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
|
|
1218
|
+
document.addEventListener('visibilitychange', this.boundVisibilityHandler);
|
|
1084
1219
|
}
|
|
1085
1220
|
destroy() {
|
|
1086
1221
|
if (this.boundMarkEngaged && typeof document !== 'undefined') {
|
|
@@ -1091,6 +1226,9 @@
|
|
|
1091
1226
|
if (this.boundTrackTimeOnPage && typeof window !== 'undefined') {
|
|
1092
1227
|
window.removeEventListener('beforeunload', this.boundTrackTimeOnPage);
|
|
1093
1228
|
}
|
|
1229
|
+
if (this.boundVisibilityHandler && typeof document !== 'undefined') {
|
|
1230
|
+
document.removeEventListener('visibilitychange', this.boundVisibilityHandler);
|
|
1231
|
+
}
|
|
1094
1232
|
if (this.engagementTimeout) {
|
|
1095
1233
|
clearTimeout(this.engagementTimeout);
|
|
1096
1234
|
}
|
|
@@ -1135,20 +1273,69 @@
|
|
|
1135
1273
|
this.name = 'downloads';
|
|
1136
1274
|
this.trackedDownloads = new Set();
|
|
1137
1275
|
this.boundHandler = null;
|
|
1276
|
+
/** SPA navigation support */
|
|
1277
|
+
this.originalPushState = null;
|
|
1278
|
+
this.originalReplaceState = null;
|
|
1279
|
+
this.popstateHandler = null;
|
|
1138
1280
|
}
|
|
1139
1281
|
init(tracker) {
|
|
1140
1282
|
super.init(tracker);
|
|
1141
1283
|
if (typeof document !== 'undefined') {
|
|
1142
1284
|
this.boundHandler = this.handleClick.bind(this);
|
|
1143
1285
|
document.addEventListener('click', this.boundHandler, true);
|
|
1286
|
+
// Setup SPA navigation reset
|
|
1287
|
+
this.setupNavigationReset();
|
|
1144
1288
|
}
|
|
1145
1289
|
}
|
|
1146
1290
|
destroy() {
|
|
1147
1291
|
if (this.boundHandler && typeof document !== 'undefined') {
|
|
1148
1292
|
document.removeEventListener('click', this.boundHandler, true);
|
|
1149
1293
|
}
|
|
1294
|
+
// Restore original history methods
|
|
1295
|
+
if (this.originalPushState) {
|
|
1296
|
+
history.pushState = this.originalPushState;
|
|
1297
|
+
this.originalPushState = null;
|
|
1298
|
+
}
|
|
1299
|
+
if (this.originalReplaceState) {
|
|
1300
|
+
history.replaceState = this.originalReplaceState;
|
|
1301
|
+
this.originalReplaceState = null;
|
|
1302
|
+
}
|
|
1303
|
+
// Remove popstate listener
|
|
1304
|
+
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
1305
|
+
window.removeEventListener('popstate', this.popstateHandler);
|
|
1306
|
+
this.popstateHandler = null;
|
|
1307
|
+
}
|
|
1150
1308
|
super.destroy();
|
|
1151
1309
|
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Reset download tracking for SPA navigation
|
|
1312
|
+
*/
|
|
1313
|
+
resetForNavigation() {
|
|
1314
|
+
this.trackedDownloads.clear();
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Setup History API interception for SPA navigation
|
|
1318
|
+
*/
|
|
1319
|
+
setupNavigationReset() {
|
|
1320
|
+
if (typeof window === 'undefined')
|
|
1321
|
+
return;
|
|
1322
|
+
// Store originals for cleanup
|
|
1323
|
+
this.originalPushState = history.pushState;
|
|
1324
|
+
this.originalReplaceState = history.replaceState;
|
|
1325
|
+
// Intercept pushState and replaceState
|
|
1326
|
+
const self = this;
|
|
1327
|
+
history.pushState = function (...args) {
|
|
1328
|
+
self.originalPushState.apply(history, args);
|
|
1329
|
+
self.resetForNavigation();
|
|
1330
|
+
};
|
|
1331
|
+
history.replaceState = function (...args) {
|
|
1332
|
+
self.originalReplaceState.apply(history, args);
|
|
1333
|
+
self.resetForNavigation();
|
|
1334
|
+
};
|
|
1335
|
+
// Handle back/forward navigation
|
|
1336
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1337
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
1338
|
+
}
|
|
1152
1339
|
handleClick(e) {
|
|
1153
1340
|
const link = e.target.closest('a');
|
|
1154
1341
|
if (!link || !link.href)
|
|
@@ -1274,34 +1461,72 @@
|
|
|
1274
1461
|
constructor() {
|
|
1275
1462
|
super(...arguments);
|
|
1276
1463
|
this.name = 'performance';
|
|
1464
|
+
this.boundLoadHandler = null;
|
|
1465
|
+
this.observers = [];
|
|
1466
|
+
this.boundClsVisibilityHandler = null;
|
|
1277
1467
|
}
|
|
1278
1468
|
init(tracker) {
|
|
1279
1469
|
super.init(tracker);
|
|
1280
1470
|
if (typeof window !== 'undefined') {
|
|
1281
1471
|
// Track performance after page load
|
|
1282
|
-
|
|
1472
|
+
this.boundLoadHandler = () => {
|
|
1283
1473
|
// Delay to ensure all metrics are available
|
|
1284
1474
|
setTimeout(() => this.trackPerformance(), 100);
|
|
1285
|
-
}
|
|
1475
|
+
};
|
|
1476
|
+
window.addEventListener('load', this.boundLoadHandler);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
destroy() {
|
|
1480
|
+
if (this.boundLoadHandler && typeof window !== 'undefined') {
|
|
1481
|
+
window.removeEventListener('load', this.boundLoadHandler);
|
|
1286
1482
|
}
|
|
1483
|
+
for (const observer of this.observers) {
|
|
1484
|
+
observer.disconnect();
|
|
1485
|
+
}
|
|
1486
|
+
this.observers = [];
|
|
1487
|
+
if (this.boundClsVisibilityHandler && typeof window !== 'undefined') {
|
|
1488
|
+
window.removeEventListener('visibilitychange', this.boundClsVisibilityHandler);
|
|
1489
|
+
}
|
|
1490
|
+
super.destroy();
|
|
1287
1491
|
}
|
|
1288
1492
|
trackPerformance() {
|
|
1289
1493
|
if (typeof performance === 'undefined')
|
|
1290
1494
|
return;
|
|
1291
|
-
// Use Navigation Timing API
|
|
1292
|
-
const
|
|
1293
|
-
if (
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1495
|
+
// Use modern Navigation Timing API (PerformanceNavigationTiming)
|
|
1496
|
+
const entries = performance.getEntriesByType('navigation');
|
|
1497
|
+
if (entries.length > 0) {
|
|
1498
|
+
const navTiming = entries[0];
|
|
1499
|
+
const loadTime = Math.round(navTiming.loadEventEnd - navTiming.startTime);
|
|
1500
|
+
const domReady = Math.round(navTiming.domContentLoadedEventEnd - navTiming.startTime);
|
|
1501
|
+
const ttfb = Math.round(navTiming.responseStart - navTiming.requestStart);
|
|
1502
|
+
const domInteractive = Math.round(navTiming.domInteractive - navTiming.startTime);
|
|
1503
|
+
this.track('performance', 'Page Performance', {
|
|
1504
|
+
loadTime,
|
|
1505
|
+
domReady,
|
|
1506
|
+
ttfb, // Time to First Byte
|
|
1507
|
+
domInteractive,
|
|
1508
|
+
// Additional modern metrics
|
|
1509
|
+
dns: Math.round(navTiming.domainLookupEnd - navTiming.domainLookupStart),
|
|
1510
|
+
connection: Math.round(navTiming.connectEnd - navTiming.connectStart),
|
|
1511
|
+
transferSize: navTiming.transferSize,
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
else {
|
|
1515
|
+
// Fallback for older browsers using deprecated API
|
|
1516
|
+
const timing = performance.timing;
|
|
1517
|
+
if (!timing)
|
|
1518
|
+
return;
|
|
1519
|
+
const loadTime = timing.loadEventEnd - timing.navigationStart;
|
|
1520
|
+
const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;
|
|
1521
|
+
const ttfb = timing.responseStart - timing.navigationStart;
|
|
1522
|
+
const domInteractive = timing.domInteractive - timing.navigationStart;
|
|
1523
|
+
this.track('performance', 'Page Performance', {
|
|
1524
|
+
loadTime,
|
|
1525
|
+
domReady,
|
|
1526
|
+
ttfb,
|
|
1527
|
+
domInteractive,
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1305
1530
|
// Track Web Vitals if available
|
|
1306
1531
|
this.trackWebVitals();
|
|
1307
1532
|
}
|
|
@@ -1320,6 +1545,7 @@
|
|
|
1320
1545
|
}
|
|
1321
1546
|
});
|
|
1322
1547
|
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
|
|
1548
|
+
this.observers.push(lcpObserver);
|
|
1323
1549
|
}
|
|
1324
1550
|
catch {
|
|
1325
1551
|
// LCP not supported
|
|
@@ -1337,6 +1563,7 @@
|
|
|
1337
1563
|
}
|
|
1338
1564
|
});
|
|
1339
1565
|
fidObserver.observe({ type: 'first-input', buffered: true });
|
|
1566
|
+
this.observers.push(fidObserver);
|
|
1340
1567
|
}
|
|
1341
1568
|
catch {
|
|
1342
1569
|
// FID not supported
|
|
@@ -1353,15 +1580,17 @@
|
|
|
1353
1580
|
});
|
|
1354
1581
|
});
|
|
1355
1582
|
clsObserver.observe({ type: 'layout-shift', buffered: true });
|
|
1583
|
+
this.observers.push(clsObserver);
|
|
1356
1584
|
// Report CLS after page is hidden
|
|
1357
|
-
|
|
1585
|
+
this.boundClsVisibilityHandler = () => {
|
|
1358
1586
|
if (document.visibilityState === 'hidden' && clsValue > 0) {
|
|
1359
1587
|
this.track('performance', 'Web Vital - CLS', {
|
|
1360
1588
|
metric: 'CLS',
|
|
1361
1589
|
value: Math.round(clsValue * 1000) / 1000,
|
|
1362
1590
|
});
|
|
1363
1591
|
}
|
|
1364
|
-
}
|
|
1592
|
+
};
|
|
1593
|
+
window.addEventListener('visibilitychange', this.boundClsVisibilityHandler, { once: true });
|
|
1365
1594
|
}
|
|
1366
1595
|
catch {
|
|
1367
1596
|
// CLS not supported
|
|
@@ -1578,8 +1807,8 @@
|
|
|
1578
1807
|
opacity: 0;
|
|
1579
1808
|
transition: all 0.3s ease;
|
|
1580
1809
|
`;
|
|
1581
|
-
// Build form
|
|
1582
|
-
|
|
1810
|
+
// Build form using safe DOM APIs (no innerHTML for user content)
|
|
1811
|
+
this.buildFormDOM(form, container);
|
|
1583
1812
|
overlay.appendChild(container);
|
|
1584
1813
|
document.body.appendChild(overlay);
|
|
1585
1814
|
// Animate in
|
|
@@ -1591,95 +1820,162 @@
|
|
|
1591
1820
|
// Setup event listeners
|
|
1592
1821
|
this.setupFormEvents(form, overlay, container);
|
|
1593
1822
|
}
|
|
1594
|
-
|
|
1823
|
+
/**
|
|
1824
|
+
* Escape HTML to prevent XSS - used only for static structure
|
|
1825
|
+
*/
|
|
1826
|
+
escapeHTML(str) {
|
|
1827
|
+
const div = document.createElement('div');
|
|
1828
|
+
div.textContent = str;
|
|
1829
|
+
return div.innerHTML;
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Build form using safe DOM APIs (prevents XSS)
|
|
1833
|
+
*/
|
|
1834
|
+
buildFormDOM(form, container) {
|
|
1595
1835
|
const style = form.style || {};
|
|
1596
1836
|
const primaryColor = style.primaryColor || '#10B981';
|
|
1597
1837
|
const textColor = style.textColor || '#18181B';
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1838
|
+
// Close button
|
|
1839
|
+
const closeBtn = document.createElement('button');
|
|
1840
|
+
closeBtn.id = 'clianta-form-close';
|
|
1841
|
+
closeBtn.style.cssText = `
|
|
1842
|
+
position: absolute;
|
|
1843
|
+
top: 12px;
|
|
1844
|
+
right: 12px;
|
|
1845
|
+
background: none;
|
|
1846
|
+
border: none;
|
|
1847
|
+
font-size: 20px;
|
|
1848
|
+
cursor: pointer;
|
|
1849
|
+
color: #71717A;
|
|
1850
|
+
padding: 4px;
|
|
1851
|
+
`;
|
|
1852
|
+
closeBtn.textContent = '×';
|
|
1853
|
+
container.appendChild(closeBtn);
|
|
1854
|
+
// Headline
|
|
1855
|
+
const headline = document.createElement('h2');
|
|
1856
|
+
headline.style.cssText = `font-size: 20px; font-weight: 700; margin-bottom: 8px; color: ${this.escapeHTML(textColor)};`;
|
|
1857
|
+
headline.textContent = form.headline || 'Stay in touch';
|
|
1858
|
+
container.appendChild(headline);
|
|
1859
|
+
// Subheadline
|
|
1860
|
+
const subheadline = document.createElement('p');
|
|
1861
|
+
subheadline.style.cssText = 'font-size: 14px; color: #71717A; margin-bottom: 16px;';
|
|
1862
|
+
subheadline.textContent = form.subheadline || 'Get the latest updates';
|
|
1863
|
+
container.appendChild(subheadline);
|
|
1864
|
+
// Form element
|
|
1865
|
+
const formElement = document.createElement('form');
|
|
1866
|
+
formElement.id = 'clianta-form-element';
|
|
1867
|
+
// Build fields
|
|
1868
|
+
form.fields.forEach(field => {
|
|
1869
|
+
const fieldWrapper = document.createElement('div');
|
|
1870
|
+
fieldWrapper.style.marginBottom = '12px';
|
|
1871
|
+
if (field.type === 'checkbox') {
|
|
1872
|
+
// Checkbox layout
|
|
1873
|
+
const label = document.createElement('label');
|
|
1874
|
+
label.style.cssText = `display: flex; align-items: center; gap: 8px; font-size: 14px; color: ${this.escapeHTML(textColor)}; cursor: pointer;`;
|
|
1875
|
+
const input = document.createElement('input');
|
|
1876
|
+
input.type = 'checkbox';
|
|
1877
|
+
input.name = field.name;
|
|
1878
|
+
if (field.required)
|
|
1879
|
+
input.required = true;
|
|
1880
|
+
input.style.cssText = 'width: 16px; height: 16px;';
|
|
1881
|
+
label.appendChild(input);
|
|
1882
|
+
const labelText = document.createTextNode(field.label + ' ');
|
|
1883
|
+
label.appendChild(labelText);
|
|
1884
|
+
if (field.required) {
|
|
1885
|
+
const requiredMark = document.createElement('span');
|
|
1886
|
+
requiredMark.style.color = '#EF4444';
|
|
1887
|
+
requiredMark.textContent = '*';
|
|
1888
|
+
label.appendChild(requiredMark);
|
|
1889
|
+
}
|
|
1890
|
+
fieldWrapper.appendChild(label);
|
|
1629
1891
|
}
|
|
1630
1892
|
else {
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1893
|
+
// Label
|
|
1894
|
+
const label = document.createElement('label');
|
|
1895
|
+
label.style.cssText = `display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${this.escapeHTML(textColor)};`;
|
|
1896
|
+
label.textContent = field.label + ' ';
|
|
1897
|
+
if (field.required) {
|
|
1898
|
+
const requiredMark = document.createElement('span');
|
|
1899
|
+
requiredMark.style.color = '#EF4444';
|
|
1900
|
+
requiredMark.textContent = '*';
|
|
1901
|
+
label.appendChild(requiredMark);
|
|
1902
|
+
}
|
|
1903
|
+
fieldWrapper.appendChild(label);
|
|
1904
|
+
// Input/Textarea/Select
|
|
1905
|
+
if (field.type === 'textarea') {
|
|
1906
|
+
const textarea = document.createElement('textarea');
|
|
1907
|
+
textarea.name = field.name;
|
|
1908
|
+
if (field.placeholder)
|
|
1909
|
+
textarea.placeholder = field.placeholder;
|
|
1910
|
+
if (field.required)
|
|
1911
|
+
textarea.required = true;
|
|
1912
|
+
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;';
|
|
1913
|
+
fieldWrapper.appendChild(textarea);
|
|
1914
|
+
}
|
|
1915
|
+
else if (field.type === 'select') {
|
|
1916
|
+
const select = document.createElement('select');
|
|
1917
|
+
select.name = field.name;
|
|
1918
|
+
if (field.required)
|
|
1919
|
+
select.required = true;
|
|
1920
|
+
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;';
|
|
1921
|
+
// Add placeholder option
|
|
1922
|
+
if (field.placeholder) {
|
|
1923
|
+
const placeholderOption = document.createElement('option');
|
|
1924
|
+
placeholderOption.value = '';
|
|
1925
|
+
placeholderOption.textContent = field.placeholder;
|
|
1926
|
+
placeholderOption.disabled = true;
|
|
1927
|
+
placeholderOption.selected = true;
|
|
1928
|
+
select.appendChild(placeholderOption);
|
|
1929
|
+
}
|
|
1930
|
+
// Add options from field.options array if provided
|
|
1931
|
+
if (field.options && Array.isArray(field.options)) {
|
|
1932
|
+
field.options.forEach((opt) => {
|
|
1933
|
+
const option = document.createElement('option');
|
|
1934
|
+
if (typeof opt === 'string') {
|
|
1935
|
+
option.value = opt;
|
|
1936
|
+
option.textContent = opt;
|
|
1937
|
+
}
|
|
1938
|
+
else {
|
|
1939
|
+
option.value = opt.value;
|
|
1940
|
+
option.textContent = opt.label;
|
|
1941
|
+
}
|
|
1942
|
+
select.appendChild(option);
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
fieldWrapper.appendChild(select);
|
|
1946
|
+
}
|
|
1947
|
+
else {
|
|
1948
|
+
const input = document.createElement('input');
|
|
1949
|
+
input.type = field.type;
|
|
1950
|
+
input.name = field.name;
|
|
1951
|
+
if (field.placeholder)
|
|
1952
|
+
input.placeholder = field.placeholder;
|
|
1953
|
+
if (field.required)
|
|
1954
|
+
input.required = true;
|
|
1955
|
+
input.style.cssText = 'width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box;';
|
|
1956
|
+
fieldWrapper.appendChild(input);
|
|
1957
|
+
}
|
|
1645
1958
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
<p style="font-size: 14px; color: #71717A; margin-bottom: 16px;">
|
|
1663
|
-
${form.subheadline || 'Get the latest updates'}
|
|
1664
|
-
</p>
|
|
1665
|
-
<form id="clianta-form-element">
|
|
1666
|
-
${fieldsHTML}
|
|
1667
|
-
<button type="submit" style="
|
|
1668
|
-
width: 100%;
|
|
1669
|
-
padding: 10px 16px;
|
|
1670
|
-
background: ${primaryColor};
|
|
1671
|
-
color: white;
|
|
1672
|
-
border: none;
|
|
1673
|
-
border-radius: 6px;
|
|
1674
|
-
font-size: 14px;
|
|
1675
|
-
font-weight: 500;
|
|
1676
|
-
cursor: pointer;
|
|
1677
|
-
margin-top: 8px;
|
|
1678
|
-
">
|
|
1679
|
-
${form.submitButtonText || 'Subscribe'}
|
|
1680
|
-
</button>
|
|
1681
|
-
</form>
|
|
1959
|
+
formElement.appendChild(fieldWrapper);
|
|
1960
|
+
});
|
|
1961
|
+
// Submit button
|
|
1962
|
+
const submitBtn = document.createElement('button');
|
|
1963
|
+
submitBtn.type = 'submit';
|
|
1964
|
+
submitBtn.style.cssText = `
|
|
1965
|
+
width: 100%;
|
|
1966
|
+
padding: 10px 16px;
|
|
1967
|
+
background: ${this.escapeHTML(primaryColor)};
|
|
1968
|
+
color: white;
|
|
1969
|
+
border: none;
|
|
1970
|
+
border-radius: 6px;
|
|
1971
|
+
font-size: 14px;
|
|
1972
|
+
font-weight: 500;
|
|
1973
|
+
cursor: pointer;
|
|
1974
|
+
margin-top: 8px;
|
|
1682
1975
|
`;
|
|
1976
|
+
submitBtn.textContent = form.submitButtonText || 'Subscribe';
|
|
1977
|
+
formElement.appendChild(submitBtn);
|
|
1978
|
+
container.appendChild(formElement);
|
|
1683
1979
|
}
|
|
1684
1980
|
setupFormEvents(form, overlay, container) {
|
|
1685
1981
|
// Close button
|
|
@@ -1740,19 +2036,29 @@
|
|
|
1740
2036
|
});
|
|
1741
2037
|
const result = await response.json();
|
|
1742
2038
|
if (result.success) {
|
|
1743
|
-
// Show success message
|
|
1744
|
-
container.innerHTML =
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
2039
|
+
// Show success message using safe DOM APIs
|
|
2040
|
+
container.innerHTML = '';
|
|
2041
|
+
const successWrapper = document.createElement('div');
|
|
2042
|
+
successWrapper.style.cssText = 'text-align: center; padding: 20px;';
|
|
2043
|
+
const iconWrapper = document.createElement('div');
|
|
2044
|
+
iconWrapper.style.cssText = 'width: 48px; height: 48px; background: #10B981; border-radius: 50%; margin: 0 auto 16px; display: flex; align-items: center; justify-content: center;';
|
|
2045
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
2046
|
+
svg.setAttribute('width', '24');
|
|
2047
|
+
svg.setAttribute('height', '24');
|
|
2048
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
2049
|
+
svg.setAttribute('fill', 'none');
|
|
2050
|
+
svg.setAttribute('stroke', 'white');
|
|
2051
|
+
svg.setAttribute('stroke-width', '2');
|
|
2052
|
+
const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
|
2053
|
+
polyline.setAttribute('points', '20 6 9 17 4 12');
|
|
2054
|
+
svg.appendChild(polyline);
|
|
2055
|
+
iconWrapper.appendChild(svg);
|
|
2056
|
+
const message = document.createElement('p');
|
|
2057
|
+
message.style.cssText = 'font-size: 16px; font-weight: 500; color: #18181B;';
|
|
2058
|
+
message.textContent = form.successMessage || 'Thank you!';
|
|
2059
|
+
successWrapper.appendChild(iconWrapper);
|
|
2060
|
+
successWrapper.appendChild(message);
|
|
2061
|
+
container.appendChild(successWrapper);
|
|
1756
2062
|
// Track identify
|
|
1757
2063
|
if (data.email) {
|
|
1758
2064
|
this.tracker?.identify(data.email, data);
|
|
@@ -1889,6 +2195,8 @@
|
|
|
1889
2195
|
* Manages consent state and event buffering for GDPR/CCPA compliance
|
|
1890
2196
|
* @see SDK_VERSION in core/config.ts
|
|
1891
2197
|
*/
|
|
2198
|
+
/** Maximum events to buffer while waiting for consent */
|
|
2199
|
+
const MAX_BUFFER_SIZE = 100;
|
|
1892
2200
|
/**
|
|
1893
2201
|
* Manages user consent state for tracking
|
|
1894
2202
|
*/
|
|
@@ -2005,6 +2313,11 @@
|
|
|
2005
2313
|
* Buffer an event (for waitForConsent mode)
|
|
2006
2314
|
*/
|
|
2007
2315
|
bufferEvent(event) {
|
|
2316
|
+
// Prevent unbounded buffer growth
|
|
2317
|
+
if (this.eventBuffer.length >= MAX_BUFFER_SIZE) {
|
|
2318
|
+
logger.warn('Consent event buffer full, dropping oldest event');
|
|
2319
|
+
this.eventBuffer.shift();
|
|
2320
|
+
}
|
|
2008
2321
|
this.eventBuffer.push(event);
|
|
2009
2322
|
logger.debug('Event buffered (waiting for consent):', event.eventName);
|
|
2010
2323
|
}
|
|
@@ -2064,6 +2377,8 @@
|
|
|
2064
2377
|
constructor(workspaceId, userConfig = {}) {
|
|
2065
2378
|
this.plugins = [];
|
|
2066
2379
|
this.isInitialized = false;
|
|
2380
|
+
/** Pending identify retry on next flush */
|
|
2381
|
+
this.pendingIdentify = null;
|
|
2067
2382
|
if (!workspaceId) {
|
|
2068
2383
|
throw new Error('[Clianta] Workspace ID is required');
|
|
2069
2384
|
}
|
|
@@ -2149,6 +2464,7 @@
|
|
|
2149
2464
|
}
|
|
2150
2465
|
/**
|
|
2151
2466
|
* Initialize enabled plugins
|
|
2467
|
+
* Handles both sync and async plugin init methods
|
|
2152
2468
|
*/
|
|
2153
2469
|
initPlugins() {
|
|
2154
2470
|
const pluginsToLoad = this.config.plugins;
|
|
@@ -2159,7 +2475,13 @@
|
|
|
2159
2475
|
for (const pluginName of filteredPlugins) {
|
|
2160
2476
|
try {
|
|
2161
2477
|
const plugin = getPlugin(pluginName);
|
|
2162
|
-
|
|
2478
|
+
// Handle both sync and async init (fire-and-forget for async)
|
|
2479
|
+
const result = plugin.init(this);
|
|
2480
|
+
if (result instanceof Promise) {
|
|
2481
|
+
result.catch((error) => {
|
|
2482
|
+
logger.error(`Async plugin init failed: ${pluginName}`, error);
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2163
2485
|
this.plugins.push(plugin);
|
|
2164
2486
|
logger.debug(`Plugin loaded: ${pluginName}`);
|
|
2165
2487
|
}
|
|
@@ -2186,7 +2508,7 @@
|
|
|
2186
2508
|
referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
|
|
2187
2509
|
properties,
|
|
2188
2510
|
device: getDeviceInfo(),
|
|
2189
|
-
|
|
2511
|
+
...getUTMParams(),
|
|
2190
2512
|
timestamp: new Date().toISOString(),
|
|
2191
2513
|
sdkVersion: SDK_VERSION,
|
|
2192
2514
|
};
|
|
@@ -2231,11 +2553,24 @@
|
|
|
2231
2553
|
});
|
|
2232
2554
|
if (result.success) {
|
|
2233
2555
|
logger.info('Visitor identified successfully');
|
|
2556
|
+
this.pendingIdentify = null;
|
|
2234
2557
|
}
|
|
2235
2558
|
else {
|
|
2236
2559
|
logger.error('Failed to identify visitor:', result.error);
|
|
2560
|
+
// Store for retry on next flush
|
|
2561
|
+
this.pendingIdentify = { email, traits };
|
|
2237
2562
|
}
|
|
2238
2563
|
}
|
|
2564
|
+
/**
|
|
2565
|
+
* Retry pending identify call
|
|
2566
|
+
*/
|
|
2567
|
+
async retryPendingIdentify() {
|
|
2568
|
+
if (!this.pendingIdentify)
|
|
2569
|
+
return;
|
|
2570
|
+
const { email, traits } = this.pendingIdentify;
|
|
2571
|
+
this.pendingIdentify = null;
|
|
2572
|
+
await this.identify(email, traits);
|
|
2573
|
+
}
|
|
2239
2574
|
/**
|
|
2240
2575
|
* Update consent state
|
|
2241
2576
|
*/
|
|
@@ -2283,6 +2618,7 @@
|
|
|
2283
2618
|
* Force flush event queue
|
|
2284
2619
|
*/
|
|
2285
2620
|
async flush() {
|
|
2621
|
+
await this.retryPendingIdentify();
|
|
2286
2622
|
await this.queue.flush();
|
|
2287
2623
|
}
|
|
2288
2624
|
/**
|
|
@@ -2337,10 +2673,10 @@
|
|
|
2337
2673
|
/**
|
|
2338
2674
|
* Destroy tracker and cleanup
|
|
2339
2675
|
*/
|
|
2340
|
-
destroy() {
|
|
2676
|
+
async destroy() {
|
|
2341
2677
|
logger.info('Destroying tracker');
|
|
2342
|
-
// Flush any remaining events
|
|
2343
|
-
this.queue.flush();
|
|
2678
|
+
// Flush any remaining events (await to ensure completion)
|
|
2679
|
+
await this.queue.flush();
|
|
2344
2680
|
// Destroy plugins
|
|
2345
2681
|
for (const plugin of this.plugins) {
|
|
2346
2682
|
if (plugin.destroy) {
|
|
@@ -2355,20 +2691,28 @@
|
|
|
2355
2691
|
}
|
|
2356
2692
|
|
|
2357
2693
|
/**
|
|
2358
|
-
* Clianta SDK -
|
|
2359
|
-
*
|
|
2694
|
+
* Clianta SDK - Event Triggers Manager
|
|
2695
|
+
* Manages event-driven automation and email notifications
|
|
2360
2696
|
*/
|
|
2361
2697
|
/**
|
|
2362
|
-
*
|
|
2698
|
+
* Event Triggers Manager
|
|
2699
|
+
* Handles event-driven automation based on CRM actions
|
|
2700
|
+
*
|
|
2701
|
+
* Similar to:
|
|
2702
|
+
* - Salesforce: Process Builder, Flow Automation
|
|
2703
|
+
* - HubSpot: Workflows, Email Sequences
|
|
2704
|
+
* - Pipedrive: Workflow Automation
|
|
2363
2705
|
*/
|
|
2364
|
-
class
|
|
2706
|
+
class EventTriggersManager {
|
|
2365
2707
|
constructor(apiEndpoint, workspaceId, authToken) {
|
|
2708
|
+
this.triggers = new Map();
|
|
2709
|
+
this.listeners = new Map();
|
|
2366
2710
|
this.apiEndpoint = apiEndpoint;
|
|
2367
2711
|
this.workspaceId = workspaceId;
|
|
2368
2712
|
this.authToken = authToken;
|
|
2369
2713
|
}
|
|
2370
2714
|
/**
|
|
2371
|
-
* Set authentication token
|
|
2715
|
+
* Set authentication token
|
|
2372
2716
|
*/
|
|
2373
2717
|
setAuthToken(token) {
|
|
2374
2718
|
this.authToken = token;
|
|
@@ -2413,120 +2757,942 @@
|
|
|
2413
2757
|
}
|
|
2414
2758
|
}
|
|
2415
2759
|
// ============================================
|
|
2416
|
-
//
|
|
2760
|
+
// TRIGGER MANAGEMENT
|
|
2417
2761
|
// ============================================
|
|
2418
2762
|
/**
|
|
2419
|
-
* Get all
|
|
2763
|
+
* Get all event triggers
|
|
2420
2764
|
*/
|
|
2421
|
-
async
|
|
2422
|
-
|
|
2423
|
-
if (params?.page)
|
|
2424
|
-
queryParams.set('page', params.page.toString());
|
|
2425
|
-
if (params?.limit)
|
|
2426
|
-
queryParams.set('limit', params.limit.toString());
|
|
2427
|
-
if (params?.search)
|
|
2428
|
-
queryParams.set('search', params.search);
|
|
2429
|
-
if (params?.status)
|
|
2430
|
-
queryParams.set('status', params.status);
|
|
2431
|
-
const query = queryParams.toString();
|
|
2432
|
-
const endpoint = `/api/workspaces/${this.workspaceId}/contacts${query ? `?${query}` : ''}`;
|
|
2433
|
-
return this.request(endpoint);
|
|
2765
|
+
async getTriggers() {
|
|
2766
|
+
return this.request(`/api/workspaces/${this.workspaceId}/triggers`);
|
|
2434
2767
|
}
|
|
2435
2768
|
/**
|
|
2436
|
-
* Get a single
|
|
2769
|
+
* Get a single trigger by ID
|
|
2437
2770
|
*/
|
|
2438
|
-
async
|
|
2439
|
-
return this.request(`/api/workspaces/${this.workspaceId}/
|
|
2771
|
+
async getTrigger(triggerId) {
|
|
2772
|
+
return this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`);
|
|
2440
2773
|
}
|
|
2441
2774
|
/**
|
|
2442
|
-
* Create a new
|
|
2775
|
+
* Create a new event trigger
|
|
2443
2776
|
*/
|
|
2444
|
-
async
|
|
2445
|
-
|
|
2777
|
+
async createTrigger(trigger) {
|
|
2778
|
+
const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers`, {
|
|
2446
2779
|
method: 'POST',
|
|
2447
|
-
body: JSON.stringify(
|
|
2780
|
+
body: JSON.stringify(trigger),
|
|
2448
2781
|
});
|
|
2782
|
+
// Cache the trigger locally if successful
|
|
2783
|
+
if (result.success && result.data?._id) {
|
|
2784
|
+
this.triggers.set(result.data._id, result.data);
|
|
2785
|
+
}
|
|
2786
|
+
return result;
|
|
2449
2787
|
}
|
|
2450
2788
|
/**
|
|
2451
|
-
* Update an existing
|
|
2789
|
+
* Update an existing trigger
|
|
2452
2790
|
*/
|
|
2453
|
-
async
|
|
2454
|
-
|
|
2791
|
+
async updateTrigger(triggerId, updates) {
|
|
2792
|
+
const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
|
|
2455
2793
|
method: 'PUT',
|
|
2456
2794
|
body: JSON.stringify(updates),
|
|
2457
2795
|
});
|
|
2796
|
+
// Update cache if successful
|
|
2797
|
+
if (result.success && result.data?._id) {
|
|
2798
|
+
this.triggers.set(result.data._id, result.data);
|
|
2799
|
+
}
|
|
2800
|
+
return result;
|
|
2458
2801
|
}
|
|
2459
2802
|
/**
|
|
2460
|
-
* Delete a
|
|
2803
|
+
* Delete a trigger
|
|
2461
2804
|
*/
|
|
2462
|
-
async
|
|
2463
|
-
|
|
2805
|
+
async deleteTrigger(triggerId) {
|
|
2806
|
+
const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
|
|
2464
2807
|
method: 'DELETE',
|
|
2465
2808
|
});
|
|
2809
|
+
// Remove from cache if successful
|
|
2810
|
+
if (result.success) {
|
|
2811
|
+
this.triggers.delete(triggerId);
|
|
2812
|
+
}
|
|
2813
|
+
return result;
|
|
2814
|
+
}
|
|
2815
|
+
/**
|
|
2816
|
+
* Activate a trigger
|
|
2817
|
+
*/
|
|
2818
|
+
async activateTrigger(triggerId) {
|
|
2819
|
+
return this.updateTrigger(triggerId, { isActive: true });
|
|
2820
|
+
}
|
|
2821
|
+
/**
|
|
2822
|
+
* Deactivate a trigger
|
|
2823
|
+
*/
|
|
2824
|
+
async deactivateTrigger(triggerId) {
|
|
2825
|
+
return this.updateTrigger(triggerId, { isActive: false });
|
|
2466
2826
|
}
|
|
2467
2827
|
// ============================================
|
|
2468
|
-
//
|
|
2828
|
+
// EVENT HANDLING (CLIENT-SIDE)
|
|
2469
2829
|
// ============================================
|
|
2470
2830
|
/**
|
|
2471
|
-
*
|
|
2831
|
+
* Register a local event listener for client-side triggers
|
|
2832
|
+
* This allows immediate client-side reactions to events
|
|
2472
2833
|
*/
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
if (params?.pipelineId)
|
|
2480
|
-
queryParams.set('pipelineId', params.pipelineId);
|
|
2481
|
-
if (params?.stageId)
|
|
2482
|
-
queryParams.set('stageId', params.stageId);
|
|
2483
|
-
const query = queryParams.toString();
|
|
2484
|
-
const endpoint = `/api/workspaces/${this.workspaceId}/opportunities${query ? `?${query}` : ''}`;
|
|
2485
|
-
return this.request(endpoint);
|
|
2834
|
+
on(eventType, callback) {
|
|
2835
|
+
if (!this.listeners.has(eventType)) {
|
|
2836
|
+
this.listeners.set(eventType, new Set());
|
|
2837
|
+
}
|
|
2838
|
+
this.listeners.get(eventType).add(callback);
|
|
2839
|
+
logger.debug(`Event listener registered: ${eventType}`);
|
|
2486
2840
|
}
|
|
2487
2841
|
/**
|
|
2488
|
-
*
|
|
2842
|
+
* Remove an event listener
|
|
2489
2843
|
*/
|
|
2490
|
-
|
|
2491
|
-
|
|
2844
|
+
off(eventType, callback) {
|
|
2845
|
+
const listeners = this.listeners.get(eventType);
|
|
2846
|
+
if (listeners) {
|
|
2847
|
+
listeners.delete(callback);
|
|
2848
|
+
}
|
|
2492
2849
|
}
|
|
2493
2850
|
/**
|
|
2494
|
-
*
|
|
2851
|
+
* Emit an event (client-side only)
|
|
2852
|
+
* This will trigger any registered local listeners
|
|
2495
2853
|
*/
|
|
2496
|
-
|
|
2497
|
-
|
|
2854
|
+
emit(eventType, data) {
|
|
2855
|
+
logger.debug(`Event emitted: ${eventType}`, data);
|
|
2856
|
+
const listeners = this.listeners.get(eventType);
|
|
2857
|
+
if (listeners) {
|
|
2858
|
+
listeners.forEach(callback => {
|
|
2859
|
+
try {
|
|
2860
|
+
callback(data);
|
|
2861
|
+
}
|
|
2862
|
+
catch (error) {
|
|
2863
|
+
logger.error(`Error in event listener for ${eventType}:`, error);
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2869
|
+
* Check if conditions are met for a trigger
|
|
2870
|
+
* Supports dynamic field evaluation including custom fields and nested paths
|
|
2871
|
+
*/
|
|
2872
|
+
evaluateConditions(conditions, data) {
|
|
2873
|
+
if (!conditions || conditions.length === 0) {
|
|
2874
|
+
return true; // No conditions means always fire
|
|
2875
|
+
}
|
|
2876
|
+
return conditions.every(condition => {
|
|
2877
|
+
// Support dot notation for nested fields (e.g., 'customFields.industry')
|
|
2878
|
+
const fieldValue = condition.field.includes('.')
|
|
2879
|
+
? this.getNestedValue(data, condition.field)
|
|
2880
|
+
: data[condition.field];
|
|
2881
|
+
const targetValue = condition.value;
|
|
2882
|
+
switch (condition.operator) {
|
|
2883
|
+
case 'equals':
|
|
2884
|
+
return fieldValue === targetValue;
|
|
2885
|
+
case 'not_equals':
|
|
2886
|
+
return fieldValue !== targetValue;
|
|
2887
|
+
case 'contains':
|
|
2888
|
+
return String(fieldValue).includes(String(targetValue));
|
|
2889
|
+
case 'greater_than':
|
|
2890
|
+
return Number(fieldValue) > Number(targetValue);
|
|
2891
|
+
case 'less_than':
|
|
2892
|
+
return Number(fieldValue) < Number(targetValue);
|
|
2893
|
+
case 'in':
|
|
2894
|
+
return Array.isArray(targetValue) && targetValue.includes(fieldValue);
|
|
2895
|
+
case 'not_in':
|
|
2896
|
+
return Array.isArray(targetValue) && !targetValue.includes(fieldValue);
|
|
2897
|
+
default:
|
|
2898
|
+
return false;
|
|
2899
|
+
}
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
/**
|
|
2903
|
+
* Execute actions for a triggered event (client-side preview)
|
|
2904
|
+
* Note: Actual execution happens on the backend
|
|
2905
|
+
*/
|
|
2906
|
+
async executeActions(trigger, data) {
|
|
2907
|
+
logger.info(`Executing actions for trigger: ${trigger.name}`);
|
|
2908
|
+
for (const action of trigger.actions) {
|
|
2909
|
+
try {
|
|
2910
|
+
await this.executeAction(action, data);
|
|
2911
|
+
}
|
|
2912
|
+
catch (error) {
|
|
2913
|
+
logger.error(`Failed to execute action:`, error);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
/**
|
|
2918
|
+
* Execute a single action
|
|
2919
|
+
*/
|
|
2920
|
+
async executeAction(action, data) {
|
|
2921
|
+
switch (action.type) {
|
|
2922
|
+
case 'send_email':
|
|
2923
|
+
await this.executeSendEmail(action, data);
|
|
2924
|
+
break;
|
|
2925
|
+
case 'webhook':
|
|
2926
|
+
await this.executeWebhook(action, data);
|
|
2927
|
+
break;
|
|
2928
|
+
case 'create_task':
|
|
2929
|
+
await this.executeCreateTask(action, data);
|
|
2930
|
+
break;
|
|
2931
|
+
case 'update_contact':
|
|
2932
|
+
await this.executeUpdateContact(action, data);
|
|
2933
|
+
break;
|
|
2934
|
+
default:
|
|
2935
|
+
logger.warn(`Unknown action type:`, action);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
/**
|
|
2939
|
+
* Execute send email action (via backend API)
|
|
2940
|
+
*/
|
|
2941
|
+
async executeSendEmail(action, data) {
|
|
2942
|
+
logger.debug('Sending email:', action);
|
|
2943
|
+
const payload = {
|
|
2944
|
+
to: this.replaceVariables(action.to, data),
|
|
2945
|
+
subject: action.subject ? this.replaceVariables(action.subject, data) : undefined,
|
|
2946
|
+
body: action.body ? this.replaceVariables(action.body, data) : undefined,
|
|
2947
|
+
templateId: action.templateId,
|
|
2948
|
+
cc: action.cc,
|
|
2949
|
+
bcc: action.bcc,
|
|
2950
|
+
from: action.from,
|
|
2951
|
+
delayMinutes: action.delayMinutes,
|
|
2952
|
+
};
|
|
2953
|
+
await this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
|
|
2498
2954
|
method: 'POST',
|
|
2499
|
-
body: JSON.stringify(
|
|
2955
|
+
body: JSON.stringify(payload),
|
|
2500
2956
|
});
|
|
2501
2957
|
}
|
|
2502
2958
|
/**
|
|
2503
|
-
*
|
|
2959
|
+
* Execute webhook action
|
|
2504
2960
|
*/
|
|
2505
|
-
async
|
|
2506
|
-
|
|
2961
|
+
async executeWebhook(action, data) {
|
|
2962
|
+
logger.debug('Calling webhook:', action.url);
|
|
2963
|
+
const body = action.body ? this.replaceVariables(action.body, data) : JSON.stringify(data);
|
|
2964
|
+
await fetch(action.url, {
|
|
2965
|
+
method: action.method,
|
|
2966
|
+
headers: {
|
|
2967
|
+
'Content-Type': 'application/json',
|
|
2968
|
+
...action.headers,
|
|
2969
|
+
},
|
|
2970
|
+
body,
|
|
2971
|
+
});
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Execute create task action
|
|
2975
|
+
*/
|
|
2976
|
+
async executeCreateTask(action, data) {
|
|
2977
|
+
logger.debug('Creating task:', action.title);
|
|
2978
|
+
const dueDate = action.dueDays
|
|
2979
|
+
? new Date(Date.now() + action.dueDays * 24 * 60 * 60 * 1000).toISOString()
|
|
2980
|
+
: undefined;
|
|
2981
|
+
await this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
|
|
2982
|
+
method: 'POST',
|
|
2983
|
+
body: JSON.stringify({
|
|
2984
|
+
title: this.replaceVariables(action.title, data),
|
|
2985
|
+
description: action.description ? this.replaceVariables(action.description, data) : undefined,
|
|
2986
|
+
priority: action.priority,
|
|
2987
|
+
dueDate,
|
|
2988
|
+
assignedTo: action.assignedTo,
|
|
2989
|
+
relatedContactId: typeof data.contactId === 'string' ? data.contactId : undefined,
|
|
2990
|
+
}),
|
|
2991
|
+
});
|
|
2992
|
+
}
|
|
2993
|
+
/**
|
|
2994
|
+
* Execute update contact action
|
|
2995
|
+
*/
|
|
2996
|
+
async executeUpdateContact(action, data) {
|
|
2997
|
+
const contactId = data.contactId || data._id;
|
|
2998
|
+
if (!contactId) {
|
|
2999
|
+
logger.warn('Cannot update contact: no contactId in data');
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
logger.debug('Updating contact:', contactId);
|
|
3003
|
+
await this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
|
|
2507
3004
|
method: 'PUT',
|
|
2508
|
-
body: JSON.stringify(updates),
|
|
3005
|
+
body: JSON.stringify(action.updates),
|
|
2509
3006
|
});
|
|
2510
3007
|
}
|
|
2511
3008
|
/**
|
|
2512
|
-
*
|
|
3009
|
+
* Replace variables in a string template
|
|
3010
|
+
* Supports syntax like {{contact.email}}, {{opportunity.value}}
|
|
2513
3011
|
*/
|
|
2514
|
-
|
|
2515
|
-
return
|
|
2516
|
-
|
|
3012
|
+
replaceVariables(template, data) {
|
|
3013
|
+
return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
|
|
3014
|
+
const value = this.getNestedValue(data, path.trim());
|
|
3015
|
+
return value !== undefined ? String(value) : match;
|
|
2517
3016
|
});
|
|
2518
3017
|
}
|
|
2519
3018
|
/**
|
|
2520
|
-
*
|
|
3019
|
+
* Get nested value from object using dot notation
|
|
3020
|
+
* Supports dynamic field access including custom fields
|
|
2521
3021
|
*/
|
|
2522
|
-
|
|
2523
|
-
return
|
|
2524
|
-
|
|
2525
|
-
|
|
3022
|
+
getNestedValue(obj, path) {
|
|
3023
|
+
return path.split('.').reduce((current, key) => {
|
|
3024
|
+
return current !== null && current !== undefined && typeof current === 'object'
|
|
3025
|
+
? current[key]
|
|
3026
|
+
: undefined;
|
|
3027
|
+
}, obj);
|
|
3028
|
+
}
|
|
3029
|
+
/**
|
|
3030
|
+
* Extract all available field paths from a data object
|
|
3031
|
+
* Useful for dynamic field discovery based on platform-specific attributes
|
|
3032
|
+
* @param obj - The data object to extract fields from
|
|
3033
|
+
* @param prefix - Internal use for nested paths
|
|
3034
|
+
* @param maxDepth - Maximum depth to traverse (default: 3)
|
|
3035
|
+
* @returns Array of field paths (e.g., ['email', 'contact.firstName', 'customFields.industry'])
|
|
3036
|
+
*/
|
|
3037
|
+
extractAvailableFields(obj, prefix = '', maxDepth = 3) {
|
|
3038
|
+
if (maxDepth <= 0)
|
|
3039
|
+
return [];
|
|
3040
|
+
const fields = [];
|
|
3041
|
+
for (const key in obj) {
|
|
3042
|
+
if (!obj.hasOwnProperty(key))
|
|
3043
|
+
continue;
|
|
3044
|
+
const value = obj[key];
|
|
3045
|
+
const fieldPath = prefix ? `${prefix}.${key}` : key;
|
|
3046
|
+
fields.push(fieldPath);
|
|
3047
|
+
// Recursively traverse nested objects
|
|
3048
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
3049
|
+
const nestedFields = this.extractAvailableFields(value, fieldPath, maxDepth - 1);
|
|
3050
|
+
fields.push(...nestedFields);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
return fields;
|
|
3054
|
+
}
|
|
3055
|
+
/**
|
|
3056
|
+
* Get available fields from sample data
|
|
3057
|
+
* Helps with dynamic field detection for platform-specific attributes
|
|
3058
|
+
* @param sampleData - Sample data object to analyze
|
|
3059
|
+
* @returns Array of available field paths
|
|
3060
|
+
*/
|
|
3061
|
+
getAvailableFields(sampleData) {
|
|
3062
|
+
return this.extractAvailableFields(sampleData);
|
|
3063
|
+
}
|
|
3064
|
+
// ============================================
|
|
3065
|
+
// HELPER METHODS FOR COMMON PATTERNS
|
|
3066
|
+
// ============================================
|
|
3067
|
+
/**
|
|
3068
|
+
* Create a simple email trigger
|
|
3069
|
+
* Helper method for common use case
|
|
3070
|
+
*/
|
|
3071
|
+
async createEmailTrigger(config) {
|
|
3072
|
+
return this.createTrigger({
|
|
3073
|
+
name: config.name,
|
|
3074
|
+
eventType: config.eventType,
|
|
3075
|
+
conditions: config.conditions,
|
|
3076
|
+
actions: [
|
|
3077
|
+
{
|
|
3078
|
+
type: 'send_email',
|
|
3079
|
+
to: config.to,
|
|
3080
|
+
subject: config.subject,
|
|
3081
|
+
body: config.body,
|
|
3082
|
+
},
|
|
3083
|
+
],
|
|
3084
|
+
isActive: true,
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
/**
|
|
3088
|
+
* Create a task creation trigger
|
|
3089
|
+
*/
|
|
3090
|
+
async createTaskTrigger(config) {
|
|
3091
|
+
return this.createTrigger({
|
|
3092
|
+
name: config.name,
|
|
3093
|
+
eventType: config.eventType,
|
|
3094
|
+
conditions: config.conditions,
|
|
3095
|
+
actions: [
|
|
3096
|
+
{
|
|
3097
|
+
type: 'create_task',
|
|
3098
|
+
title: config.taskTitle,
|
|
3099
|
+
description: config.taskDescription,
|
|
3100
|
+
priority: config.priority,
|
|
3101
|
+
dueDays: config.dueDays,
|
|
3102
|
+
},
|
|
3103
|
+
],
|
|
3104
|
+
isActive: true,
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
/**
|
|
3108
|
+
* Create a webhook trigger
|
|
3109
|
+
*/
|
|
3110
|
+
async createWebhookTrigger(config) {
|
|
3111
|
+
return this.createTrigger({
|
|
3112
|
+
name: config.name,
|
|
3113
|
+
eventType: config.eventType,
|
|
3114
|
+
conditions: config.conditions,
|
|
3115
|
+
actions: [
|
|
3116
|
+
{
|
|
3117
|
+
type: 'webhook',
|
|
3118
|
+
url: config.webhookUrl,
|
|
3119
|
+
method: config.method || 'POST',
|
|
3120
|
+
},
|
|
3121
|
+
],
|
|
3122
|
+
isActive: true,
|
|
2526
3123
|
});
|
|
2527
3124
|
}
|
|
2528
3125
|
}
|
|
2529
3126
|
|
|
3127
|
+
/**
|
|
3128
|
+
* Clianta SDK - CRM API Client
|
|
3129
|
+
* @see SDK_VERSION in core/config.ts
|
|
3130
|
+
*/
|
|
3131
|
+
/**
|
|
3132
|
+
* CRM API Client for managing contacts and opportunities
|
|
3133
|
+
*/
|
|
3134
|
+
class CRMClient {
|
|
3135
|
+
constructor(apiEndpoint, workspaceId, authToken) {
|
|
3136
|
+
this.apiEndpoint = apiEndpoint;
|
|
3137
|
+
this.workspaceId = workspaceId;
|
|
3138
|
+
this.authToken = authToken;
|
|
3139
|
+
this.triggers = new EventTriggersManager(apiEndpoint, workspaceId, authToken);
|
|
3140
|
+
}
|
|
3141
|
+
/**
|
|
3142
|
+
* Set authentication token for API requests
|
|
3143
|
+
*/
|
|
3144
|
+
setAuthToken(token) {
|
|
3145
|
+
this.authToken = token;
|
|
3146
|
+
this.triggers.setAuthToken(token);
|
|
3147
|
+
}
|
|
3148
|
+
/**
|
|
3149
|
+
* Validate required parameter exists
|
|
3150
|
+
* @throws {Error} if value is null/undefined or empty string
|
|
3151
|
+
*/
|
|
3152
|
+
validateRequired(param, value, methodName) {
|
|
3153
|
+
if (value === null || value === undefined || value === '') {
|
|
3154
|
+
throw new Error(`[CRMClient.${methodName}] ${param} is required`);
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
/**
|
|
3158
|
+
* Make authenticated API request
|
|
3159
|
+
*/
|
|
3160
|
+
async request(endpoint, options = {}) {
|
|
3161
|
+
const url = `${this.apiEndpoint}${endpoint}`;
|
|
3162
|
+
const headers = {
|
|
3163
|
+
'Content-Type': 'application/json',
|
|
3164
|
+
...(options.headers || {}),
|
|
3165
|
+
};
|
|
3166
|
+
if (this.authToken) {
|
|
3167
|
+
headers['Authorization'] = `Bearer ${this.authToken}`;
|
|
3168
|
+
}
|
|
3169
|
+
try {
|
|
3170
|
+
const response = await fetch(url, {
|
|
3171
|
+
...options,
|
|
3172
|
+
headers,
|
|
3173
|
+
});
|
|
3174
|
+
const data = await response.json();
|
|
3175
|
+
if (!response.ok) {
|
|
3176
|
+
return {
|
|
3177
|
+
success: false,
|
|
3178
|
+
error: data.message || 'Request failed',
|
|
3179
|
+
status: response.status,
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
3182
|
+
return {
|
|
3183
|
+
success: true,
|
|
3184
|
+
data: data.data || data,
|
|
3185
|
+
status: response.status,
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
catch (error) {
|
|
3189
|
+
return {
|
|
3190
|
+
success: false,
|
|
3191
|
+
error: error instanceof Error ? error.message : 'Network error',
|
|
3192
|
+
status: 0,
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
// ============================================
|
|
3197
|
+
// CONTACTS API
|
|
3198
|
+
// ============================================
|
|
3199
|
+
/**
|
|
3200
|
+
* Get all contacts with pagination
|
|
3201
|
+
*/
|
|
3202
|
+
async getContacts(params) {
|
|
3203
|
+
const queryParams = new URLSearchParams();
|
|
3204
|
+
if (params?.page)
|
|
3205
|
+
queryParams.set('page', params.page.toString());
|
|
3206
|
+
if (params?.limit)
|
|
3207
|
+
queryParams.set('limit', params.limit.toString());
|
|
3208
|
+
if (params?.search)
|
|
3209
|
+
queryParams.set('search', params.search);
|
|
3210
|
+
if (params?.status)
|
|
3211
|
+
queryParams.set('status', params.status);
|
|
3212
|
+
const query = queryParams.toString();
|
|
3213
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts${query ? `?${query}` : ''}`;
|
|
3214
|
+
return this.request(endpoint);
|
|
3215
|
+
}
|
|
3216
|
+
/**
|
|
3217
|
+
* Get a single contact by ID
|
|
3218
|
+
*/
|
|
3219
|
+
async getContact(contactId) {
|
|
3220
|
+
this.validateRequired('contactId', contactId, 'getContact');
|
|
3221
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
|
|
3222
|
+
}
|
|
3223
|
+
/**
|
|
3224
|
+
* Create a new contact
|
|
3225
|
+
*/
|
|
3226
|
+
async createContact(contact) {
|
|
3227
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts`, {
|
|
3228
|
+
method: 'POST',
|
|
3229
|
+
body: JSON.stringify(contact),
|
|
3230
|
+
});
|
|
3231
|
+
}
|
|
3232
|
+
/**
|
|
3233
|
+
* Update an existing contact
|
|
3234
|
+
*/
|
|
3235
|
+
async updateContact(contactId, updates) {
|
|
3236
|
+
this.validateRequired('contactId', contactId, 'updateContact');
|
|
3237
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
|
|
3238
|
+
method: 'PUT',
|
|
3239
|
+
body: JSON.stringify(updates),
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
/**
|
|
3243
|
+
* Delete a contact
|
|
3244
|
+
*/
|
|
3245
|
+
async deleteContact(contactId) {
|
|
3246
|
+
this.validateRequired('contactId', contactId, 'deleteContact');
|
|
3247
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
|
|
3248
|
+
method: 'DELETE',
|
|
3249
|
+
});
|
|
3250
|
+
}
|
|
3251
|
+
// ============================================
|
|
3252
|
+
// OPPORTUNITIES API
|
|
3253
|
+
// ============================================
|
|
3254
|
+
/**
|
|
3255
|
+
* Get all opportunities with pagination
|
|
3256
|
+
*/
|
|
3257
|
+
async getOpportunities(params) {
|
|
3258
|
+
const queryParams = new URLSearchParams();
|
|
3259
|
+
if (params?.page)
|
|
3260
|
+
queryParams.set('page', params.page.toString());
|
|
3261
|
+
if (params?.limit)
|
|
3262
|
+
queryParams.set('limit', params.limit.toString());
|
|
3263
|
+
if (params?.pipelineId)
|
|
3264
|
+
queryParams.set('pipelineId', params.pipelineId);
|
|
3265
|
+
if (params?.stageId)
|
|
3266
|
+
queryParams.set('stageId', params.stageId);
|
|
3267
|
+
const query = queryParams.toString();
|
|
3268
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/opportunities${query ? `?${query}` : ''}`;
|
|
3269
|
+
return this.request(endpoint);
|
|
3270
|
+
}
|
|
3271
|
+
/**
|
|
3272
|
+
* Get a single opportunity by ID
|
|
3273
|
+
*/
|
|
3274
|
+
async getOpportunity(opportunityId) {
|
|
3275
|
+
return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`);
|
|
3276
|
+
}
|
|
3277
|
+
/**
|
|
3278
|
+
* Create a new opportunity
|
|
3279
|
+
*/
|
|
3280
|
+
async createOpportunity(opportunity) {
|
|
3281
|
+
return this.request(`/api/workspaces/${this.workspaceId}/opportunities`, {
|
|
3282
|
+
method: 'POST',
|
|
3283
|
+
body: JSON.stringify(opportunity),
|
|
3284
|
+
});
|
|
3285
|
+
}
|
|
3286
|
+
/**
|
|
3287
|
+
* Update an existing opportunity
|
|
3288
|
+
*/
|
|
3289
|
+
async updateOpportunity(opportunityId, updates) {
|
|
3290
|
+
return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
|
|
3291
|
+
method: 'PUT',
|
|
3292
|
+
body: JSON.stringify(updates),
|
|
3293
|
+
});
|
|
3294
|
+
}
|
|
3295
|
+
/**
|
|
3296
|
+
* Delete an opportunity
|
|
3297
|
+
*/
|
|
3298
|
+
async deleteOpportunity(opportunityId) {
|
|
3299
|
+
return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
|
|
3300
|
+
method: 'DELETE',
|
|
3301
|
+
});
|
|
3302
|
+
}
|
|
3303
|
+
/**
|
|
3304
|
+
* Move opportunity to a different stage
|
|
3305
|
+
*/
|
|
3306
|
+
async moveOpportunity(opportunityId, stageId) {
|
|
3307
|
+
return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/move`, {
|
|
3308
|
+
method: 'POST',
|
|
3309
|
+
body: JSON.stringify({ stageId }),
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
// ============================================
|
|
3313
|
+
// COMPANIES API
|
|
3314
|
+
// ============================================
|
|
3315
|
+
/**
|
|
3316
|
+
* Get all companies with pagination
|
|
3317
|
+
*/
|
|
3318
|
+
async getCompanies(params) {
|
|
3319
|
+
const queryParams = new URLSearchParams();
|
|
3320
|
+
if (params?.page)
|
|
3321
|
+
queryParams.set('page', params.page.toString());
|
|
3322
|
+
if (params?.limit)
|
|
3323
|
+
queryParams.set('limit', params.limit.toString());
|
|
3324
|
+
if (params?.search)
|
|
3325
|
+
queryParams.set('search', params.search);
|
|
3326
|
+
if (params?.status)
|
|
3327
|
+
queryParams.set('status', params.status);
|
|
3328
|
+
if (params?.industry)
|
|
3329
|
+
queryParams.set('industry', params.industry);
|
|
3330
|
+
const query = queryParams.toString();
|
|
3331
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/companies${query ? `?${query}` : ''}`;
|
|
3332
|
+
return this.request(endpoint);
|
|
3333
|
+
}
|
|
3334
|
+
/**
|
|
3335
|
+
* Get a single company by ID
|
|
3336
|
+
*/
|
|
3337
|
+
async getCompany(companyId) {
|
|
3338
|
+
return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`);
|
|
3339
|
+
}
|
|
3340
|
+
/**
|
|
3341
|
+
* Create a new company
|
|
3342
|
+
*/
|
|
3343
|
+
async createCompany(company) {
|
|
3344
|
+
return this.request(`/api/workspaces/${this.workspaceId}/companies`, {
|
|
3345
|
+
method: 'POST',
|
|
3346
|
+
body: JSON.stringify(company),
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
/**
|
|
3350
|
+
* Update an existing company
|
|
3351
|
+
*/
|
|
3352
|
+
async updateCompany(companyId, updates) {
|
|
3353
|
+
return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
|
|
3354
|
+
method: 'PUT',
|
|
3355
|
+
body: JSON.stringify(updates),
|
|
3356
|
+
});
|
|
3357
|
+
}
|
|
3358
|
+
/**
|
|
3359
|
+
* Delete a company
|
|
3360
|
+
*/
|
|
3361
|
+
async deleteCompany(companyId) {
|
|
3362
|
+
return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
|
|
3363
|
+
method: 'DELETE',
|
|
3364
|
+
});
|
|
3365
|
+
}
|
|
3366
|
+
/**
|
|
3367
|
+
* Get contacts belonging to a company
|
|
3368
|
+
*/
|
|
3369
|
+
async getCompanyContacts(companyId, params) {
|
|
3370
|
+
const queryParams = new URLSearchParams();
|
|
3371
|
+
if (params?.page)
|
|
3372
|
+
queryParams.set('page', params.page.toString());
|
|
3373
|
+
if (params?.limit)
|
|
3374
|
+
queryParams.set('limit', params.limit.toString());
|
|
3375
|
+
const query = queryParams.toString();
|
|
3376
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/contacts${query ? `?${query}` : ''}`;
|
|
3377
|
+
return this.request(endpoint);
|
|
3378
|
+
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Get deals/opportunities belonging to a company
|
|
3381
|
+
*/
|
|
3382
|
+
async getCompanyDeals(companyId, params) {
|
|
3383
|
+
const queryParams = new URLSearchParams();
|
|
3384
|
+
if (params?.page)
|
|
3385
|
+
queryParams.set('page', params.page.toString());
|
|
3386
|
+
if (params?.limit)
|
|
3387
|
+
queryParams.set('limit', params.limit.toString());
|
|
3388
|
+
const query = queryParams.toString();
|
|
3389
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/deals${query ? `?${query}` : ''}`;
|
|
3390
|
+
return this.request(endpoint);
|
|
3391
|
+
}
|
|
3392
|
+
// ============================================
|
|
3393
|
+
// PIPELINES API
|
|
3394
|
+
// ============================================
|
|
3395
|
+
/**
|
|
3396
|
+
* Get all pipelines
|
|
3397
|
+
*/
|
|
3398
|
+
async getPipelines() {
|
|
3399
|
+
return this.request(`/api/workspaces/${this.workspaceId}/pipelines`);
|
|
3400
|
+
}
|
|
3401
|
+
/**
|
|
3402
|
+
* Get a single pipeline by ID
|
|
3403
|
+
*/
|
|
3404
|
+
async getPipeline(pipelineId) {
|
|
3405
|
+
return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`);
|
|
3406
|
+
}
|
|
3407
|
+
/**
|
|
3408
|
+
* Create a new pipeline
|
|
3409
|
+
*/
|
|
3410
|
+
async createPipeline(pipeline) {
|
|
3411
|
+
return this.request(`/api/workspaces/${this.workspaceId}/pipelines`, {
|
|
3412
|
+
method: 'POST',
|
|
3413
|
+
body: JSON.stringify(pipeline),
|
|
3414
|
+
});
|
|
3415
|
+
}
|
|
3416
|
+
/**
|
|
3417
|
+
* Update an existing pipeline
|
|
3418
|
+
*/
|
|
3419
|
+
async updatePipeline(pipelineId, updates) {
|
|
3420
|
+
return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
|
|
3421
|
+
method: 'PUT',
|
|
3422
|
+
body: JSON.stringify(updates),
|
|
3423
|
+
});
|
|
3424
|
+
}
|
|
3425
|
+
/**
|
|
3426
|
+
* Delete a pipeline
|
|
3427
|
+
*/
|
|
3428
|
+
async deletePipeline(pipelineId) {
|
|
3429
|
+
return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
|
|
3430
|
+
method: 'DELETE',
|
|
3431
|
+
});
|
|
3432
|
+
}
|
|
3433
|
+
// ============================================
|
|
3434
|
+
// TASKS API
|
|
3435
|
+
// ============================================
|
|
3436
|
+
/**
|
|
3437
|
+
* Get all tasks with pagination
|
|
3438
|
+
*/
|
|
3439
|
+
async getTasks(params) {
|
|
3440
|
+
const queryParams = new URLSearchParams();
|
|
3441
|
+
if (params?.page)
|
|
3442
|
+
queryParams.set('page', params.page.toString());
|
|
3443
|
+
if (params?.limit)
|
|
3444
|
+
queryParams.set('limit', params.limit.toString());
|
|
3445
|
+
if (params?.status)
|
|
3446
|
+
queryParams.set('status', params.status);
|
|
3447
|
+
if (params?.priority)
|
|
3448
|
+
queryParams.set('priority', params.priority);
|
|
3449
|
+
if (params?.contactId)
|
|
3450
|
+
queryParams.set('contactId', params.contactId);
|
|
3451
|
+
if (params?.companyId)
|
|
3452
|
+
queryParams.set('companyId', params.companyId);
|
|
3453
|
+
if (params?.opportunityId)
|
|
3454
|
+
queryParams.set('opportunityId', params.opportunityId);
|
|
3455
|
+
const query = queryParams.toString();
|
|
3456
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/tasks${query ? `?${query}` : ''}`;
|
|
3457
|
+
return this.request(endpoint);
|
|
3458
|
+
}
|
|
3459
|
+
/**
|
|
3460
|
+
* Get a single task by ID
|
|
3461
|
+
*/
|
|
3462
|
+
async getTask(taskId) {
|
|
3463
|
+
return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`);
|
|
3464
|
+
}
|
|
3465
|
+
/**
|
|
3466
|
+
* Create a new task
|
|
3467
|
+
*/
|
|
3468
|
+
async createTask(task) {
|
|
3469
|
+
return this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
|
|
3470
|
+
method: 'POST',
|
|
3471
|
+
body: JSON.stringify(task),
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
/**
|
|
3475
|
+
* Update an existing task
|
|
3476
|
+
*/
|
|
3477
|
+
async updateTask(taskId, updates) {
|
|
3478
|
+
return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
|
|
3479
|
+
method: 'PUT',
|
|
3480
|
+
body: JSON.stringify(updates),
|
|
3481
|
+
});
|
|
3482
|
+
}
|
|
3483
|
+
/**
|
|
3484
|
+
* Mark a task as completed
|
|
3485
|
+
*/
|
|
3486
|
+
async completeTask(taskId) {
|
|
3487
|
+
return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}/complete`, {
|
|
3488
|
+
method: 'PATCH',
|
|
3489
|
+
});
|
|
3490
|
+
}
|
|
3491
|
+
/**
|
|
3492
|
+
* Delete a task
|
|
3493
|
+
*/
|
|
3494
|
+
async deleteTask(taskId) {
|
|
3495
|
+
return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
|
|
3496
|
+
method: 'DELETE',
|
|
3497
|
+
});
|
|
3498
|
+
}
|
|
3499
|
+
// ============================================
|
|
3500
|
+
// ACTIVITIES API
|
|
3501
|
+
// ============================================
|
|
3502
|
+
/**
|
|
3503
|
+
* Get activities for a contact
|
|
3504
|
+
*/
|
|
3505
|
+
async getContactActivities(contactId, params) {
|
|
3506
|
+
const queryParams = new URLSearchParams();
|
|
3507
|
+
if (params?.page)
|
|
3508
|
+
queryParams.set('page', params.page.toString());
|
|
3509
|
+
if (params?.limit)
|
|
3510
|
+
queryParams.set('limit', params.limit.toString());
|
|
3511
|
+
if (params?.type)
|
|
3512
|
+
queryParams.set('type', params.type);
|
|
3513
|
+
const query = queryParams.toString();
|
|
3514
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
|
|
3515
|
+
return this.request(endpoint);
|
|
3516
|
+
}
|
|
3517
|
+
/**
|
|
3518
|
+
* Get activities for an opportunity/deal
|
|
3519
|
+
*/
|
|
3520
|
+
async getOpportunityActivities(opportunityId, params) {
|
|
3521
|
+
const queryParams = new URLSearchParams();
|
|
3522
|
+
if (params?.page)
|
|
3523
|
+
queryParams.set('page', params.page.toString());
|
|
3524
|
+
if (params?.limit)
|
|
3525
|
+
queryParams.set('limit', params.limit.toString());
|
|
3526
|
+
if (params?.type)
|
|
3527
|
+
queryParams.set('type', params.type);
|
|
3528
|
+
const query = queryParams.toString();
|
|
3529
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/activities${query ? `?${query}` : ''}`;
|
|
3530
|
+
return this.request(endpoint);
|
|
3531
|
+
}
|
|
3532
|
+
/**
|
|
3533
|
+
* Create a new activity
|
|
3534
|
+
*/
|
|
3535
|
+
async createActivity(activity) {
|
|
3536
|
+
// Determine the correct endpoint based on related entity
|
|
3537
|
+
let endpoint;
|
|
3538
|
+
if (activity.opportunityId) {
|
|
3539
|
+
endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${activity.opportunityId}/activities`;
|
|
3540
|
+
}
|
|
3541
|
+
else if (activity.contactId) {
|
|
3542
|
+
endpoint = `/api/workspaces/${this.workspaceId}/contacts/${activity.contactId}/activities`;
|
|
3543
|
+
}
|
|
3544
|
+
else {
|
|
3545
|
+
endpoint = `/api/workspaces/${this.workspaceId}/activities`;
|
|
3546
|
+
}
|
|
3547
|
+
return this.request(endpoint, {
|
|
3548
|
+
method: 'POST',
|
|
3549
|
+
body: JSON.stringify(activity),
|
|
3550
|
+
});
|
|
3551
|
+
}
|
|
3552
|
+
/**
|
|
3553
|
+
* Update an existing activity
|
|
3554
|
+
*/
|
|
3555
|
+
async updateActivity(activityId, updates) {
|
|
3556
|
+
return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
|
|
3557
|
+
method: 'PATCH',
|
|
3558
|
+
body: JSON.stringify(updates),
|
|
3559
|
+
});
|
|
3560
|
+
}
|
|
3561
|
+
/**
|
|
3562
|
+
* Delete an activity
|
|
3563
|
+
*/
|
|
3564
|
+
async deleteActivity(activityId) {
|
|
3565
|
+
return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
|
|
3566
|
+
method: 'DELETE',
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3569
|
+
/**
|
|
3570
|
+
* Log a call activity
|
|
3571
|
+
*/
|
|
3572
|
+
async logCall(data) {
|
|
3573
|
+
return this.createActivity({
|
|
3574
|
+
type: 'call',
|
|
3575
|
+
title: `${data.direction === 'inbound' ? 'Inbound' : 'Outbound'} Call`,
|
|
3576
|
+
direction: data.direction,
|
|
3577
|
+
duration: data.duration,
|
|
3578
|
+
outcome: data.outcome,
|
|
3579
|
+
description: data.notes,
|
|
3580
|
+
contactId: data.contactId,
|
|
3581
|
+
opportunityId: data.opportunityId,
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
/**
|
|
3585
|
+
* Log a meeting activity
|
|
3586
|
+
*/
|
|
3587
|
+
async logMeeting(data) {
|
|
3588
|
+
return this.createActivity({
|
|
3589
|
+
type: 'meeting',
|
|
3590
|
+
title: data.title,
|
|
3591
|
+
duration: data.duration,
|
|
3592
|
+
outcome: data.outcome,
|
|
3593
|
+
description: data.notes,
|
|
3594
|
+
contactId: data.contactId,
|
|
3595
|
+
opportunityId: data.opportunityId,
|
|
3596
|
+
});
|
|
3597
|
+
}
|
|
3598
|
+
/**
|
|
3599
|
+
* Add a note to a contact or opportunity
|
|
3600
|
+
*/
|
|
3601
|
+
async addNote(data) {
|
|
3602
|
+
return this.createActivity({
|
|
3603
|
+
type: 'note',
|
|
3604
|
+
title: 'Note',
|
|
3605
|
+
description: data.content,
|
|
3606
|
+
contactId: data.contactId,
|
|
3607
|
+
opportunityId: data.opportunityId,
|
|
3608
|
+
});
|
|
3609
|
+
}
|
|
3610
|
+
// ============================================
|
|
3611
|
+
// EMAIL TEMPLATES API
|
|
3612
|
+
// ============================================
|
|
3613
|
+
/**
|
|
3614
|
+
* Get all email templates
|
|
3615
|
+
*/
|
|
3616
|
+
async getEmailTemplates(params) {
|
|
3617
|
+
const queryParams = new URLSearchParams();
|
|
3618
|
+
if (params?.page)
|
|
3619
|
+
queryParams.set('page', params.page.toString());
|
|
3620
|
+
if (params?.limit)
|
|
3621
|
+
queryParams.set('limit', params.limit.toString());
|
|
3622
|
+
const query = queryParams.toString();
|
|
3623
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/email-templates${query ? `?${query}` : ''}`;
|
|
3624
|
+
return this.request(endpoint);
|
|
3625
|
+
}
|
|
3626
|
+
/**
|
|
3627
|
+
* Get a single email template by ID
|
|
3628
|
+
*/
|
|
3629
|
+
async getEmailTemplate(templateId) {
|
|
3630
|
+
return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`);
|
|
3631
|
+
}
|
|
3632
|
+
/**
|
|
3633
|
+
* Create a new email template
|
|
3634
|
+
*/
|
|
3635
|
+
async createEmailTemplate(template) {
|
|
3636
|
+
return this.request(`/api/workspaces/${this.workspaceId}/email-templates`, {
|
|
3637
|
+
method: 'POST',
|
|
3638
|
+
body: JSON.stringify(template),
|
|
3639
|
+
});
|
|
3640
|
+
}
|
|
3641
|
+
/**
|
|
3642
|
+
* Update an email template
|
|
3643
|
+
*/
|
|
3644
|
+
async updateEmailTemplate(templateId, updates) {
|
|
3645
|
+
return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
|
|
3646
|
+
method: 'PUT',
|
|
3647
|
+
body: JSON.stringify(updates),
|
|
3648
|
+
});
|
|
3649
|
+
}
|
|
3650
|
+
/**
|
|
3651
|
+
* Delete an email template
|
|
3652
|
+
*/
|
|
3653
|
+
async deleteEmailTemplate(templateId) {
|
|
3654
|
+
return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
|
|
3655
|
+
method: 'DELETE',
|
|
3656
|
+
});
|
|
3657
|
+
}
|
|
3658
|
+
/**
|
|
3659
|
+
* Send an email using a template
|
|
3660
|
+
*/
|
|
3661
|
+
async sendEmail(data) {
|
|
3662
|
+
return this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
|
|
3663
|
+
method: 'POST',
|
|
3664
|
+
body: JSON.stringify(data),
|
|
3665
|
+
});
|
|
3666
|
+
}
|
|
3667
|
+
// ============================================
|
|
3668
|
+
// EVENT TRIGGERS API (delegated to triggers manager)
|
|
3669
|
+
// ============================================
|
|
3670
|
+
/**
|
|
3671
|
+
* Get all event triggers
|
|
3672
|
+
*/
|
|
3673
|
+
async getEventTriggers() {
|
|
3674
|
+
return this.triggers.getTriggers();
|
|
3675
|
+
}
|
|
3676
|
+
/**
|
|
3677
|
+
* Create a new event trigger
|
|
3678
|
+
*/
|
|
3679
|
+
async createEventTrigger(trigger) {
|
|
3680
|
+
return this.triggers.createTrigger(trigger);
|
|
3681
|
+
}
|
|
3682
|
+
/**
|
|
3683
|
+
* Update an event trigger
|
|
3684
|
+
*/
|
|
3685
|
+
async updateEventTrigger(triggerId, updates) {
|
|
3686
|
+
return this.triggers.updateTrigger(triggerId, updates);
|
|
3687
|
+
}
|
|
3688
|
+
/**
|
|
3689
|
+
* Delete an event trigger
|
|
3690
|
+
*/
|
|
3691
|
+
async deleteEventTrigger(triggerId) {
|
|
3692
|
+
return this.triggers.deleteTrigger(triggerId);
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
|
|
2530
3696
|
/**
|
|
2531
3697
|
* Clianta SDK
|
|
2532
3698
|
* Professional CRM and tracking SDK for lead generation
|
|
@@ -2579,11 +3745,13 @@
|
|
|
2579
3745
|
Tracker,
|
|
2580
3746
|
CRMClient,
|
|
2581
3747
|
ConsentManager,
|
|
3748
|
+
EventTriggersManager,
|
|
2582
3749
|
};
|
|
2583
3750
|
}
|
|
2584
3751
|
|
|
2585
3752
|
exports.CRMClient = CRMClient;
|
|
2586
3753
|
exports.ConsentManager = ConsentManager;
|
|
3754
|
+
exports.EventTriggersManager = EventTriggersManager;
|
|
2587
3755
|
exports.SDK_VERSION = SDK_VERSION;
|
|
2588
3756
|
exports.Tracker = Tracker;
|
|
2589
3757
|
exports.clianta = clianta;
|