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.
- package/README.md +72 -1
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
- package/android/build/.transforms/bb54161301273cf9b5b94a21c0fb3f23/transformed/classes/classes_dex/classes.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$1.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$2.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$3.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$4.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$5.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$6.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$InitiateCallback.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$UploadCallback.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/desugar_graph.bin +0 -0
- package/android/build/intermediates/compile_library_classes_jar/debug/classes.jar +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$1.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$2.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$3.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$4.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$5.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$6.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$InitiateCallback.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$UploadCallback.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$1.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$2.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$3.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$4.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$5.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$6.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$InitiateCallback.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$UploadCallback.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin.class +0 -0
- package/android/build/intermediates/runtime_library_classes_jar/debug/classes.jar +0 -0
- package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/cuoral/ionic/CuoralPlugin.java +205 -5
- package/dist/cuoral.d.ts +27 -0
- package/dist/cuoral.d.ts.map +1 -1
- package/dist/cuoral.js +137 -7
- package/dist/index.esm.js +471 -17
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +490 -17
- package/dist/index.js.map +1 -1
- package/dist/intelligence.d.ts +51 -0
- package/dist/intelligence.d.ts.map +1 -1
- package/dist/intelligence.js +307 -0
- package/dist/plugin.d.ts +15 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +23 -10
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/ios/Plugin/CuoralPlugin.swift +249 -13
- package/package.json +4 -2
- package/src/cuoral.ts +151 -8
- package/src/intelligence.ts +375 -0
- package/src/plugin.ts +39 -11
- 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
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
//
|
|
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('
|
|
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
|
|
1436
|
-
|
|
1437
|
-
|
|
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
|
package/dist/index.esm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|