cuoral-ionic 0.0.1 → 0.0.3

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