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