cuoral-ionic 0.0.5 → 0.0.7

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 (59) hide show
  1. package/README.md +72 -1
  2. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  3. package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
  4. package/android/build/.transforms/bb54161301273cf9b5b94a21c0fb3f23/transformed/classes/classes_dex/classes.dex +0 -0
  5. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$1.dex +0 -0
  6. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$2.dex +0 -0
  7. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$3.dex +0 -0
  8. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$4.dex +0 -0
  9. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$5.dex +0 -0
  10. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$6.dex +0 -0
  11. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$InitiateCallback.dex +0 -0
  12. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$UploadCallback.dex +0 -0
  13. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin.dex +0 -0
  14. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/desugar_graph.bin +0 -0
  15. package/android/build/intermediates/compile_library_classes_jar/debug/classes.jar +0 -0
  16. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$1.class +0 -0
  17. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$2.class +0 -0
  18. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$3.class +0 -0
  19. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$4.class +0 -0
  20. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$5.class +0 -0
  21. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$6.class +0 -0
  22. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$InitiateCallback.class +0 -0
  23. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$UploadCallback.class +0 -0
  24. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin.class +0 -0
  25. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$1.class +0 -0
  26. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$2.class +0 -0
  27. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$3.class +0 -0
  28. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$4.class +0 -0
  29. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$5.class +0 -0
  30. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$6.class +0 -0
  31. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$InitiateCallback.class +0 -0
  32. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$UploadCallback.class +0 -0
  33. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin.class +0 -0
  34. package/android/build/intermediates/runtime_library_classes_jar/debug/classes.jar +0 -0
  35. package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
  36. package/android/build.gradle +1 -0
  37. package/android/src/main/java/com/cuoral/ionic/CuoralPlugin.java +205 -5
  38. package/dist/cuoral.d.ts +27 -0
  39. package/dist/cuoral.d.ts.map +1 -1
  40. package/dist/cuoral.js +137 -7
  41. package/dist/index.esm.js +471 -17
  42. package/dist/index.esm.js.map +1 -1
  43. package/dist/index.js +490 -17
  44. package/dist/index.js.map +1 -1
  45. package/dist/intelligence.d.ts +51 -0
  46. package/dist/intelligence.d.ts.map +1 -1
  47. package/dist/intelligence.js +307 -0
  48. package/dist/plugin.d.ts +15 -2
  49. package/dist/plugin.d.ts.map +1 -1
  50. package/dist/plugin.js +23 -10
  51. package/dist/types.d.ts +4 -0
  52. package/dist/types.d.ts.map +1 -1
  53. package/dist/types.js +4 -0
  54. package/ios/Plugin/CuoralPlugin.swift +249 -13
  55. package/package.json +4 -2
  56. package/src/cuoral.ts +151 -8
  57. package/src/intelligence.ts +375 -0
  58. package/src/plugin.ts +39 -11
  59. package/src/types.ts +4 -0
package/dist/index.esm.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { registerPlugin, Capacitor } from '@capacitor/core';
2
+ import * as rrweb from 'rrweb';
2
3
 
3
4
  /**
4
5
  * Message types for communication between WebView and Native code
@@ -10,6 +11,7 @@ var CuoralMessageType;
10
11
  CuoralMessageType["STOP_RECORDING"] = "CUORAL_STOP_RECORDING";
11
12
  CuoralMessageType["RECORDING_STARTED"] = "CUORAL_RECORDING_STARTED";
12
13
  CuoralMessageType["RECORDING_STOPPED"] = "CUORAL_RECORDING_STOPPED";
14
+ CuoralMessageType["RECORDING_UPLOADED"] = "CUORAL_RECORDING_UPLOADED";
13
15
  CuoralMessageType["RECORDING_ERROR"] = "CUORAL_RECORDING_ERROR";
14
16
  // Screenshot
15
17
  CuoralMessageType["TAKE_SCREENSHOT"] = "CUORAL_TAKE_SCREENSHOT";
@@ -18,6 +20,9 @@ var CuoralMessageType;
18
20
  // Widget Communication
19
21
  CuoralMessageType["WIDGET_READY"] = "CUORAL_WIDGET_READY";
20
22
  CuoralMessageType["WIDGET_CLOSED"] = "CUORAL_WIDGET_CLOSED";
23
+ CuoralMessageType["SESSION_UPDATED"] = "CUORAL_SESSION_UPDATED";
24
+ CuoralMessageType["REQUEST_SESSION_ID"] = "CUORAL_REQUEST_SESSION_ID";
25
+ CuoralMessageType["SESSION_ID_RESPONSE"] = "CUORAL_SESSION_ID_RESPONSE";
21
26
  // File Upload
22
27
  CuoralMessageType["UPLOAD_FILE"] = "CUORAL_UPLOAD_FILE";
23
28
  CuoralMessageType["UPLOAD_PROGRESS"] = "CUORAL_UPLOAD_PROGRESS";
@@ -92,7 +97,7 @@ class CuoralRecorder {
92
97
  /**
93
98
  * Stop recording
94
99
  */
95
- async stopRecording() {
100
+ async stopRecording(options) {
96
101
  try {
97
102
  if (!this.isRecording) {
98
103
  // Send error message to widget so it can exit "stopping" state
@@ -102,23 +107,36 @@ class CuoralRecorder {
102
107
  });
103
108
  return null;
104
109
  }
105
- const result = await CuoralPlugin$1.stopRecording();
110
+ const result = await CuoralPlugin$1.stopRecording(options);
106
111
  if (result.success) {
107
112
  this.isRecording = false;
108
113
  const duration = this.recordingStartTime
109
114
  ? Math.floor((Date.now() - this.recordingStartTime) / 1000)
110
115
  : 0;
111
- // Post message to widget
112
- this.postMessage({
113
- type: CuoralMessageType.RECORDING_STOPPED,
114
- payload: {
115
- filePath: result.filePath,
116
- duration: result.duration || duration,
117
- },
118
- });
116
+ // If uploaded, notify widget differently
117
+ if (result.uploaded) {
118
+ this.postMessage({
119
+ type: CuoralMessageType.RECORDING_UPLOADED,
120
+ payload: {
121
+ duration: result.duration || duration,
122
+ uploaded: true
123
+ },
124
+ });
125
+ }
126
+ else {
127
+ // Post message to widget (old behavior)
128
+ this.postMessage({
129
+ type: CuoralMessageType.RECORDING_STOPPED,
130
+ payload: {
131
+ filePath: result.filePath,
132
+ duration: result.duration || duration,
133
+ },
134
+ });
135
+ }
119
136
  return {
120
137
  filePath: result.filePath,
121
138
  duration: result.duration || duration,
139
+ uploaded: result.uploaded,
122
140
  };
123
141
  }
124
142
  // If result.success is false, send error to widget
@@ -602,11 +620,13 @@ class CuoralIntelligence {
602
620
  consoleErrorBackendUrl: 'https://api.cuoral.com/customer-intelligence/console-error',
603
621
  pageViewBackendUrl: 'https://api.cuoral.com/customer-intelligence/page-view',
604
622
  apiResponseBackendUrl: 'https://api.cuoral.com/customer-intelligence/api-response',
623
+ sessionReplayBackendUrl: 'https://api.cuoral.com/customer-intelligence/session-recording/batch',
605
624
  batchSize: 10,
606
625
  batchInterval: 2000,
607
626
  maxQueueSize: 30,
608
627
  retryAttempts: 1,
609
628
  retryDelay: 2000,
629
+ sessionReplayBatchInterval: 10000, // 10 seconds
610
630
  };
611
631
  this.queues = {
612
632
  console_error: [],
@@ -623,8 +643,34 @@ class CuoralIntelligence {
623
643
  // Network monitoring state
624
644
  this.originalFetch = null;
625
645
  this.originalXMLHttpRequest = null;
646
+ // Session replay state
647
+ this.rrwebStopFn = null;
648
+ this.rrwebEvents = [];
649
+ this.customEvents = [];
650
+ this.sessionReplayTimer = null;
651
+ this.clickTimestamps = new Map();
652
+ this.rageClickThreshold = 5; // 5 rapid clicks
653
+ this.rageClickWindowMs = 2000; // Within 2 seconds
626
654
  this.sessionId = sessionId;
627
655
  }
656
+ /**
657
+ * Update the session ID (e.g., when user logs in)
658
+ */
659
+ updateSessionId(newSessionId) {
660
+ console.log('[Cuoral Intelligence] Updating session ID from', this.sessionId, 'to', newSessionId);
661
+ this.sessionId = newSessionId;
662
+ // Flush any pending events with the old session before switching
663
+ this.flush();
664
+ // Update native error capture with new session ID
665
+ if (Capacitor.isNativePlatform()) {
666
+ CuoralPlugin.setupNativeErrorCapture({
667
+ backendUrl: this.config.consoleErrorBackendUrl,
668
+ sessionId: newSessionId
669
+ }).catch(error => {
670
+ console.warn('[Cuoral Intelligence] Failed to update native error capture:', error);
671
+ });
672
+ }
673
+ }
628
674
  /**
629
675
  * Initialize intelligence tracking
630
676
  */
@@ -637,6 +683,7 @@ class CuoralIntelligence {
637
683
  this.setupNetworkMonitoring();
638
684
  this.setupAppStateListener();
639
685
  this.setupNativeErrorCapture();
686
+ this.setupSessionReplay();
640
687
  this.isInitialized = true;
641
688
  this.flushPendingEvents();
642
689
  }
@@ -684,6 +731,17 @@ class CuoralIntelligence {
684
731
  delete this.batchTimers[type];
685
732
  }
686
733
  });
734
+ // Stop session replay
735
+ if (this.rrwebStopFn) {
736
+ this.rrwebStopFn();
737
+ this.rrwebStopFn = null;
738
+ }
739
+ if (this.sessionReplayTimer) {
740
+ clearInterval(this.sessionReplayTimer);
741
+ this.sessionReplayTimer = null;
742
+ }
743
+ // Flush remaining session replay data
744
+ this.flushSessionReplayBatch();
687
745
  // Restore original functions
688
746
  if (this.originalFetch) {
689
747
  window.fetch = this.originalFetch;
@@ -942,6 +1000,7 @@ class CuoralIntelligence {
942
1000
  url: window.location.href,
943
1001
  data,
944
1002
  };
1003
+ console.log(`[Intelligence] Enqueuing ${type} event with session:`, this.sessionId);
945
1004
  queue.push(event);
946
1005
  // Flush immediately for critical errors
947
1006
  const shouldFlushImmediately = type === 'api_call' && (data.status_code === 0 || data.status_code >= 400);
@@ -1099,6 +1158,271 @@ class CuoralIntelligence {
1099
1158
  // Silently fail if plugin is not available
1100
1159
  }
1101
1160
  }
1161
+ /**
1162
+ * Setup session replay with rrweb
1163
+ */
1164
+ setupSessionReplay() {
1165
+ if (!this.sessionId) {
1166
+ console.warn('[Cuoral Intelligence] Session replay requires a session ID');
1167
+ return;
1168
+ }
1169
+ // Start rrweb recording
1170
+ this.rrwebStopFn = rrweb.record({
1171
+ emit: (event) => {
1172
+ this.rrwebEvents.push(event);
1173
+ },
1174
+ checkoutEveryNms: 60000, // Full snapshot every minute
1175
+ sampling: {
1176
+ scroll: 150, // Throttle scroll events
1177
+ media: 800,
1178
+ input: 'last', // Only record final input value (privacy)
1179
+ },
1180
+ maskAllInputs: true, // Mask sensitive inputs (privacy)
1181
+ blockClass: 'cuoral-block',
1182
+ ignoreClass: 'cuoral-ignore',
1183
+ });
1184
+ // Setup custom event tracking
1185
+ this.setupClickTracking();
1186
+ this.setupScrollTracking();
1187
+ this.setupFormTracking();
1188
+ // Start batch timer (send every ~10 seconds)
1189
+ this.sessionReplayTimer = setInterval(() => {
1190
+ this.flushSessionReplayBatch();
1191
+ }, this.config.sessionReplayBatchInterval);
1192
+ }
1193
+ /**
1194
+ * Setup click tracking (including rage clicks)
1195
+ */
1196
+ setupClickTracking() {
1197
+ document.addEventListener('click', (event) => {
1198
+ const target = event.target;
1199
+ if (!target)
1200
+ return;
1201
+ const selector = this.getElementSelector(target);
1202
+ const elementText = target.textContent?.trim().substring(0, 100) || '';
1203
+ const url = window.location.href;
1204
+ const now = Date.now();
1205
+ // Track regular click
1206
+ this.addCustomEvent({
1207
+ name: 'click',
1208
+ category: 'interaction',
1209
+ url,
1210
+ element_selector: selector,
1211
+ element_text: elementText,
1212
+ event_timestamp: new Date(now).toISOString(),
1213
+ session_id: this.sessionId,
1214
+ properties: this.getMetadata(),
1215
+ });
1216
+ // Track rage click detection
1217
+ const clicks = this.clickTimestamps.get(selector) || [];
1218
+ clicks.push(now);
1219
+ // Remove old clicks outside the time window
1220
+ const recentClicks = clicks.filter(timestamp => now - timestamp < this.rageClickWindowMs);
1221
+ this.clickTimestamps.set(selector, recentClicks);
1222
+ // If 5+ clicks within 2 seconds = rage click
1223
+ if (recentClicks.length >= this.rageClickThreshold) {
1224
+ this.addCustomEvent({
1225
+ name: 'rage_click',
1226
+ category: 'frustration',
1227
+ url,
1228
+ element_selector: selector,
1229
+ element_text: elementText,
1230
+ event_timestamp: new Date(now).toISOString(),
1231
+ session_id: this.sessionId,
1232
+ properties: {
1233
+ ...this.getMetadata(),
1234
+ click_count: recentClicks.length,
1235
+ },
1236
+ });
1237
+ // Clear after detecting rage click
1238
+ this.clickTimestamps.delete(selector);
1239
+ }
1240
+ }, true);
1241
+ }
1242
+ /**
1243
+ * Setup scroll depth tracking
1244
+ */
1245
+ setupScrollTracking() {
1246
+ let scrollDepths = new Set();
1247
+ let ticking = false;
1248
+ const trackScroll = () => {
1249
+ const scrollPercentage = Math.round((window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100);
1250
+ // Track milestones: 25%, 50%, 75%, 100%
1251
+ const milestones = [25, 50, 75, 100];
1252
+ for (const milestone of milestones) {
1253
+ if (scrollPercentage >= milestone && !scrollDepths.has(milestone)) {
1254
+ scrollDepths.add(milestone);
1255
+ this.addCustomEvent({
1256
+ name: 'scroll_depth',
1257
+ category: 'engagement',
1258
+ url: window.location.href,
1259
+ event_timestamp: new Date().toISOString(),
1260
+ session_id: this.sessionId,
1261
+ properties: {
1262
+ ...this.getMetadata(),
1263
+ percentage: milestone,
1264
+ },
1265
+ });
1266
+ }
1267
+ }
1268
+ ticking = false;
1269
+ };
1270
+ window.addEventListener('scroll', () => {
1271
+ if (!ticking) {
1272
+ window.requestAnimationFrame(trackScroll);
1273
+ ticking = true;
1274
+ }
1275
+ });
1276
+ }
1277
+ /**
1278
+ * Setup form tracking
1279
+ */
1280
+ setupFormTracking() {
1281
+ const trackedForms = new WeakSet();
1282
+ // Track form starts
1283
+ document.addEventListener('focusin', (event) => {
1284
+ const target = event.target;
1285
+ if (!target)
1286
+ return;
1287
+ const form = target.closest('form');
1288
+ if (form && !trackedForms.has(form)) {
1289
+ trackedForms.add(form);
1290
+ this.addCustomEvent({
1291
+ name: 'form_started',
1292
+ category: 'form',
1293
+ url: window.location.href,
1294
+ element_selector: this.getElementSelector(form),
1295
+ event_timestamp: new Date().toISOString(),
1296
+ session_id: this.sessionId,
1297
+ properties: this.getMetadata(),
1298
+ });
1299
+ }
1300
+ }, true);
1301
+ // Track form submissions
1302
+ document.addEventListener('submit', (event) => {
1303
+ const form = event.target;
1304
+ if (!form)
1305
+ return;
1306
+ this.addCustomEvent({
1307
+ name: 'form_submitted',
1308
+ category: 'form',
1309
+ url: window.location.href,
1310
+ element_selector: this.getElementSelector(form),
1311
+ event_timestamp: new Date().toISOString(),
1312
+ session_id: this.sessionId,
1313
+ properties: this.getMetadata(),
1314
+ });
1315
+ }, true);
1316
+ // Track form abandonment (on page exit with incomplete forms)
1317
+ window.addEventListener('beforeunload', () => {
1318
+ document.querySelectorAll('form').forEach((form) => {
1319
+ const inputs = form.querySelectorAll('input, textarea, select');
1320
+ const hasValue = Array.from(inputs).some((input) => input.value);
1321
+ if (hasValue && !trackedForms.has(form)) {
1322
+ this.addCustomEvent({
1323
+ name: 'form_abandoned',
1324
+ category: 'form',
1325
+ url: window.location.href,
1326
+ element_selector: this.getElementSelector(form),
1327
+ event_timestamp: new Date().toISOString(),
1328
+ session_id: this.sessionId,
1329
+ properties: this.getMetadata(),
1330
+ });
1331
+ }
1332
+ });
1333
+ });
1334
+ }
1335
+ /**
1336
+ * Track custom business events (flows, features, etc.)
1337
+ */
1338
+ trackCustomEvent(name, category, properties = {}, elementSelector, elementText) {
1339
+ this.addCustomEvent({
1340
+ name,
1341
+ category,
1342
+ url: window.location.href,
1343
+ element_selector: elementSelector,
1344
+ element_text: elementText,
1345
+ event_timestamp: new Date().toISOString(),
1346
+ session_id: this.sessionId,
1347
+ properties: {
1348
+ ...this.getMetadata(),
1349
+ ...properties,
1350
+ },
1351
+ });
1352
+ }
1353
+ /**
1354
+ * Add a custom event to the buffer
1355
+ */
1356
+ addCustomEvent(event) {
1357
+ this.customEvents.push(event);
1358
+ }
1359
+ /**
1360
+ * Flush session replay batch
1361
+ */
1362
+ flushSessionReplayBatch() {
1363
+ if (!this.sessionId)
1364
+ return;
1365
+ const eventsToSend = [...this.rrwebEvents];
1366
+ const customEventsToSend = [...this.customEvents];
1367
+ // Clear buffers
1368
+ this.rrwebEvents = [];
1369
+ this.customEvents = [];
1370
+ // Only send if there's data
1371
+ if (eventsToSend.length === 0 && customEventsToSend.length === 0) {
1372
+ return;
1373
+ }
1374
+ const batch = {
1375
+ session_id: this.sessionId,
1376
+ events: eventsToSend,
1377
+ custom_events: customEventsToSend,
1378
+ };
1379
+ this.sendSessionReplayBatch(batch);
1380
+ }
1381
+ /**
1382
+ * Send session replay batch to backend
1383
+ */
1384
+ async sendSessionReplayBatch(batch) {
1385
+ try {
1386
+ const response = await fetch(this.config.sessionReplayBackendUrl, {
1387
+ method: 'POST',
1388
+ headers: {
1389
+ 'Content-Type': 'application/json',
1390
+ },
1391
+ body: JSON.stringify(batch),
1392
+ });
1393
+ if (!response.ok) {
1394
+ console.warn('[Cuoral Intelligence] Failed to send session replay batch');
1395
+ }
1396
+ }
1397
+ catch (error) {
1398
+ console.warn('[Cuoral Intelligence] Error sending session replay batch:', error);
1399
+ }
1400
+ }
1401
+ /**
1402
+ * Get element selector (CSS selector)
1403
+ */
1404
+ getElementSelector(element) {
1405
+ if (element.id) {
1406
+ return `#${element.id}`;
1407
+ }
1408
+ if (element.className && typeof element.className === 'string') {
1409
+ const classes = element.className.split(' ').filter(c => c).join('.');
1410
+ if (classes) {
1411
+ return `${element.tagName.toLowerCase()}.${classes}`;
1412
+ }
1413
+ }
1414
+ return element.tagName.toLowerCase();
1415
+ }
1416
+ /**
1417
+ * Get metadata (user agent, screen resolution, viewport)
1418
+ */
1419
+ getMetadata() {
1420
+ return {
1421
+ user_agent: navigator.userAgent,
1422
+ screen_resolution: `${screen.width}x${screen.height}`,
1423
+ viewport_size: `${window.innerWidth}x${window.innerHeight}`,
1424
+ };
1425
+ }
1102
1426
  }
1103
1427
 
1104
1428
  /**
@@ -1122,6 +1446,11 @@ class Cuoral {
1122
1446
  key: options.publicKey,
1123
1447
  _t: Date.now().toString(),
1124
1448
  });
1449
+ // Add session_id if available (for widget to use existing session)
1450
+ const existingSessionId = localStorage.getItem('__x_loadID');
1451
+ if (existingSessionId) {
1452
+ params.set('cuoral_mobile_session_id', existingSessionId);
1453
+ }
1125
1454
  if (options.email)
1126
1455
  params.set('email', options.email);
1127
1456
  if (options.firstName)
@@ -1146,18 +1475,23 @@ class Cuoral {
1146
1475
  * Initialize Cuoral
1147
1476
  */
1148
1477
  async initialize() {
1478
+ // Fetch session configuration and initialize intelligence if enabled by backend
1479
+ await this.initializeIntelligence();
1480
+ console.log('[Cuoral] Initialize - Session ID:', localStorage.getItem('__x_loadID'));
1481
+ // Setup localStorage listener to detect when widget changes session
1482
+ this.setupStorageListener();
1149
1483
  this.bridge.initialize();
1150
1484
  // Recreate modal if it was destroyed (e.g., after clearSession)
1151
1485
  if (this.options.useModal && !this.modal) {
1152
1486
  const widgetUrl = this.getWidgetUrl();
1153
1487
  this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton);
1154
1488
  }
1155
- // Initialize modal if enabled
1489
+ // Update modal URL with session ID
1156
1490
  if (this.modal) {
1491
+ const widgetUrl = this.getWidgetUrl();
1492
+ this.modal.updateWidgetUrl(widgetUrl);
1157
1493
  this.modal.initialize();
1158
1494
  }
1159
- // Fetch session configuration and initialize intelligence if enabled by backend
1160
- await this.initializeIntelligence();
1161
1495
  }
1162
1496
  /**
1163
1497
  * Initialize intelligence based on backend configuration
@@ -1189,9 +1523,13 @@ class Cuoral {
1189
1523
  }
1190
1524
  // Only initialize intelligence if customer_intelligence is enabled in backend
1191
1525
  if (config && config.customer_intelligence === true) {
1526
+ console.log('[Cuoral] Initializing intelligence with session:', sessionId);
1192
1527
  this.intelligence = new CuoralIntelligence(sessionId);
1193
1528
  this.intelligence.init();
1194
1529
  }
1530
+ else {
1531
+ console.log('[Cuoral] Intelligence not enabled or no config for session:', sessionId);
1532
+ }
1195
1533
  }
1196
1534
  catch (error) {
1197
1535
  console.warn('[Cuoral] Failed to initialize intelligence:', error);
@@ -1248,6 +1586,82 @@ class Cuoral {
1248
1586
  this.intelligence.trackError(message, stackTrace, metadata);
1249
1587
  }
1250
1588
  }
1589
+ /**
1590
+ * Update user profile for the current session
1591
+ * Call this after user logs in to update the intelligence session with their profile
1592
+ * @param email - User's email address
1593
+ * @param name - User's full name
1594
+ */
1595
+ async updateUserProfile(email, name) {
1596
+ try {
1597
+ const sessionId = localStorage.getItem('__x_loadID');
1598
+ if (!sessionId) {
1599
+ console.warn('[Cuoral] No session ID found, cannot update profile');
1600
+ return false;
1601
+ }
1602
+ console.log('[Cuoral] Updating user profile for session:', sessionId);
1603
+ const response = await fetch('https://api.cuoral.com/conversation/set-profile', {
1604
+ method: 'POST',
1605
+ headers: { 'Content-Type': 'application/json' },
1606
+ body: JSON.stringify({
1607
+ session_id: sessionId,
1608
+ email: email,
1609
+ name: name,
1610
+ }),
1611
+ });
1612
+ if (!response.ok) {
1613
+ console.error('[Cuoral] Failed to update profile:', response.statusText);
1614
+ return false;
1615
+ }
1616
+ console.log('[Cuoral] ✓ User profile updated successfully for session:', sessionId);
1617
+ // Store user info locally
1618
+ this.options.email = email;
1619
+ const nameParts = name.split(' ');
1620
+ if (nameParts.length > 0) {
1621
+ this.options.firstName = nameParts[0];
1622
+ this.options.lastName = nameParts.slice(1).join(' ');
1623
+ }
1624
+ return true;
1625
+ }
1626
+ catch (error) {
1627
+ console.error('[Cuoral] Error updating user profile:', error);
1628
+ return false;
1629
+ }
1630
+ }
1631
+ /**
1632
+ * Start native screen recording programmatically
1633
+ * @returns Promise<boolean> - true if recording started successfully
1634
+ */
1635
+ async startRecording() {
1636
+ try {
1637
+ return await this.recorder.startRecording();
1638
+ }
1639
+ catch (error) {
1640
+ console.error('[Cuoral] Failed to start recording:', error);
1641
+ return false;
1642
+ }
1643
+ }
1644
+ /**
1645
+ * Stop native screen recording programmatically
1646
+ * Recording will be automatically uploaded to the portal
1647
+ * @returns Promise<{filePath?: string; duration?: number; uploaded?: boolean} | null> - Recording result or null if failed
1648
+ */
1649
+ async stopRecording() {
1650
+ try {
1651
+ const sessionId = localStorage.getItem('__x_loadID');
1652
+ const customerId = localStorage.getItem('cuoralCustomerId');
1653
+ return await this.recorder.stopRecording({
1654
+ autoUpload: true,
1655
+ sessionId: sessionId || undefined,
1656
+ publicKey: this.options.publicKey,
1657
+ customerId: customerId || undefined,
1658
+ });
1659
+ }
1660
+ catch (error) {
1661
+ console.error('[Cuoral] Failed to stop recording:', error);
1662
+ return null;
1663
+ }
1664
+ }
1251
1665
  /**
1252
1666
  * Open the widget modal
1253
1667
  */
@@ -1287,7 +1701,7 @@ class Cuoral {
1287
1701
  // Add session_id if available
1288
1702
  const sessionId = localStorage.getItem('__x_loadID');
1289
1703
  if (sessionId) {
1290
- params.set('session_id', sessionId);
1704
+ params.set('cuoral_mobile_session_id', sessionId);
1291
1705
  }
1292
1706
  if (this.options.email)
1293
1707
  params.set('email', this.options.email);
@@ -1341,6 +1755,28 @@ class Cuoral {
1341
1755
  }
1342
1756
  }
1343
1757
  }
1758
+ /**
1759
+ * Setup localStorage listener for session changes
1760
+ * Widget updates localStorage when creating new session, SDK detects and syncs
1761
+ */
1762
+ setupStorageListener() {
1763
+ // Poll localStorage every 2 seconds to detect session changes
1764
+ // (storage event doesn't fire for same-window changes)
1765
+ let lastKnownSession = localStorage.getItem('__x_loadID');
1766
+ setInterval(() => {
1767
+ const currentSession = localStorage.getItem('__x_loadID');
1768
+ if (currentSession && currentSession !== lastKnownSession) {
1769
+ console.log('[Cuoral] 🔄 Session changed in localStorage');
1770
+ console.log('[Cuoral] Old session:', lastKnownSession);
1771
+ console.log('[Cuoral] New session:', currentSession);
1772
+ lastKnownSession = currentSession;
1773
+ // Update intelligence to use new session
1774
+ if (this.intelligence) {
1775
+ this.intelligence.updateSessionId(currentSession);
1776
+ }
1777
+ }
1778
+ }, 2000);
1779
+ }
1344
1780
  /**
1345
1781
  * Clean up resources
1346
1782
  */
@@ -1432,9 +1868,27 @@ class Cuoral {
1432
1868
  });
1433
1869
  // Handle stop recording requests from widget
1434
1870
  this.bridge.on(CuoralMessageType.STOP_RECORDING, async () => {
1435
- const result = await this.recorder.stopRecording();
1436
- if (result) {
1437
- // Convert file path to web-accessible URL
1871
+ const sessionId = localStorage.getItem('__x_loadID');
1872
+ const customerId = localStorage.getItem('cuoralCustomerId');
1873
+ const result = await this.recorder.stopRecording({
1874
+ autoUpload: true,
1875
+ sessionId: sessionId || undefined,
1876
+ publicKey: this.options.publicKey,
1877
+ customerId: customerId || undefined,
1878
+ });
1879
+ if (result && result.uploaded) {
1880
+ // Video was automatically uploaded, just notify widget
1881
+ this.bridge.sendToWidget({
1882
+ type: CuoralMessageType.RECORDING_UPLOADED,
1883
+ payload: {
1884
+ duration: result.duration,
1885
+ uploaded: true,
1886
+ timestamp: Date.now()
1887
+ }
1888
+ });
1889
+ }
1890
+ else if (result) {
1891
+ // Upload failed or was disabled, send video data to widget (old behavior)
1438
1892
  const capacitorUrl = result.filePath ? Capacitor.convertFileSrc(result.filePath) : '';
1439
1893
  try {
1440
1894
  // Fetch the video blob from the capacitor URL
@@ -1 +1 @@
1
- {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}