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.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
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
//
|
|
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('
|
|
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
|
|
1438
|
-
|
|
1439
|
-
|
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|