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.js CHANGED
@@ -1,6 +1,26 @@
1
1
  'use strict';
2
2
 
3
3
  var core = require('@capacitor/core');
4
+ var rrweb = require('rrweb');
5
+
6
+ function _interopNamespaceDefault(e) {
7
+ var n = Object.create(null);
8
+ if (e) {
9
+ Object.keys(e).forEach(function (k) {
10
+ if (k !== 'default') {
11
+ var d = Object.getOwnPropertyDescriptor(e, k);
12
+ Object.defineProperty(n, k, d.get ? d : {
13
+ enumerable: true,
14
+ get: function () { return e[k]; }
15
+ });
16
+ }
17
+ });
18
+ }
19
+ n.default = e;
20
+ return Object.freeze(n);
21
+ }
22
+
23
+ var rrweb__namespace = /*#__PURE__*/_interopNamespaceDefault(rrweb);
4
24
 
5
25
  /**
6
26
  * Message types for communication between WebView and Native code
@@ -12,6 +32,7 @@ exports.CuoralMessageType = void 0;
12
32
  CuoralMessageType["STOP_RECORDING"] = "CUORAL_STOP_RECORDING";
13
33
  CuoralMessageType["RECORDING_STARTED"] = "CUORAL_RECORDING_STARTED";
14
34
  CuoralMessageType["RECORDING_STOPPED"] = "CUORAL_RECORDING_STOPPED";
35
+ CuoralMessageType["RECORDING_UPLOADED"] = "CUORAL_RECORDING_UPLOADED";
15
36
  CuoralMessageType["RECORDING_ERROR"] = "CUORAL_RECORDING_ERROR";
16
37
  // Screenshot
17
38
  CuoralMessageType["TAKE_SCREENSHOT"] = "CUORAL_TAKE_SCREENSHOT";
@@ -20,6 +41,9 @@ exports.CuoralMessageType = void 0;
20
41
  // Widget Communication
21
42
  CuoralMessageType["WIDGET_READY"] = "CUORAL_WIDGET_READY";
22
43
  CuoralMessageType["WIDGET_CLOSED"] = "CUORAL_WIDGET_CLOSED";
44
+ CuoralMessageType["SESSION_UPDATED"] = "CUORAL_SESSION_UPDATED";
45
+ CuoralMessageType["REQUEST_SESSION_ID"] = "CUORAL_REQUEST_SESSION_ID";
46
+ CuoralMessageType["SESSION_ID_RESPONSE"] = "CUORAL_SESSION_ID_RESPONSE";
23
47
  // File Upload
24
48
  CuoralMessageType["UPLOAD_FILE"] = "CUORAL_UPLOAD_FILE";
25
49
  CuoralMessageType["UPLOAD_PROGRESS"] = "CUORAL_UPLOAD_PROGRESS";
@@ -94,7 +118,7 @@ class CuoralRecorder {
94
118
  /**
95
119
  * Stop recording
96
120
  */
97
- async stopRecording() {
121
+ async stopRecording(options) {
98
122
  try {
99
123
  if (!this.isRecording) {
100
124
  // Send error message to widget so it can exit "stopping" state
@@ -104,23 +128,36 @@ class CuoralRecorder {
104
128
  });
105
129
  return null;
106
130
  }
107
- const result = await CuoralPlugin$1.stopRecording();
131
+ const result = await CuoralPlugin$1.stopRecording(options);
108
132
  if (result.success) {
109
133
  this.isRecording = false;
110
134
  const duration = this.recordingStartTime
111
135
  ? Math.floor((Date.now() - this.recordingStartTime) / 1000)
112
136
  : 0;
113
- // Post message to widget
114
- this.postMessage({
115
- type: exports.CuoralMessageType.RECORDING_STOPPED,
116
- payload: {
117
- filePath: result.filePath,
118
- duration: result.duration || duration,
119
- },
120
- });
137
+ // If uploaded, notify widget differently
138
+ if (result.uploaded) {
139
+ this.postMessage({
140
+ type: exports.CuoralMessageType.RECORDING_UPLOADED,
141
+ payload: {
142
+ duration: result.duration || duration,
143
+ uploaded: true
144
+ },
145
+ });
146
+ }
147
+ else {
148
+ // Post message to widget (old behavior)
149
+ this.postMessage({
150
+ type: exports.CuoralMessageType.RECORDING_STOPPED,
151
+ payload: {
152
+ filePath: result.filePath,
153
+ duration: result.duration || duration,
154
+ },
155
+ });
156
+ }
121
157
  return {
122
158
  filePath: result.filePath,
123
159
  duration: result.duration || duration,
160
+ uploaded: result.uploaded,
124
161
  };
125
162
  }
126
163
  // If result.success is false, send error to widget
@@ -604,11 +641,13 @@ class CuoralIntelligence {
604
641
  consoleErrorBackendUrl: 'https://api.cuoral.com/customer-intelligence/console-error',
605
642
  pageViewBackendUrl: 'https://api.cuoral.com/customer-intelligence/page-view',
606
643
  apiResponseBackendUrl: 'https://api.cuoral.com/customer-intelligence/api-response',
644
+ sessionReplayBackendUrl: 'https://api.cuoral.com/customer-intelligence/session-recording/batch',
607
645
  batchSize: 10,
608
646
  batchInterval: 2000,
609
647
  maxQueueSize: 30,
610
648
  retryAttempts: 1,
611
649
  retryDelay: 2000,
650
+ sessionReplayBatchInterval: 10000, // 10 seconds
612
651
  };
613
652
  this.queues = {
614
653
  console_error: [],
@@ -625,8 +664,34 @@ class CuoralIntelligence {
625
664
  // Network monitoring state
626
665
  this.originalFetch = null;
627
666
  this.originalXMLHttpRequest = null;
667
+ // Session replay state
668
+ this.rrwebStopFn = null;
669
+ this.rrwebEvents = [];
670
+ this.customEvents = [];
671
+ this.sessionReplayTimer = null;
672
+ this.clickTimestamps = new Map();
673
+ this.rageClickThreshold = 5; // 5 rapid clicks
674
+ this.rageClickWindowMs = 2000; // Within 2 seconds
628
675
  this.sessionId = sessionId;
629
676
  }
677
+ /**
678
+ * Update the session ID (e.g., when user logs in)
679
+ */
680
+ updateSessionId(newSessionId) {
681
+ console.log('[Cuoral Intelligence] Updating session ID from', this.sessionId, 'to', newSessionId);
682
+ this.sessionId = newSessionId;
683
+ // Flush any pending events with the old session before switching
684
+ this.flush();
685
+ // Update native error capture with new session ID
686
+ if (core.Capacitor.isNativePlatform()) {
687
+ CuoralPlugin.setupNativeErrorCapture({
688
+ backendUrl: this.config.consoleErrorBackendUrl,
689
+ sessionId: newSessionId
690
+ }).catch(error => {
691
+ console.warn('[Cuoral Intelligence] Failed to update native error capture:', error);
692
+ });
693
+ }
694
+ }
630
695
  /**
631
696
  * Initialize intelligence tracking
632
697
  */
@@ -639,6 +704,7 @@ class CuoralIntelligence {
639
704
  this.setupNetworkMonitoring();
640
705
  this.setupAppStateListener();
641
706
  this.setupNativeErrorCapture();
707
+ this.setupSessionReplay();
642
708
  this.isInitialized = true;
643
709
  this.flushPendingEvents();
644
710
  }
@@ -686,6 +752,17 @@ class CuoralIntelligence {
686
752
  delete this.batchTimers[type];
687
753
  }
688
754
  });
755
+ // Stop session replay
756
+ if (this.rrwebStopFn) {
757
+ this.rrwebStopFn();
758
+ this.rrwebStopFn = null;
759
+ }
760
+ if (this.sessionReplayTimer) {
761
+ clearInterval(this.sessionReplayTimer);
762
+ this.sessionReplayTimer = null;
763
+ }
764
+ // Flush remaining session replay data
765
+ this.flushSessionReplayBatch();
689
766
  // Restore original functions
690
767
  if (this.originalFetch) {
691
768
  window.fetch = this.originalFetch;
@@ -944,6 +1021,7 @@ class CuoralIntelligence {
944
1021
  url: window.location.href,
945
1022
  data,
946
1023
  };
1024
+ console.log(`[Intelligence] Enqueuing ${type} event with session:`, this.sessionId);
947
1025
  queue.push(event);
948
1026
  // Flush immediately for critical errors
949
1027
  const shouldFlushImmediately = type === 'api_call' && (data.status_code === 0 || data.status_code >= 400);
@@ -1101,6 +1179,271 @@ class CuoralIntelligence {
1101
1179
  // Silently fail if plugin is not available
1102
1180
  }
1103
1181
  }
1182
+ /**
1183
+ * Setup session replay with rrweb
1184
+ */
1185
+ setupSessionReplay() {
1186
+ if (!this.sessionId) {
1187
+ console.warn('[Cuoral Intelligence] Session replay requires a session ID');
1188
+ return;
1189
+ }
1190
+ // Start rrweb recording
1191
+ this.rrwebStopFn = rrweb__namespace.record({
1192
+ emit: (event) => {
1193
+ this.rrwebEvents.push(event);
1194
+ },
1195
+ checkoutEveryNms: 60000, // Full snapshot every minute
1196
+ sampling: {
1197
+ scroll: 150, // Throttle scroll events
1198
+ media: 800,
1199
+ input: 'last', // Only record final input value (privacy)
1200
+ },
1201
+ maskAllInputs: true, // Mask sensitive inputs (privacy)
1202
+ blockClass: 'cuoral-block',
1203
+ ignoreClass: 'cuoral-ignore',
1204
+ });
1205
+ // Setup custom event tracking
1206
+ this.setupClickTracking();
1207
+ this.setupScrollTracking();
1208
+ this.setupFormTracking();
1209
+ // Start batch timer (send every ~10 seconds)
1210
+ this.sessionReplayTimer = setInterval(() => {
1211
+ this.flushSessionReplayBatch();
1212
+ }, this.config.sessionReplayBatchInterval);
1213
+ }
1214
+ /**
1215
+ * Setup click tracking (including rage clicks)
1216
+ */
1217
+ setupClickTracking() {
1218
+ document.addEventListener('click', (event) => {
1219
+ const target = event.target;
1220
+ if (!target)
1221
+ return;
1222
+ const selector = this.getElementSelector(target);
1223
+ const elementText = target.textContent?.trim().substring(0, 100) || '';
1224
+ const url = window.location.href;
1225
+ const now = Date.now();
1226
+ // Track regular click
1227
+ this.addCustomEvent({
1228
+ name: 'click',
1229
+ category: 'interaction',
1230
+ url,
1231
+ element_selector: selector,
1232
+ element_text: elementText,
1233
+ event_timestamp: new Date(now).toISOString(),
1234
+ session_id: this.sessionId,
1235
+ properties: this.getMetadata(),
1236
+ });
1237
+ // Track rage click detection
1238
+ const clicks = this.clickTimestamps.get(selector) || [];
1239
+ clicks.push(now);
1240
+ // Remove old clicks outside the time window
1241
+ const recentClicks = clicks.filter(timestamp => now - timestamp < this.rageClickWindowMs);
1242
+ this.clickTimestamps.set(selector, recentClicks);
1243
+ // If 5+ clicks within 2 seconds = rage click
1244
+ if (recentClicks.length >= this.rageClickThreshold) {
1245
+ this.addCustomEvent({
1246
+ name: 'rage_click',
1247
+ category: 'frustration',
1248
+ url,
1249
+ element_selector: selector,
1250
+ element_text: elementText,
1251
+ event_timestamp: new Date(now).toISOString(),
1252
+ session_id: this.sessionId,
1253
+ properties: {
1254
+ ...this.getMetadata(),
1255
+ click_count: recentClicks.length,
1256
+ },
1257
+ });
1258
+ // Clear after detecting rage click
1259
+ this.clickTimestamps.delete(selector);
1260
+ }
1261
+ }, true);
1262
+ }
1263
+ /**
1264
+ * Setup scroll depth tracking
1265
+ */
1266
+ setupScrollTracking() {
1267
+ let scrollDepths = new Set();
1268
+ let ticking = false;
1269
+ const trackScroll = () => {
1270
+ const scrollPercentage = Math.round((window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100);
1271
+ // Track milestones: 25%, 50%, 75%, 100%
1272
+ const milestones = [25, 50, 75, 100];
1273
+ for (const milestone of milestones) {
1274
+ if (scrollPercentage >= milestone && !scrollDepths.has(milestone)) {
1275
+ scrollDepths.add(milestone);
1276
+ this.addCustomEvent({
1277
+ name: 'scroll_depth',
1278
+ category: 'engagement',
1279
+ url: window.location.href,
1280
+ event_timestamp: new Date().toISOString(),
1281
+ session_id: this.sessionId,
1282
+ properties: {
1283
+ ...this.getMetadata(),
1284
+ percentage: milestone,
1285
+ },
1286
+ });
1287
+ }
1288
+ }
1289
+ ticking = false;
1290
+ };
1291
+ window.addEventListener('scroll', () => {
1292
+ if (!ticking) {
1293
+ window.requestAnimationFrame(trackScroll);
1294
+ ticking = true;
1295
+ }
1296
+ });
1297
+ }
1298
+ /**
1299
+ * Setup form tracking
1300
+ */
1301
+ setupFormTracking() {
1302
+ const trackedForms = new WeakSet();
1303
+ // Track form starts
1304
+ document.addEventListener('focusin', (event) => {
1305
+ const target = event.target;
1306
+ if (!target)
1307
+ return;
1308
+ const form = target.closest('form');
1309
+ if (form && !trackedForms.has(form)) {
1310
+ trackedForms.add(form);
1311
+ this.addCustomEvent({
1312
+ name: 'form_started',
1313
+ category: 'form',
1314
+ url: window.location.href,
1315
+ element_selector: this.getElementSelector(form),
1316
+ event_timestamp: new Date().toISOString(),
1317
+ session_id: this.sessionId,
1318
+ properties: this.getMetadata(),
1319
+ });
1320
+ }
1321
+ }, true);
1322
+ // Track form submissions
1323
+ document.addEventListener('submit', (event) => {
1324
+ const form = event.target;
1325
+ if (!form)
1326
+ return;
1327
+ this.addCustomEvent({
1328
+ name: 'form_submitted',
1329
+ category: 'form',
1330
+ url: window.location.href,
1331
+ element_selector: this.getElementSelector(form),
1332
+ event_timestamp: new Date().toISOString(),
1333
+ session_id: this.sessionId,
1334
+ properties: this.getMetadata(),
1335
+ });
1336
+ }, true);
1337
+ // Track form abandonment (on page exit with incomplete forms)
1338
+ window.addEventListener('beforeunload', () => {
1339
+ document.querySelectorAll('form').forEach((form) => {
1340
+ const inputs = form.querySelectorAll('input, textarea, select');
1341
+ const hasValue = Array.from(inputs).some((input) => input.value);
1342
+ if (hasValue && !trackedForms.has(form)) {
1343
+ this.addCustomEvent({
1344
+ name: 'form_abandoned',
1345
+ category: 'form',
1346
+ url: window.location.href,
1347
+ element_selector: this.getElementSelector(form),
1348
+ event_timestamp: new Date().toISOString(),
1349
+ session_id: this.sessionId,
1350
+ properties: this.getMetadata(),
1351
+ });
1352
+ }
1353
+ });
1354
+ });
1355
+ }
1356
+ /**
1357
+ * Track custom business events (flows, features, etc.)
1358
+ */
1359
+ trackCustomEvent(name, category, properties = {}, elementSelector, elementText) {
1360
+ this.addCustomEvent({
1361
+ name,
1362
+ category,
1363
+ url: window.location.href,
1364
+ element_selector: elementSelector,
1365
+ element_text: elementText,
1366
+ event_timestamp: new Date().toISOString(),
1367
+ session_id: this.sessionId,
1368
+ properties: {
1369
+ ...this.getMetadata(),
1370
+ ...properties,
1371
+ },
1372
+ });
1373
+ }
1374
+ /**
1375
+ * Add a custom event to the buffer
1376
+ */
1377
+ addCustomEvent(event) {
1378
+ this.customEvents.push(event);
1379
+ }
1380
+ /**
1381
+ * Flush session replay batch
1382
+ */
1383
+ flushSessionReplayBatch() {
1384
+ if (!this.sessionId)
1385
+ return;
1386
+ const eventsToSend = [...this.rrwebEvents];
1387
+ const customEventsToSend = [...this.customEvents];
1388
+ // Clear buffers
1389
+ this.rrwebEvents = [];
1390
+ this.customEvents = [];
1391
+ // Only send if there's data
1392
+ if (eventsToSend.length === 0 && customEventsToSend.length === 0) {
1393
+ return;
1394
+ }
1395
+ const batch = {
1396
+ session_id: this.sessionId,
1397
+ events: eventsToSend,
1398
+ custom_events: customEventsToSend,
1399
+ };
1400
+ this.sendSessionReplayBatch(batch);
1401
+ }
1402
+ /**
1403
+ * Send session replay batch to backend
1404
+ */
1405
+ async sendSessionReplayBatch(batch) {
1406
+ try {
1407
+ const response = await fetch(this.config.sessionReplayBackendUrl, {
1408
+ method: 'POST',
1409
+ headers: {
1410
+ 'Content-Type': 'application/json',
1411
+ },
1412
+ body: JSON.stringify(batch),
1413
+ });
1414
+ if (!response.ok) {
1415
+ console.warn('[Cuoral Intelligence] Failed to send session replay batch');
1416
+ }
1417
+ }
1418
+ catch (error) {
1419
+ console.warn('[Cuoral Intelligence] Error sending session replay batch:', error);
1420
+ }
1421
+ }
1422
+ /**
1423
+ * Get element selector (CSS selector)
1424
+ */
1425
+ getElementSelector(element) {
1426
+ if (element.id) {
1427
+ return `#${element.id}`;
1428
+ }
1429
+ if (element.className && typeof element.className === 'string') {
1430
+ const classes = element.className.split(' ').filter(c => c).join('.');
1431
+ if (classes) {
1432
+ return `${element.tagName.toLowerCase()}.${classes}`;
1433
+ }
1434
+ }
1435
+ return element.tagName.toLowerCase();
1436
+ }
1437
+ /**
1438
+ * Get metadata (user agent, screen resolution, viewport)
1439
+ */
1440
+ getMetadata() {
1441
+ return {
1442
+ user_agent: navigator.userAgent,
1443
+ screen_resolution: `${screen.width}x${screen.height}`,
1444
+ viewport_size: `${window.innerWidth}x${window.innerHeight}`,
1445
+ };
1446
+ }
1104
1447
  }
1105
1448
 
1106
1449
  /**
@@ -1124,6 +1467,11 @@ class Cuoral {
1124
1467
  key: options.publicKey,
1125
1468
  _t: Date.now().toString(),
1126
1469
  });
1470
+ // Add session_id if available (for widget to use existing session)
1471
+ const existingSessionId = localStorage.getItem('__x_loadID');
1472
+ if (existingSessionId) {
1473
+ params.set('cuoral_mobile_session_id', existingSessionId);
1474
+ }
1127
1475
  if (options.email)
1128
1476
  params.set('email', options.email);
1129
1477
  if (options.firstName)
@@ -1148,18 +1496,23 @@ class Cuoral {
1148
1496
  * Initialize Cuoral
1149
1497
  */
1150
1498
  async initialize() {
1499
+ // Fetch session configuration and initialize intelligence if enabled by backend
1500
+ await this.initializeIntelligence();
1501
+ console.log('[Cuoral] Initialize - Session ID:', localStorage.getItem('__x_loadID'));
1502
+ // Setup localStorage listener to detect when widget changes session
1503
+ this.setupStorageListener();
1151
1504
  this.bridge.initialize();
1152
1505
  // Recreate modal if it was destroyed (e.g., after clearSession)
1153
1506
  if (this.options.useModal && !this.modal) {
1154
1507
  const widgetUrl = this.getWidgetUrl();
1155
1508
  this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton);
1156
1509
  }
1157
- // Initialize modal if enabled
1510
+ // Update modal URL with session ID
1158
1511
  if (this.modal) {
1512
+ const widgetUrl = this.getWidgetUrl();
1513
+ this.modal.updateWidgetUrl(widgetUrl);
1159
1514
  this.modal.initialize();
1160
1515
  }
1161
- // Fetch session configuration and initialize intelligence if enabled by backend
1162
- await this.initializeIntelligence();
1163
1516
  }
1164
1517
  /**
1165
1518
  * Initialize intelligence based on backend configuration
@@ -1191,9 +1544,13 @@ class Cuoral {
1191
1544
  }
1192
1545
  // Only initialize intelligence if customer_intelligence is enabled in backend
1193
1546
  if (config && config.customer_intelligence === true) {
1547
+ console.log('[Cuoral] Initializing intelligence with session:', sessionId);
1194
1548
  this.intelligence = new CuoralIntelligence(sessionId);
1195
1549
  this.intelligence.init();
1196
1550
  }
1551
+ else {
1552
+ console.log('[Cuoral] Intelligence not enabled or no config for session:', sessionId);
1553
+ }
1197
1554
  }
1198
1555
  catch (error) {
1199
1556
  console.warn('[Cuoral] Failed to initialize intelligence:', error);
@@ -1250,6 +1607,82 @@ class Cuoral {
1250
1607
  this.intelligence.trackError(message, stackTrace, metadata);
1251
1608
  }
1252
1609
  }
1610
+ /**
1611
+ * Update user profile for the current session
1612
+ * Call this after user logs in to update the intelligence session with their profile
1613
+ * @param email - User's email address
1614
+ * @param name - User's full name
1615
+ */
1616
+ async updateUserProfile(email, name) {
1617
+ try {
1618
+ const sessionId = localStorage.getItem('__x_loadID');
1619
+ if (!sessionId) {
1620
+ console.warn('[Cuoral] No session ID found, cannot update profile');
1621
+ return false;
1622
+ }
1623
+ console.log('[Cuoral] Updating user profile for session:', sessionId);
1624
+ const response = await fetch('https://api.cuoral.com/conversation/set-profile', {
1625
+ method: 'POST',
1626
+ headers: { 'Content-Type': 'application/json' },
1627
+ body: JSON.stringify({
1628
+ session_id: sessionId,
1629
+ email: email,
1630
+ name: name,
1631
+ }),
1632
+ });
1633
+ if (!response.ok) {
1634
+ console.error('[Cuoral] Failed to update profile:', response.statusText);
1635
+ return false;
1636
+ }
1637
+ console.log('[Cuoral] ✓ User profile updated successfully for session:', sessionId);
1638
+ // Store user info locally
1639
+ this.options.email = email;
1640
+ const nameParts = name.split(' ');
1641
+ if (nameParts.length > 0) {
1642
+ this.options.firstName = nameParts[0];
1643
+ this.options.lastName = nameParts.slice(1).join(' ');
1644
+ }
1645
+ return true;
1646
+ }
1647
+ catch (error) {
1648
+ console.error('[Cuoral] Error updating user profile:', error);
1649
+ return false;
1650
+ }
1651
+ }
1652
+ /**
1653
+ * Start native screen recording programmatically
1654
+ * @returns Promise<boolean> - true if recording started successfully
1655
+ */
1656
+ async startRecording() {
1657
+ try {
1658
+ return await this.recorder.startRecording();
1659
+ }
1660
+ catch (error) {
1661
+ console.error('[Cuoral] Failed to start recording:', error);
1662
+ return false;
1663
+ }
1664
+ }
1665
+ /**
1666
+ * Stop native screen recording programmatically
1667
+ * Recording will be automatically uploaded to the portal
1668
+ * @returns Promise<{filePath?: string; duration?: number; uploaded?: boolean} | null> - Recording result or null if failed
1669
+ */
1670
+ async stopRecording() {
1671
+ try {
1672
+ const sessionId = localStorage.getItem('__x_loadID');
1673
+ const customerId = localStorage.getItem('cuoralCustomerId');
1674
+ return await this.recorder.stopRecording({
1675
+ autoUpload: true,
1676
+ sessionId: sessionId || undefined,
1677
+ publicKey: this.options.publicKey,
1678
+ customerId: customerId || undefined,
1679
+ });
1680
+ }
1681
+ catch (error) {
1682
+ console.error('[Cuoral] Failed to stop recording:', error);
1683
+ return null;
1684
+ }
1685
+ }
1253
1686
  /**
1254
1687
  * Open the widget modal
1255
1688
  */
@@ -1289,7 +1722,7 @@ class Cuoral {
1289
1722
  // Add session_id if available
1290
1723
  const sessionId = localStorage.getItem('__x_loadID');
1291
1724
  if (sessionId) {
1292
- params.set('session_id', sessionId);
1725
+ params.set('cuoral_mobile_session_id', sessionId);
1293
1726
  }
1294
1727
  if (this.options.email)
1295
1728
  params.set('email', this.options.email);
@@ -1343,6 +1776,28 @@ class Cuoral {
1343
1776
  }
1344
1777
  }
1345
1778
  }
1779
+ /**
1780
+ * Setup localStorage listener for session changes
1781
+ * Widget updates localStorage when creating new session, SDK detects and syncs
1782
+ */
1783
+ setupStorageListener() {
1784
+ // Poll localStorage every 2 seconds to detect session changes
1785
+ // (storage event doesn't fire for same-window changes)
1786
+ let lastKnownSession = localStorage.getItem('__x_loadID');
1787
+ setInterval(() => {
1788
+ const currentSession = localStorage.getItem('__x_loadID');
1789
+ if (currentSession && currentSession !== lastKnownSession) {
1790
+ console.log('[Cuoral] 🔄 Session changed in localStorage');
1791
+ console.log('[Cuoral] Old session:', lastKnownSession);
1792
+ console.log('[Cuoral] New session:', currentSession);
1793
+ lastKnownSession = currentSession;
1794
+ // Update intelligence to use new session
1795
+ if (this.intelligence) {
1796
+ this.intelligence.updateSessionId(currentSession);
1797
+ }
1798
+ }
1799
+ }, 2000);
1800
+ }
1346
1801
  /**
1347
1802
  * Clean up resources
1348
1803
  */
@@ -1434,9 +1889,27 @@ class Cuoral {
1434
1889
  });
1435
1890
  // Handle stop recording requests from widget
1436
1891
  this.bridge.on(exports.CuoralMessageType.STOP_RECORDING, async () => {
1437
- const result = await this.recorder.stopRecording();
1438
- if (result) {
1439
- // Convert file path to web-accessible URL
1892
+ const sessionId = localStorage.getItem('__x_loadID');
1893
+ const customerId = localStorage.getItem('cuoralCustomerId');
1894
+ const result = await this.recorder.stopRecording({
1895
+ autoUpload: true,
1896
+ sessionId: sessionId || undefined,
1897
+ publicKey: this.options.publicKey,
1898
+ customerId: customerId || undefined,
1899
+ });
1900
+ if (result && result.uploaded) {
1901
+ // Video was automatically uploaded, just notify widget
1902
+ this.bridge.sendToWidget({
1903
+ type: exports.CuoralMessageType.RECORDING_UPLOADED,
1904
+ payload: {
1905
+ duration: result.duration,
1906
+ uploaded: true,
1907
+ timestamp: Date.now()
1908
+ }
1909
+ });
1910
+ }
1911
+ else if (result) {
1912
+ // Upload failed or was disabled, send video data to widget (old behavior)
1440
1913
  const capacitorUrl = result.filePath ? core.Capacitor.convertFileSrc(result.filePath) : '';
1441
1914
  try {
1442
1915
  // Fetch the video blob from the capacitor URL
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}