cuoral-ionic 0.0.1 → 0.0.2

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.
Files changed (85) hide show
  1. package/README.md +40 -4
  2. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  3. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  4. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  5. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  6. package/android/.gradle/8.9/gc.properties +0 -0
  7. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  8. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  9. package/android/.gradle/vcs-1/gc.properties +0 -0
  10. package/android/build/.transforms/2c48e1f34ca03014b78fcb3e0ab7197b/results.bin +1 -0
  11. package/android/build/.transforms/2c48e1f34ca03014b78fcb3e0ab7197b/transformed/classes/classes_dex/classes.dex +0 -0
  12. package/android/build/.transforms/980c51bd075f726f311ad662d5d20ba0/results.bin +1 -0
  13. package/android/build/.transforms/980c51bd075f726f311ad662d5d20ba0/transformed/classes/classes_dex/classes.dex +0 -0
  14. package/android/build/.transforms/bb54161301273cf9b5b94a21c0fb3f23/results.bin +1 -0
  15. package/android/build/.transforms/bb54161301273cf9b5b94a21c0fb3f23/transformed/classes/classes_dex/classes.dex +0 -0
  16. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/results.bin +1 -0
  17. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$1.dex +0 -0
  18. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$2.dex +0 -0
  19. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin.dex +0 -0
  20. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/ScreenRecordService.dex +0 -0
  21. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/desugar_graph.bin +0 -0
  22. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/aapt/AndroidManifest.xml +22 -0
  23. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/aapt/output-metadata.json +18 -0
  24. package/android/build/intermediates/aar_main_jar/debug/classes.jar +0 -0
  25. package/android/build/intermediates/aar_metadata/debug/aar-metadata.properties +6 -0
  26. package/android/build/intermediates/annotation_processor_list/debug/annotationProcessors.json +1 -0
  27. package/android/build/intermediates/annotations_typedef_file/debug/typedefs.txt +0 -0
  28. package/android/build/intermediates/compile_library_classes_jar/debug/classes.jar +0 -0
  29. package/android/build/intermediates/compile_r_class_jar/debug/R.jar +0 -0
  30. package/android/build/intermediates/compile_symbol_list/debug/R.txt +0 -0
  31. package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +1 -0
  32. package/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +2 -0
  33. package/android/build/intermediates/incremental/debug-mergeJavaRes/merge-state +0 -0
  34. package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +2 -0
  35. package/android/build/intermediates/incremental/mergeDebugShaders/merger.xml +2 -0
  36. package/android/build/intermediates/incremental/packageDebugAssets/merger.xml +2 -0
  37. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$1.class +0 -0
  38. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$2.class +0 -0
  39. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin.class +0 -0
  40. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/ScreenRecordService.class +0 -0
  41. package/android/build/intermediates/local_only_symbol_list/debug/R-def.txt +2 -0
  42. package/android/build/intermediates/manifest_merge_blame_file/debug/manifest-merger-blame-debug-report.txt +38 -0
  43. package/android/build/intermediates/merged_java_res/debug/feature-cuoral-ionic.jar +0 -0
  44. package/android/build/intermediates/merged_manifest/debug/AndroidManifest.xml +22 -0
  45. package/android/build/intermediates/navigation_json/debug/navigation.json +1 -0
  46. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$1.class +0 -0
  47. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$2.class +0 -0
  48. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin.class +0 -0
  49. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/ScreenRecordService.class +0 -0
  50. package/android/build/intermediates/runtime_library_classes_jar/debug/classes.jar +0 -0
  51. package/android/build/intermediates/symbol_list_with_package_name/debug/package-aware-r.txt +1 -0
  52. package/android/build/outputs/aar/cuoral-ionic-debug.aar +0 -0
  53. package/android/build/outputs/logs/manifest-merger-debug-report.txt +49 -0
  54. package/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/CuoralPlugin$1.class.uniqueId1 +0 -0
  55. package/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/CuoralPlugin$2.class.uniqueId2 +0 -0
  56. package/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/CuoralPlugin.class.uniqueId0 +0 -0
  57. package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
  58. package/android/build.gradle +61 -0
  59. package/android/src/main/AndroidManifest.xml +9 -0
  60. package/android/src/main/java/com/cuoral/ionic/CuoralPlugin.java +290 -33
  61. package/android/src/main/java/com/cuoral/ionic/ScreenRecordService.java +59 -0
  62. package/dist/cuoral.d.ts +30 -1
  63. package/dist/cuoral.d.ts.map +1 -1
  64. package/dist/cuoral.js +168 -1
  65. package/dist/index.d.ts +1 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.esm.js +706 -127
  68. package/dist/index.esm.js.map +1 -1
  69. package/dist/index.js +706 -126
  70. package/dist/index.js.map +1 -1
  71. package/dist/intelligence.d.ts +66 -0
  72. package/dist/intelligence.d.ts.map +1 -0
  73. package/dist/intelligence.js +508 -0
  74. package/dist/plugin.d.ts +1 -1
  75. package/dist/plugin.d.ts.map +1 -1
  76. package/dist/plugin.js +24 -6
  77. package/dist/web.d.ts.map +1 -1
  78. package/dist/web.js +0 -3
  79. package/ios/Plugin/CuoralPlugin.swift +78 -1
  80. package/package.json +1 -1
  81. package/src/cuoral.ts +205 -1
  82. package/src/index.ts +1 -0
  83. package/src/intelligence.ts +609 -0
  84. package/src/plugin.ts +26 -6
  85. package/src/web.ts +0 -6
package/dist/index.js CHANGED
@@ -28,11 +28,9 @@ exports.CuoralMessageType = void 0;
28
28
  })(exports.CuoralMessageType || (exports.CuoralMessageType = {}));
29
29
 
30
30
  /**
31
- * Register the Capacitor plugin
31
+ * Register the Capacitor plugin (mobile only - iOS and Android)
32
32
  */
33
- const CuoralPlugin = core.registerPlugin('CuoralPlugin', {
34
- web: () => Promise.resolve().then(function () { return web; }).then(m => new m.CuoralPluginWeb()),
35
- });
33
+ const CuoralPlugin$1 = core.registerPlugin('CuoralPlugin');
36
34
  /**
37
35
  * High-level API for easier integration
38
36
  */
@@ -51,17 +49,17 @@ class CuoralRecorder {
51
49
  return false;
52
50
  }
53
51
  // Check support
54
- const { supported } = await CuoralPlugin.isRecordingSupported();
52
+ const { supported } = await CuoralPlugin$1.isRecordingSupported();
55
53
  if (!supported) {
56
54
  throw new Error('Screen recording is not supported on this device');
57
55
  }
58
56
  // Request permissions
59
- const { granted } = await CuoralPlugin.requestPermissions();
57
+ const { granted } = await CuoralPlugin$1.requestPermissions();
60
58
  if (!granted) {
61
59
  throw new Error('Recording permissions not granted');
62
60
  }
63
61
  // Start recording
64
- const result = await CuoralPlugin.startRecording(options);
62
+ const result = await CuoralPlugin$1.startRecording(options);
65
63
  if (result.success) {
66
64
  this.isRecording = true;
67
65
  this.recordingStartTime = Date.now();
@@ -71,10 +69,21 @@ class CuoralRecorder {
71
69
  payload: { timestamp: this.recordingStartTime },
72
70
  });
73
71
  }
72
+ else {
73
+ // Recording failed - reset state and notify widget
74
+ this.isRecording = false;
75
+ this.recordingStartTime = undefined;
76
+ this.postMessage({
77
+ type: exports.CuoralMessageType.RECORDING_ERROR,
78
+ payload: { error: 'Failed to start recording' },
79
+ });
80
+ }
74
81
  return result.success;
75
82
  }
76
83
  catch (error) {
77
- console.error('[Cuoral] Failed to start recording:', error);
84
+ // Exception occurred - reset state and notify widget
85
+ this.isRecording = false;
86
+ this.recordingStartTime = undefined;
78
87
  this.postMessage({
79
88
  type: exports.CuoralMessageType.RECORDING_ERROR,
80
89
  payload: { error: error.message },
@@ -88,10 +97,14 @@ class CuoralRecorder {
88
97
  async stopRecording() {
89
98
  try {
90
99
  if (!this.isRecording) {
91
- console.warn('[Cuoral] Not recording');
100
+ // Send error message to widget so it can exit "stopping" state
101
+ this.postMessage({
102
+ type: exports.CuoralMessageType.RECORDING_ERROR,
103
+ payload: { error: 'Not recording' },
104
+ });
92
105
  return null;
93
106
  }
94
- const result = await CuoralPlugin.stopRecording();
107
+ const result = await CuoralPlugin$1.stopRecording();
95
108
  if (result.success) {
96
109
  this.isRecording = false;
97
110
  const duration = this.recordingStartTime
@@ -110,6 +123,11 @@ class CuoralRecorder {
110
123
  duration: result.duration || duration,
111
124
  };
112
125
  }
126
+ // If result.success is false, send error to widget
127
+ this.postMessage({
128
+ type: exports.CuoralMessageType.RECORDING_ERROR,
129
+ payload: { error: 'Failed to stop recording' },
130
+ });
113
131
  return null;
114
132
  }
115
133
  catch (error) {
@@ -126,7 +144,7 @@ class CuoralRecorder {
126
144
  */
127
145
  async takeScreenshot(options) {
128
146
  try {
129
- const screenshot = await CuoralPlugin.takeScreenshot(options);
147
+ const screenshot = await CuoralPlugin$1.takeScreenshot(options);
130
148
  // Post message to widget
131
149
  this.postMessage({
132
150
  type: exports.CuoralMessageType.SCREENSHOT_TAKEN,
@@ -147,7 +165,7 @@ class CuoralRecorder {
147
165
  * Get current recording state
148
166
  */
149
167
  async getState() {
150
- return await CuoralPlugin.getRecordingState();
168
+ return await CuoralPlugin$1.getRecordingState();
151
169
  }
152
170
  /**
153
171
  * Post message to WebView
@@ -539,11 +557,522 @@ class CuoralModal {
539
557
  }
540
558
  }
541
559
 
560
+ // Register the Cuoral plugin for native error capture
561
+ const CuoralPlugin = core.registerPlugin('CuoralPlugin');
562
+ class CuoralIntelligence {
563
+ constructor(sessionId) {
564
+ this.config = {
565
+ consoleErrorBackendUrl: 'https://api.cuoral.com/customer-intelligence/console-error',
566
+ pageViewBackendUrl: 'https://api.cuoral.com/customer-intelligence/page-view',
567
+ apiResponseBackendUrl: 'https://api.cuoral.com/customer-intelligence/api-response',
568
+ batchSize: 10,
569
+ batchInterval: 2000,
570
+ maxQueueSize: 30,
571
+ retryAttempts: 1,
572
+ retryDelay: 2000,
573
+ };
574
+ this.queues = {
575
+ console_error: [],
576
+ unhandled_error: [],
577
+ page_view: [],
578
+ api_call: [],
579
+ };
580
+ this.batchTimers = {};
581
+ this.sessionId = null;
582
+ this.isInitialized = false;
583
+ this.lastPageViewTimestamp = Date.now();
584
+ this.lastPageViewScreen = '';
585
+ this.pendingEvents = [];
586
+ // Network monitoring state
587
+ this.originalFetch = null;
588
+ this.originalXMLHttpRequest = null;
589
+ this.sessionId = sessionId;
590
+ }
591
+ /**
592
+ * Initialize intelligence tracking
593
+ */
594
+ init() {
595
+ if (this.isInitialized) {
596
+ console.warn('[Cuoral Intelligence] Already initialized');
597
+ return;
598
+ }
599
+ this.setupConsoleErrorListener();
600
+ this.setupNetworkMonitoring();
601
+ this.setupAppStateListener();
602
+ this.setupNativeErrorCapture();
603
+ this.isInitialized = true;
604
+ this.flushPendingEvents();
605
+ }
606
+ /**
607
+ * Track a page/screen view
608
+ */
609
+ trackPageView(screen, metadata) {
610
+ const duration = Date.now() - this.lastPageViewTimestamp;
611
+ this.enqueueEvent('page_view', {
612
+ screen,
613
+ referrer: this.lastPageViewScreen,
614
+ duration,
615
+ metadata: metadata || {},
616
+ });
617
+ this.lastPageViewTimestamp = Date.now();
618
+ this.lastPageViewScreen = screen;
619
+ }
620
+ /**
621
+ * Track a console error manually
622
+ */
623
+ trackError(message, stackTrace, metadata) {
624
+ this.enqueueEvent('console_error', {
625
+ message,
626
+ stack_trace: stackTrace || 'No stack trace available',
627
+ log_level: 'error',
628
+ metadata: metadata || {},
629
+ });
630
+ }
631
+ /**
632
+ * Flush all queued events immediately
633
+ */
634
+ flush() {
635
+ Object.keys(this.queues).forEach((type) => {
636
+ this.flushQueue(type);
637
+ });
638
+ }
639
+ /**
640
+ * Destroy and cleanup
641
+ */
642
+ destroy() {
643
+ // Clear all timers
644
+ Object.keys(this.batchTimers).forEach((type) => {
645
+ if (this.batchTimers[type]) {
646
+ clearTimeout(this.batchTimers[type]);
647
+ delete this.batchTimers[type];
648
+ }
649
+ });
650
+ // Restore original functions
651
+ if (this.originalFetch) {
652
+ window.fetch = this.originalFetch;
653
+ this.originalFetch = null;
654
+ }
655
+ if (this.originalXMLHttpRequest) {
656
+ window.XMLHttpRequest = this.originalXMLHttpRequest;
657
+ this.originalXMLHttpRequest = null;
658
+ }
659
+ // Clear queues
660
+ Object.keys(this.queues).forEach((type) => {
661
+ this.queues[type] = [];
662
+ });
663
+ this.isInitialized = false;
664
+ }
665
+ /**
666
+ * Setup console error listener
667
+ */
668
+ setupConsoleErrorListener() {
669
+ // Override console.error
670
+ const originalConsoleError = console.error;
671
+ console.error = (...args) => {
672
+ originalConsoleError.apply(console, args);
673
+ try {
674
+ // Try to find an Error object in arguments to get real stack trace
675
+ let stack = '';
676
+ let message = '';
677
+ // Check each argument for error information
678
+ for (const arg of args) {
679
+ if (arg instanceof Error) {
680
+ stack = arg.stack || '';
681
+ message = arg.message;
682
+ break;
683
+ }
684
+ else if (arg && typeof arg === 'object') {
685
+ if (arg.stack && typeof arg.stack === 'string') {
686
+ stack = arg.stack;
687
+ message = arg.message || arg.toString();
688
+ break;
689
+ }
690
+ if (arg.rejection && arg.rejection instanceof Error) {
691
+ stack = arg.rejection.stack || '';
692
+ message = arg.rejection.message;
693
+ break;
694
+ }
695
+ if (arg.error instanceof Error) {
696
+ stack = arg.error.stack || '';
697
+ message = arg.error.message;
698
+ break;
699
+ }
700
+ }
701
+ }
702
+ // If no stack found, generate one from current context
703
+ // Note: Angular's ErrorHandler doesn't pass Error objects, so this will
704
+ // capture the console.error call stack, not the original error stack.
705
+ // For better error tracking, use source maps or test with unminified builds.
706
+ if (!stack) {
707
+ const error = new Error();
708
+ stack = error.stack || 'No stack trace available';
709
+ message = args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ');
710
+ }
711
+ this.enqueueEvent('console_error', {
712
+ message: message || 'Console error',
713
+ stack_trace: stack,
714
+ log_level: 'error',
715
+ metadata: {},
716
+ });
717
+ }
718
+ catch (err) {
719
+ // Silently fail
720
+ }
721
+ };
722
+ // Global error handler
723
+ window.onerror = (message, source, lineno, colno, error) => {
724
+ try {
725
+ this.enqueueEvent('unhandled_error', {
726
+ message: String(message),
727
+ source: source || 'unknown',
728
+ lineno: lineno || 0,
729
+ colno: colno || 0,
730
+ stack_trace: error?.stack || 'No stack trace available',
731
+ log_level: 'error',
732
+ });
733
+ }
734
+ catch (err) {
735
+ // Silently fail
736
+ }
737
+ return false;
738
+ };
739
+ // Unhandled promise rejection
740
+ window.addEventListener('unhandledrejection', (event) => {
741
+ this.enqueueEvent('unhandled_error', {
742
+ message: event.reason instanceof Error ? event.reason.message : String(event.reason),
743
+ stack_trace: event.reason?.stack || 'No stack trace available',
744
+ log_level: 'error',
745
+ });
746
+ });
747
+ }
748
+ /**
749
+ * Setup network monitoring
750
+ */
751
+ setupNetworkMonitoring() {
752
+ // Store original fetch
753
+ if (!this.originalFetch) {
754
+ this.originalFetch = window.fetch;
755
+ }
756
+ // Override fetch
757
+ window.fetch = async (...args) => {
758
+ const url = args[0] instanceof Request ? args[0].url : args[0];
759
+ const method = args[0] instanceof Request && args[0].method
760
+ ? args[0].method
761
+ : args[1]?.method || 'GET';
762
+ const startTime = Date.now();
763
+ try {
764
+ const response = await this.originalFetch.apply(window, args);
765
+ const duration = Date.now() - startTime;
766
+ const status = response.status;
767
+ // Only track errors (4xx, 5xx)
768
+ if (status >= 400) {
769
+ const responseClone = response.clone();
770
+ try {
771
+ const text = await responseClone.text();
772
+ let responseData = null;
773
+ try {
774
+ const truncated = text.length > 5000 ? text.substring(0, 5000) + '...' : text;
775
+ responseData = JSON.parse(truncated);
776
+ }
777
+ catch (e) {
778
+ responseData = text.length > 5000 ? text.substring(0, 5000) + '...' : text;
779
+ }
780
+ this.enqueueEvent('api_call', {
781
+ method,
782
+ url,
783
+ status_code: status,
784
+ response_data: responseData,
785
+ duration,
786
+ });
787
+ }
788
+ catch (err) {
789
+ this.enqueueEvent('api_call', {
790
+ method,
791
+ url,
792
+ status_code: status,
793
+ duration,
794
+ error: 'Failed to read response body',
795
+ });
796
+ }
797
+ }
798
+ return response;
799
+ }
800
+ catch (error) {
801
+ const duration = Date.now() - startTime;
802
+ this.enqueueEvent('api_call', {
803
+ method,
804
+ url,
805
+ status_code: 0,
806
+ response_data: error.message || 'Network Error',
807
+ duration,
808
+ error: error.message || 'Network Error',
809
+ });
810
+ throw error;
811
+ }
812
+ };
813
+ // Store original XMLHttpRequest
814
+ if (!this.originalXMLHttpRequest && window.XMLHttpRequest) {
815
+ this.originalXMLHttpRequest = window.XMLHttpRequest;
816
+ }
817
+ // Override XMLHttpRequest
818
+ const originalOpen = XMLHttpRequest.prototype.open;
819
+ const originalSend = XMLHttpRequest.prototype.send;
820
+ XMLHttpRequest.prototype.open = function (method, url) {
821
+ this._cuoralMethod = method;
822
+ this._cuoralUrl = url;
823
+ return originalOpen.apply(this, arguments);
824
+ };
825
+ XMLHttpRequest.prototype.send = function () {
826
+ const startTime = Date.now();
827
+ const xhr = this;
828
+ const loadendHandler = () => {
829
+ try {
830
+ const status = xhr.status;
831
+ const duration = Date.now() - startTime;
832
+ // Only track errors (4xx, 5xx)
833
+ if (status >= 400) {
834
+ let responseData = null;
835
+ try {
836
+ const responseText = xhr.responseText || '';
837
+ const truncated = responseText.length > 5000
838
+ ? responseText.substring(0, 5000) + '...'
839
+ : responseText;
840
+ responseData = truncated ? JSON.parse(truncated) : truncated;
841
+ }
842
+ catch (e) {
843
+ const responseText = xhr.responseText || '';
844
+ responseData = responseText.length > 5000
845
+ ? responseText.substring(0, 5000) + '...'
846
+ : responseText;
847
+ }
848
+ window._cuoralIntelligence?.enqueueEvent('api_call', {
849
+ method: xhr._cuoralMethod,
850
+ url: xhr._cuoralUrl,
851
+ status_code: status,
852
+ response_data: responseData,
853
+ duration,
854
+ });
855
+ }
856
+ }
857
+ catch (err) {
858
+ // Silently fail
859
+ }
860
+ };
861
+ this.addEventListener('loadend', loadendHandler, { once: true });
862
+ return originalSend.apply(this, arguments);
863
+ };
864
+ // Store reference for XMLHttpRequest monitoring
865
+ window._cuoralIntelligence = this;
866
+ }
867
+ /**
868
+ * Setup app state listener
869
+ */
870
+ setupAppStateListener() {
871
+ // Listen for app going to background
872
+ window.addEventListener('visibilitychange', () => {
873
+ if (document.visibilityState === 'hidden') {
874
+ this.flush();
875
+ }
876
+ });
877
+ // Listen for app pause (Capacitor)
878
+ if (core.Capacitor.isNativePlatform()) {
879
+ window.addEventListener('pause', () => {
880
+ this.flush();
881
+ });
882
+ }
883
+ }
884
+ /**
885
+ * Enqueue an event
886
+ */
887
+ enqueueEvent(type, data) {
888
+ const queue = this.queues[type];
889
+ if (!queue) {
890
+ return;
891
+ }
892
+ // Don't track successful API calls (2xx, 3xx)
893
+ if (type === 'api_call' && data.status_code >= 200 && data.status_code < 400) {
894
+ return;
895
+ }
896
+ // Limit queue size
897
+ if (queue.length >= this.config.maxQueueSize) {
898
+ queue.shift();
899
+ }
900
+ const event = {
901
+ event_type: type,
902
+ timestamp: Date.now(),
903
+ session_id: this.sessionId || '',
904
+ user_agent: navigator.userAgent,
905
+ url: window.location.href,
906
+ data,
907
+ };
908
+ queue.push(event);
909
+ // Flush immediately for critical errors
910
+ const shouldFlushImmediately = type === 'api_call' && (data.status_code === 0 || data.status_code >= 400);
911
+ if (shouldFlushImmediately || queue.length >= this.config.batchSize) {
912
+ this.flushQueue(type);
913
+ }
914
+ else if (!this.batchTimers[type]) {
915
+ this.batchTimers[type] = setTimeout(() => this.flushQueue(type), this.config.batchInterval);
916
+ }
917
+ }
918
+ /**
919
+ * Flush a specific queue
920
+ */
921
+ flushQueue(eventType, useBeacon = false) {
922
+ const queue = this.queues[eventType];
923
+ if (!queue || queue.length === 0) {
924
+ return;
925
+ }
926
+ // Clear timer
927
+ if (this.batchTimers[eventType]) {
928
+ clearTimeout(this.batchTimers[eventType]);
929
+ delete this.batchTimers[eventType];
930
+ }
931
+ const eventsToProcess = [...queue];
932
+ this.queues[eventType] = [];
933
+ let backendUrl;
934
+ let transformedPayload;
935
+ if (eventType === 'console_error' || eventType === 'unhandled_error') {
936
+ backendUrl = this.config.consoleErrorBackendUrl;
937
+ transformedPayload = eventsToProcess.map((event) => ({
938
+ message: event.data.message,
939
+ stack_trace: event.data.stack_trace,
940
+ log_level: 'error',
941
+ url: event.url,
942
+ session_id: event.session_id,
943
+ source: 'mobile',
944
+ console_metadata: event.data.metadata || {},
945
+ }));
946
+ }
947
+ else if (eventType === 'page_view') {
948
+ backendUrl = this.config.pageViewBackendUrl;
949
+ transformedPayload = eventsToProcess.map((event) => ({
950
+ session_id: event.session_id,
951
+ url: event.data.screen,
952
+ referrer: event.data.referrer,
953
+ user_agent: event.user_agent,
954
+ source: 'mobile',
955
+ page_metadata: {
956
+ duration: event.data.duration,
957
+ screen: event.data.screen,
958
+ ...event.data.metadata,
959
+ },
960
+ timestamp: new Date(event.timestamp).toISOString(),
961
+ }));
962
+ }
963
+ else if (eventType === 'api_call') {
964
+ backendUrl = this.config.apiResponseBackendUrl;
965
+ transformedPayload = eventsToProcess
966
+ .map((event) => {
967
+ let responseBody = event.data.response_data;
968
+ if (typeof responseBody === 'string') {
969
+ try {
970
+ responseBody = JSON.parse(responseBody);
971
+ }
972
+ catch (e) {
973
+ responseBody = { raw_text: responseBody };
974
+ }
975
+ }
976
+ else if (responseBody === null || typeof responseBody === 'undefined') {
977
+ responseBody = {};
978
+ }
979
+ return {
980
+ url: event.data.url,
981
+ method: event.data.method,
982
+ status_code: event.data.status_code,
983
+ response_body: responseBody,
984
+ session_id: event.session_id,
985
+ source: 'mobile',
986
+ api_metadata: {
987
+ duration: event.data.duration,
988
+ error: event.data.error || null,
989
+ },
990
+ };
991
+ })
992
+ .filter((item) => item !== null && item !== undefined);
993
+ }
994
+ else {
995
+ return;
996
+ }
997
+ const filteredPayload = transformedPayload.filter((item) => item !== null && item !== undefined);
998
+ if (backendUrl && filteredPayload.length > 0) {
999
+ this.sendToBackend(backendUrl, filteredPayload, useBeacon);
1000
+ }
1001
+ }
1002
+ /**
1003
+ * Send data to backend
1004
+ */
1005
+ async sendToBackend(url, payload, useBeacon = false, retryCount = 0) {
1006
+ if (!this.sessionId) {
1007
+ this.pendingEvents.push({ url, payload, useBeacon });
1008
+ return;
1009
+ }
1010
+ if (!navigator.onLine) {
1011
+ return;
1012
+ }
1013
+ const data = JSON.stringify(payload);
1014
+ const headers = {
1015
+ 'Content-Type': 'application/json',
1016
+ };
1017
+ try {
1018
+ const response = await fetch(url, {
1019
+ method: 'POST',
1020
+ headers,
1021
+ body: data,
1022
+ keepalive: useBeacon,
1023
+ });
1024
+ if (!response.ok && retryCount < this.config.retryAttempts) {
1025
+ setTimeout(() => {
1026
+ this.sendToBackend(url, payload, false, retryCount + 1);
1027
+ }, this.config.retryDelay);
1028
+ }
1029
+ }
1030
+ catch (error) {
1031
+ // Silently fail
1032
+ }
1033
+ }
1034
+ /**
1035
+ * Flush pending events
1036
+ */
1037
+ flushPendingEvents() {
1038
+ if (this.pendingEvents.length > 0) {
1039
+ const pending = [...this.pendingEvents];
1040
+ this.pendingEvents = [];
1041
+ pending.forEach(({ url, payload, useBeacon }) => {
1042
+ this.sendToBackend(url, payload, useBeacon);
1043
+ });
1044
+ }
1045
+ }
1046
+ /**
1047
+ * Setup native error capture for Android/iOS crashes
1048
+ */
1049
+ setupNativeErrorCapture() {
1050
+ try {
1051
+ // Only run on native platforms and if we have a session ID
1052
+ if (core.Capacitor.isNativePlatform() && this.sessionId) {
1053
+ CuoralPlugin.setupNativeErrorCapture({
1054
+ backendUrl: this.config.consoleErrorBackendUrl,
1055
+ sessionId: this.sessionId,
1056
+ }).catch(() => {
1057
+ // Silently fail - native error capture optional
1058
+ });
1059
+ }
1060
+ }
1061
+ catch (error) {
1062
+ // Silently fail if plugin is not available
1063
+ }
1064
+ }
1065
+ }
1066
+
542
1067
  /**
543
1068
  * Main Cuoral class - simple API for users
544
1069
  */
545
1070
  class Cuoral {
546
1071
  constructor(options) {
1072
+ // Check if running on a mobile platform
1073
+ if (!core.Capacitor.isNativePlatform()) {
1074
+ throw new Error('Cuoral Ionic library only works on native mobile platforms (iOS/Android). Web is not supported.');
1075
+ }
547
1076
  this.options = {
548
1077
  showFloatingButton: true,
549
1078
  useModal: true,
@@ -579,12 +1108,103 @@ class Cuoral {
579
1108
  /**
580
1109
  * Initialize Cuoral
581
1110
  */
582
- initialize() {
1111
+ async initialize() {
583
1112
  this.bridge.initialize();
584
1113
  // Initialize modal if enabled
585
1114
  if (this.modal) {
586
1115
  this.modal.initialize();
587
1116
  }
1117
+ // Fetch session configuration and initialize intelligence if enabled by backend
1118
+ await this.initializeIntelligence();
1119
+ }
1120
+ /**
1121
+ * Initialize intelligence based on backend configuration
1122
+ */
1123
+ async initializeIntelligence() {
1124
+ try {
1125
+ let sessionId = localStorage.getItem('__x_loadID');
1126
+ // If no session exists, initiate a new one
1127
+ if (!sessionId) {
1128
+ sessionId = await this.initiateSession();
1129
+ if (!sessionId) {
1130
+ console.warn('[Cuoral] Failed to create session, intelligence not initialized');
1131
+ return;
1132
+ }
1133
+ }
1134
+ // Fetch session configuration
1135
+ const config = await this.fetchSessionConfiguration(sessionId);
1136
+ // If session was invalid/expired, try to initiate a new one
1137
+ if (!config && !localStorage.getItem('__x_loadID')) {
1138
+ sessionId = await this.initiateSession();
1139
+ if (sessionId) {
1140
+ const newConfig = await this.fetchSessionConfiguration(sessionId);
1141
+ if (newConfig && newConfig.customer_intelligence === true) {
1142
+ this.intelligence = new CuoralIntelligence(sessionId);
1143
+ this.intelligence.init();
1144
+ }
1145
+ }
1146
+ return;
1147
+ }
1148
+ // Only initialize intelligence if customer_intelligence is enabled in backend
1149
+ if (config && config.customer_intelligence === true) {
1150
+ this.intelligence = new CuoralIntelligence(sessionId);
1151
+ this.intelligence.init();
1152
+ }
1153
+ }
1154
+ catch (error) {
1155
+ console.warn('[Cuoral] Failed to initialize intelligence:', error);
1156
+ // Silently fail - don't break the app if intelligence fails to initialize
1157
+ }
1158
+ }
1159
+ /**
1160
+ * Fetch session configuration from backend
1161
+ */
1162
+ async fetchSessionConfiguration(sessionId) {
1163
+ try {
1164
+ const response = await fetch('https://api.cuoral.com/conversation/session/get', {
1165
+ method: 'POST',
1166
+ headers: {
1167
+ 'Content-Type': 'application/json',
1168
+ 'x-org-id': this.options.publicKey,
1169
+ },
1170
+ body: JSON.stringify({ session_id: sessionId }),
1171
+ });
1172
+ if (response.status === 404 || response.status === 400) {
1173
+ // Session not found, clear and return null
1174
+ localStorage.removeItem('__x_loadID');
1175
+ return null;
1176
+ }
1177
+ if (!response.ok) {
1178
+ return null;
1179
+ }
1180
+ const data = await response.json();
1181
+ // If session is expired, clear it
1182
+ if (data.is_expired) {
1183
+ localStorage.removeItem('__x_loadID');
1184
+ return null;
1185
+ }
1186
+ return data.configuration || null;
1187
+ }
1188
+ catch (error) {
1189
+ console.warn('[Cuoral] Failed to fetch session configuration:', error);
1190
+ return null;
1191
+ }
1192
+ }
1193
+ /**
1194
+ * Track a page/screen view
1195
+ */
1196
+ trackPageView(screen, metadata) {
1197
+ if (this.intelligence) {
1198
+ this.intelligence.trackPageView(screen, metadata);
1199
+ }
1200
+ }
1201
+ /**
1202
+ * Track an error manually
1203
+ */
1204
+ trackError(message, stackTrace, metadata) {
1205
+ if (this.intelligence) {
1206
+ this.intelligence.trackError(message, stackTrace, metadata);
1207
+ }
588
1208
  }
589
1209
  /**
590
1210
  * Open the widget modal
@@ -635,6 +1255,77 @@ class Cuoral {
635
1255
  if (this.modal) {
636
1256
  this.modal.destroy();
637
1257
  }
1258
+ // Destroy intelligence if exists
1259
+ if (this.intelligence) {
1260
+ this.intelligence.destroy();
1261
+ }
1262
+ }
1263
+ /**
1264
+ * Get or create session ID
1265
+ */
1266
+ getOrCreateSessionId() {
1267
+ const storageKey = '__x_loadID';
1268
+ let sessionId = localStorage.getItem(storageKey);
1269
+ if (!sessionId) {
1270
+ // Session will be created by initiate-session API call
1271
+ // For now, return a temporary ID that will be replaced
1272
+ sessionId = `temp-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
1273
+ }
1274
+ return sessionId;
1275
+ }
1276
+ /**
1277
+ * Initiate a new session with backend (like inline.js does)
1278
+ */
1279
+ async initiateSession() {
1280
+ try {
1281
+ const response = await fetch('https://api.cuoral.com/conversation/initiate-session', {
1282
+ method: 'POST',
1283
+ headers: {
1284
+ 'Content-Type': 'application/json',
1285
+ 'x-org-id': this.options.publicKey,
1286
+ },
1287
+ body: JSON.stringify({ public_key: this.options.publicKey }),
1288
+ });
1289
+ if (!response.ok) {
1290
+ return null;
1291
+ }
1292
+ const data = await response.json();
1293
+ if (data.status && data.session_id) {
1294
+ localStorage.setItem('__x_loadID', data.session_id);
1295
+ // Set profile if email, firstName, lastName are provided
1296
+ if (this.options.email && this.options.firstName && this.options.lastName) {
1297
+ await this.setProfile(data.session_id);
1298
+ }
1299
+ return data.session_id;
1300
+ }
1301
+ return null;
1302
+ }
1303
+ catch (error) {
1304
+ console.warn('[Cuoral] Failed to initiate session:', error);
1305
+ return null;
1306
+ }
1307
+ }
1308
+ /**
1309
+ * Set user profile for the session
1310
+ */
1311
+ async setProfile(sessionId) {
1312
+ try {
1313
+ await fetch('https://api.cuoral.com/conversation/set-profile', {
1314
+ method: 'POST',
1315
+ headers: {
1316
+ 'Content-Type': 'application/json',
1317
+ 'x-org-id': this.options.publicKey,
1318
+ },
1319
+ body: JSON.stringify({
1320
+ session_id: sessionId,
1321
+ email: this.options.email,
1322
+ name: `${this.options.firstName} ${this.options.lastName}`,
1323
+ }),
1324
+ });
1325
+ }
1326
+ catch (error) {
1327
+ console.warn('[Cuoral] Failed to set profile:', error);
1328
+ }
638
1329
  }
639
1330
  /**
640
1331
  * Setup automatic message handlers
@@ -705,121 +1396,10 @@ class Cuoral {
705
1396
  Cuoral.PRODUCTION_WIDGET_URL = 'https://js.cuoral.com/mobile.html';
706
1397
  Cuoral.DEV_WIDGET_URL = 'assets/mobile.html';
707
1398
 
708
- /**
709
- * Web implementation (for testing in browser)
710
- */
711
- class CuoralPluginWeb extends core.WebPlugin {
712
- constructor() {
713
- super(...arguments);
714
- this.isRecording = false;
715
- }
716
- async startRecording(options) {
717
- console.log('[Cuoral Web] Start recording', options);
718
- // Check if Screen Capture API is available
719
- if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
720
- throw new Error('Screen recording is not supported in this browser');
721
- }
722
- try {
723
- // Request screen capture
724
- const stream = await navigator.mediaDevices.getDisplayMedia({
725
- video: true,
726
- audio: options?.includeAudio || false,
727
- });
728
- this.isRecording = true;
729
- this.recordingStartTime = Date.now();
730
- // Store stream for later stopping
731
- window.__cuoralRecordingStream = stream;
732
- return { success: true };
733
- }
734
- catch (error) {
735
- console.error('[Cuoral Web] Failed to start recording:', error);
736
- return { success: false };
737
- }
738
- }
739
- async stopRecording() {
740
- console.log('[Cuoral Web] Stop recording');
741
- if (!this.isRecording) {
742
- return { success: false };
743
- }
744
- const stream = window.__cuoralRecordingStream;
745
- if (stream) {
746
- stream.getTracks().forEach(track => track.stop());
747
- delete window.__cuoralRecordingStream;
748
- }
749
- const duration = this.recordingStartTime
750
- ? Math.floor((Date.now() - this.recordingStartTime) / 1000)
751
- : 0;
752
- this.isRecording = false;
753
- this.recordingStartTime = undefined;
754
- return {
755
- success: true,
756
- duration,
757
- filePath: 'web-recording-not-saved',
758
- };
759
- }
760
- async getRecordingState() {
761
- return {
762
- isRecording: this.isRecording,
763
- duration: this.recordingStartTime
764
- ? Math.floor((Date.now() - this.recordingStartTime) / 1000)
765
- : undefined,
766
- };
767
- }
768
- async takeScreenshot(options) {
769
- console.log('[Cuoral Web] Take screenshot', options);
770
- try {
771
- // Use Screen Capture API
772
- const stream = await navigator.mediaDevices.getDisplayMedia({
773
- video: { width: 1920, height: 1080 },
774
- });
775
- // Capture frame from video stream
776
- const video = document.createElement('video');
777
- video.srcObject = stream;
778
- video.play();
779
- await new Promise(resolve => {
780
- video.onloadedmetadata = resolve;
781
- });
782
- const canvas = document.createElement('canvas');
783
- canvas.width = video.videoWidth;
784
- canvas.height = video.videoHeight;
785
- const ctx = canvas.getContext('2d');
786
- ctx?.drawImage(video, 0, 0);
787
- // Stop stream
788
- stream.getTracks().forEach(track => track.stop());
789
- // Convert to base64
790
- const format = options?.format || 'png';
791
- const quality = options?.quality || 0.92;
792
- const base64 = canvas.toDataURL(`image/${format}`, quality);
793
- return {
794
- base64: base64.split(',')[1],
795
- mimeType: `image/${format}`,
796
- width: canvas.width,
797
- height: canvas.height,
798
- };
799
- }
800
- catch (error) {
801
- console.error('[Cuoral Web] Failed to take screenshot:', error);
802
- throw error;
803
- }
804
- }
805
- async isRecordingSupported() {
806
- const supported = !!(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia);
807
- return { supported };
808
- }
809
- async requestPermissions() {
810
- // Web doesn't require explicit permissions - they're requested on demand
811
- return { granted: true };
812
- }
813
- }
814
-
815
- var web = /*#__PURE__*/Object.freeze({
816
- __proto__: null,
817
- CuoralPluginWeb: CuoralPluginWeb
818
- });
819
-
820
1399
  exports.Cuoral = Cuoral;
821
1400
  exports.CuoralBridge = CuoralBridge;
1401
+ exports.CuoralIntelligence = CuoralIntelligence;
822
1402
  exports.CuoralModal = CuoralModal;
823
- exports.CuoralPlugin = CuoralPlugin;
1403
+ exports.CuoralPlugin = CuoralPlugin$1;
824
1404
  exports.CuoralRecorder = CuoralRecorder;
825
1405
  //# sourceMappingURL=index.js.map