cuoral-ionic 0.0.5 → 0.0.6
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/dist/cuoral.d.ts +13 -0
- package/dist/cuoral.d.ts.map +1 -1
- package/dist/cuoral.js +26 -0
- package/dist/index.esm.js +314 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +333 -0
- package/dist/index.js.map +1 -1
- package/dist/intelligence.d.ts +47 -0
- package/dist/intelligence.d.ts.map +1 -1
- package/dist/intelligence.js +288 -0
- package/package.json +4 -2
- package/src/cuoral.ts +26 -0
- package/src/intelligence.ts +352 -0
package/README.md
CHANGED
|
@@ -343,7 +343,19 @@ new Cuoral({
|
|
|
343
343
|
|
|
344
344
|
```typescript
|
|
345
345
|
// Initialize Cuoral
|
|
346
|
-
cuoral.initialize(): void
|
|
346
|
+
cuoral.initialize(): Promise<void>
|
|
347
|
+
|
|
348
|
+
// Track page/screen view (for intelligence)
|
|
349
|
+
cuoral.trackPageView(screen: string, metadata?: any): void
|
|
350
|
+
|
|
351
|
+
// Track error manually (for intelligence)
|
|
352
|
+
cuoral.trackError(message: string, stackTrace?: string, metadata?: any): void
|
|
353
|
+
|
|
354
|
+
// Start native screen recording programmatically
|
|
355
|
+
cuoral.startRecording(): Promise<boolean>
|
|
356
|
+
|
|
357
|
+
// Stop native screen recording programmatically
|
|
358
|
+
cuoral.stopRecording(): Promise<{filePath?: string; duration?: number} | null>
|
|
347
359
|
|
|
348
360
|
// Get widget URL for iframe embedding
|
|
349
361
|
cuoral.getWidgetUrl(): string
|
|
@@ -357,10 +369,69 @@ cuoral.closeModal(): void
|
|
|
357
369
|
// Check if modal is open
|
|
358
370
|
cuoral.isModalOpen(): boolean
|
|
359
371
|
|
|
372
|
+
// Clear and end current session (call before logout)
|
|
373
|
+
cuoral.clearSession(): Promise<void>
|
|
374
|
+
|
|
360
375
|
// Clean up resources
|
|
361
376
|
cuoral.destroy(): void
|
|
362
377
|
```
|
|
363
378
|
|
|
379
|
+
### Programmatic Screen Recording
|
|
380
|
+
|
|
381
|
+
You can trigger native screen recording programmatically from your app code:
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
async startUserRecording() {
|
|
385
|
+
const started = await this.cuoral.startRecording();
|
|
386
|
+
if (started) {
|
|
387
|
+
console.log('Recording started successfully');
|
|
388
|
+
this.isRecording = true;
|
|
389
|
+
} else {
|
|
390
|
+
console.error('Failed to start recording');
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async stopUserRecording() {
|
|
395
|
+
const result = await this.cuoral.stopRecording();
|
|
396
|
+
if (result) {
|
|
397
|
+
console.log('Recording stopped', {
|
|
398
|
+
filePath: result.filePath,
|
|
399
|
+
duration: result.duration
|
|
400
|
+
});
|
|
401
|
+
this.isRecording = false;
|
|
402
|
+
} else {
|
|
403
|
+
console.error('Failed to stop recording');
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**Use Cases:**
|
|
409
|
+
- Allow users to record their issue before contacting support
|
|
410
|
+
- Implement custom recording UI in your app
|
|
411
|
+
- Record specific user flows programmatically
|
|
412
|
+
- Create bug reporting features with automatic recording
|
|
413
|
+
|
|
414
|
+
**Note:** Recording still requires user permission on iOS (microphone access). The video file is automatically processed and available for playback in the support widget.
|
|
415
|
+
|
|
416
|
+
### Manual Intelligence Tracking
|
|
417
|
+
|
|
418
|
+
Track custom events beyond automatic tracking:
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
// Track page/screen view
|
|
422
|
+
this.cuoral.trackPageView('/checkout', {
|
|
423
|
+
cart_items: 3,
|
|
424
|
+
total_value: 99.99
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Track error manually
|
|
428
|
+
this.cuoral.trackError(
|
|
429
|
+
'Payment failed',
|
|
430
|
+
error.stack,
|
|
431
|
+
{ payment_method: 'credit_card', amount: 99.99 }
|
|
432
|
+
);
|
|
433
|
+
```
|
|
434
|
+
|
|
364
435
|
## What Gets Handled Automatically
|
|
365
436
|
|
|
366
437
|
- ✅ Support ticket creation and management
|
|
Binary file
|
|
Binary file
|
package/dist/cuoral.d.ts
CHANGED
|
@@ -40,6 +40,19 @@ export declare class Cuoral {
|
|
|
40
40
|
* Track an error manually
|
|
41
41
|
*/
|
|
42
42
|
trackError(message: string, stackTrace?: string, metadata?: any): void;
|
|
43
|
+
/**
|
|
44
|
+
* Start native screen recording programmatically
|
|
45
|
+
* @returns Promise<boolean> - true if recording started successfully
|
|
46
|
+
*/
|
|
47
|
+
startRecording(): Promise<boolean>;
|
|
48
|
+
/**
|
|
49
|
+
* Stop native screen recording programmatically
|
|
50
|
+
* @returns Promise<{filePath?: string; duration?: number} | null> - Recording result or null if failed
|
|
51
|
+
*/
|
|
52
|
+
stopRecording(): Promise<{
|
|
53
|
+
filePath?: string;
|
|
54
|
+
duration?: number;
|
|
55
|
+
} | null>;
|
|
43
56
|
/**
|
|
44
57
|
* Open the widget modal
|
|
45
58
|
*/
|
package/dist/cuoral.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cuoral.d.ts","sourceRoot":"","sources":["../src/cuoral.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAeD;;GAEG;AACH,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,KAAK,CAAC,CAAc;IAC5B,OAAO,CAAC,YAAY,CAAC,CAAqB;IAC1C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAuC;IACpF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAwB;gBAElD,OAAO,EAAE,aAAa;IA2ClC;;OAEG;IACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBxC;;OAEG;YACW,sBAAsB;IAwCpC;;OAEG;YACW,yBAAyB;IAoCvC;;OAEG;IACI,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,GAAG,IAAI;IAM1D;;OAEG;IACI,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,GAAG,IAAI;IAM7E;;OAEG;IACI,SAAS,IAAI,IAAI;IASxB;;OAEG;IACI,UAAU,IAAI,IAAI;IAMzB;;OAEG;IACI,WAAW,IAAI,OAAO;IAI7B;;OAEG;IACI,YAAY,IAAI,MAAM;IAuB7B;;OAEG;IACU,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IA6C1C;;OAEG;IACI,OAAO,IAAI,IAAI;IActB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAa5B;;OAEG;YACW,eAAe;IAkC7B;;OAEG;YACW,UAAU;IAmBxB;;OAEG;IACH,OAAO,CAAC,oBAAoB;CAuE7B"}
|
|
1
|
+
{"version":3,"file":"cuoral.d.ts","sourceRoot":"","sources":["../src/cuoral.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAeD;;GAEG;AACH,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,KAAK,CAAC,CAAc;IAC5B,OAAO,CAAC,YAAY,CAAC,CAAqB;IAC1C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAuC;IACpF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAwB;gBAElD,OAAO,EAAE,aAAa;IA2ClC;;OAEG;IACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBxC;;OAEG;YACW,sBAAsB;IAwCpC;;OAEG;YACW,yBAAyB;IAoCvC;;OAEG;IACI,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,GAAG,IAAI;IAM1D;;OAEG;IACI,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,GAAG,IAAI;IAM7E;;;OAGG;IACU,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC;IAS/C;;;OAGG;IACU,aAAa,IAAI,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAC,GAAG,IAAI,CAAC;IASpF;;OAEG;IACI,SAAS,IAAI,IAAI;IASxB;;OAEG;IACI,UAAU,IAAI,IAAI;IAMzB;;OAEG;IACI,WAAW,IAAI,OAAO;IAI7B;;OAEG;IACI,YAAY,IAAI,MAAM;IAuB7B;;OAEG;IACU,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IA6C1C;;OAEG;IACI,OAAO,IAAI,IAAI;IActB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAa5B;;OAEG;YACW,eAAe;IAkC7B;;OAEG;YACW,UAAU;IAmBxB;;OAEG;IACH,OAAO,CAAC,oBAAoB;CAuE7B"}
|
package/dist/cuoral.js
CHANGED
|
@@ -151,6 +151,32 @@ export class Cuoral {
|
|
|
151
151
|
this.intelligence.trackError(message, stackTrace, metadata);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Start native screen recording programmatically
|
|
156
|
+
* @returns Promise<boolean> - true if recording started successfully
|
|
157
|
+
*/
|
|
158
|
+
async startRecording() {
|
|
159
|
+
try {
|
|
160
|
+
return await this.recorder.startRecording();
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
console.error('[Cuoral] Failed to start recording:', error);
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Stop native screen recording programmatically
|
|
169
|
+
* @returns Promise<{filePath?: string; duration?: number} | null> - Recording result or null if failed
|
|
170
|
+
*/
|
|
171
|
+
async stopRecording() {
|
|
172
|
+
try {
|
|
173
|
+
return await this.recorder.stopRecording();
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
console.error('[Cuoral] Failed to stop recording:', error);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
154
180
|
/**
|
|
155
181
|
* Open the widget modal
|
|
156
182
|
*/
|
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
|
|
@@ -602,11 +603,13 @@ class CuoralIntelligence {
|
|
|
602
603
|
consoleErrorBackendUrl: 'https://api.cuoral.com/customer-intelligence/console-error',
|
|
603
604
|
pageViewBackendUrl: 'https://api.cuoral.com/customer-intelligence/page-view',
|
|
604
605
|
apiResponseBackendUrl: 'https://api.cuoral.com/customer-intelligence/api-response',
|
|
606
|
+
sessionReplayBackendUrl: 'https://api.cuoral.com/customer-intelligence/session-recording/batch',
|
|
605
607
|
batchSize: 10,
|
|
606
608
|
batchInterval: 2000,
|
|
607
609
|
maxQueueSize: 30,
|
|
608
610
|
retryAttempts: 1,
|
|
609
611
|
retryDelay: 2000,
|
|
612
|
+
sessionReplayBatchInterval: 10000, // 10 seconds
|
|
610
613
|
};
|
|
611
614
|
this.queues = {
|
|
612
615
|
console_error: [],
|
|
@@ -623,6 +626,14 @@ class CuoralIntelligence {
|
|
|
623
626
|
// Network monitoring state
|
|
624
627
|
this.originalFetch = null;
|
|
625
628
|
this.originalXMLHttpRequest = null;
|
|
629
|
+
// Session replay state
|
|
630
|
+
this.rrwebStopFn = null;
|
|
631
|
+
this.rrwebEvents = [];
|
|
632
|
+
this.customEvents = [];
|
|
633
|
+
this.sessionReplayTimer = null;
|
|
634
|
+
this.clickTimestamps = new Map();
|
|
635
|
+
this.rageClickThreshold = 5; // 5 rapid clicks
|
|
636
|
+
this.rageClickWindowMs = 2000; // Within 2 seconds
|
|
626
637
|
this.sessionId = sessionId;
|
|
627
638
|
}
|
|
628
639
|
/**
|
|
@@ -637,6 +648,7 @@ class CuoralIntelligence {
|
|
|
637
648
|
this.setupNetworkMonitoring();
|
|
638
649
|
this.setupAppStateListener();
|
|
639
650
|
this.setupNativeErrorCapture();
|
|
651
|
+
this.setupSessionReplay();
|
|
640
652
|
this.isInitialized = true;
|
|
641
653
|
this.flushPendingEvents();
|
|
642
654
|
}
|
|
@@ -684,6 +696,17 @@ class CuoralIntelligence {
|
|
|
684
696
|
delete this.batchTimers[type];
|
|
685
697
|
}
|
|
686
698
|
});
|
|
699
|
+
// Stop session replay
|
|
700
|
+
if (this.rrwebStopFn) {
|
|
701
|
+
this.rrwebStopFn();
|
|
702
|
+
this.rrwebStopFn = null;
|
|
703
|
+
}
|
|
704
|
+
if (this.sessionReplayTimer) {
|
|
705
|
+
clearInterval(this.sessionReplayTimer);
|
|
706
|
+
this.sessionReplayTimer = null;
|
|
707
|
+
}
|
|
708
|
+
// Flush remaining session replay data
|
|
709
|
+
this.flushSessionReplayBatch();
|
|
687
710
|
// Restore original functions
|
|
688
711
|
if (this.originalFetch) {
|
|
689
712
|
window.fetch = this.originalFetch;
|
|
@@ -1099,6 +1122,271 @@ class CuoralIntelligence {
|
|
|
1099
1122
|
// Silently fail if plugin is not available
|
|
1100
1123
|
}
|
|
1101
1124
|
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Setup session replay with rrweb
|
|
1127
|
+
*/
|
|
1128
|
+
setupSessionReplay() {
|
|
1129
|
+
if (!this.sessionId) {
|
|
1130
|
+
console.warn('[Cuoral Intelligence] Session replay requires a session ID');
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
// Start rrweb recording
|
|
1134
|
+
this.rrwebStopFn = rrweb.record({
|
|
1135
|
+
emit: (event) => {
|
|
1136
|
+
this.rrwebEvents.push(event);
|
|
1137
|
+
},
|
|
1138
|
+
checkoutEveryNms: 60000, // Full snapshot every minute
|
|
1139
|
+
sampling: {
|
|
1140
|
+
scroll: 150, // Throttle scroll events
|
|
1141
|
+
media: 800,
|
|
1142
|
+
input: 'last', // Only record final input value (privacy)
|
|
1143
|
+
},
|
|
1144
|
+
maskAllInputs: true, // Mask sensitive inputs (privacy)
|
|
1145
|
+
blockClass: 'cuoral-block',
|
|
1146
|
+
ignoreClass: 'cuoral-ignore',
|
|
1147
|
+
});
|
|
1148
|
+
// Setup custom event tracking
|
|
1149
|
+
this.setupClickTracking();
|
|
1150
|
+
this.setupScrollTracking();
|
|
1151
|
+
this.setupFormTracking();
|
|
1152
|
+
// Start batch timer (send every ~10 seconds)
|
|
1153
|
+
this.sessionReplayTimer = setInterval(() => {
|
|
1154
|
+
this.flushSessionReplayBatch();
|
|
1155
|
+
}, this.config.sessionReplayBatchInterval);
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Setup click tracking (including rage clicks)
|
|
1159
|
+
*/
|
|
1160
|
+
setupClickTracking() {
|
|
1161
|
+
document.addEventListener('click', (event) => {
|
|
1162
|
+
const target = event.target;
|
|
1163
|
+
if (!target)
|
|
1164
|
+
return;
|
|
1165
|
+
const selector = this.getElementSelector(target);
|
|
1166
|
+
const elementText = target.textContent?.trim().substring(0, 100) || '';
|
|
1167
|
+
const url = window.location.href;
|
|
1168
|
+
const now = Date.now();
|
|
1169
|
+
// Track regular click
|
|
1170
|
+
this.addCustomEvent({
|
|
1171
|
+
name: 'click',
|
|
1172
|
+
category: 'interaction',
|
|
1173
|
+
url,
|
|
1174
|
+
element_selector: selector,
|
|
1175
|
+
element_text: elementText,
|
|
1176
|
+
event_timestamp: new Date(now).toISOString(),
|
|
1177
|
+
session_id: this.sessionId,
|
|
1178
|
+
properties: this.getMetadata(),
|
|
1179
|
+
});
|
|
1180
|
+
// Track rage click detection
|
|
1181
|
+
const clicks = this.clickTimestamps.get(selector) || [];
|
|
1182
|
+
clicks.push(now);
|
|
1183
|
+
// Remove old clicks outside the time window
|
|
1184
|
+
const recentClicks = clicks.filter(timestamp => now - timestamp < this.rageClickWindowMs);
|
|
1185
|
+
this.clickTimestamps.set(selector, recentClicks);
|
|
1186
|
+
// If 5+ clicks within 2 seconds = rage click
|
|
1187
|
+
if (recentClicks.length >= this.rageClickThreshold) {
|
|
1188
|
+
this.addCustomEvent({
|
|
1189
|
+
name: 'rage_click',
|
|
1190
|
+
category: 'frustration',
|
|
1191
|
+
url,
|
|
1192
|
+
element_selector: selector,
|
|
1193
|
+
element_text: elementText,
|
|
1194
|
+
event_timestamp: new Date(now).toISOString(),
|
|
1195
|
+
session_id: this.sessionId,
|
|
1196
|
+
properties: {
|
|
1197
|
+
...this.getMetadata(),
|
|
1198
|
+
click_count: recentClicks.length,
|
|
1199
|
+
},
|
|
1200
|
+
});
|
|
1201
|
+
// Clear after detecting rage click
|
|
1202
|
+
this.clickTimestamps.delete(selector);
|
|
1203
|
+
}
|
|
1204
|
+
}, true);
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Setup scroll depth tracking
|
|
1208
|
+
*/
|
|
1209
|
+
setupScrollTracking() {
|
|
1210
|
+
let scrollDepths = new Set();
|
|
1211
|
+
let ticking = false;
|
|
1212
|
+
const trackScroll = () => {
|
|
1213
|
+
const scrollPercentage = Math.round((window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100);
|
|
1214
|
+
// Track milestones: 25%, 50%, 75%, 100%
|
|
1215
|
+
const milestones = [25, 50, 75, 100];
|
|
1216
|
+
for (const milestone of milestones) {
|
|
1217
|
+
if (scrollPercentage >= milestone && !scrollDepths.has(milestone)) {
|
|
1218
|
+
scrollDepths.add(milestone);
|
|
1219
|
+
this.addCustomEvent({
|
|
1220
|
+
name: 'scroll_depth',
|
|
1221
|
+
category: 'engagement',
|
|
1222
|
+
url: window.location.href,
|
|
1223
|
+
event_timestamp: new Date().toISOString(),
|
|
1224
|
+
session_id: this.sessionId,
|
|
1225
|
+
properties: {
|
|
1226
|
+
...this.getMetadata(),
|
|
1227
|
+
percentage: milestone,
|
|
1228
|
+
},
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
ticking = false;
|
|
1233
|
+
};
|
|
1234
|
+
window.addEventListener('scroll', () => {
|
|
1235
|
+
if (!ticking) {
|
|
1236
|
+
window.requestAnimationFrame(trackScroll);
|
|
1237
|
+
ticking = true;
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Setup form tracking
|
|
1243
|
+
*/
|
|
1244
|
+
setupFormTracking() {
|
|
1245
|
+
const trackedForms = new WeakSet();
|
|
1246
|
+
// Track form starts
|
|
1247
|
+
document.addEventListener('focusin', (event) => {
|
|
1248
|
+
const target = event.target;
|
|
1249
|
+
if (!target)
|
|
1250
|
+
return;
|
|
1251
|
+
const form = target.closest('form');
|
|
1252
|
+
if (form && !trackedForms.has(form)) {
|
|
1253
|
+
trackedForms.add(form);
|
|
1254
|
+
this.addCustomEvent({
|
|
1255
|
+
name: 'form_started',
|
|
1256
|
+
category: 'form',
|
|
1257
|
+
url: window.location.href,
|
|
1258
|
+
element_selector: this.getElementSelector(form),
|
|
1259
|
+
event_timestamp: new Date().toISOString(),
|
|
1260
|
+
session_id: this.sessionId,
|
|
1261
|
+
properties: this.getMetadata(),
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
}, true);
|
|
1265
|
+
// Track form submissions
|
|
1266
|
+
document.addEventListener('submit', (event) => {
|
|
1267
|
+
const form = event.target;
|
|
1268
|
+
if (!form)
|
|
1269
|
+
return;
|
|
1270
|
+
this.addCustomEvent({
|
|
1271
|
+
name: 'form_submitted',
|
|
1272
|
+
category: 'form',
|
|
1273
|
+
url: window.location.href,
|
|
1274
|
+
element_selector: this.getElementSelector(form),
|
|
1275
|
+
event_timestamp: new Date().toISOString(),
|
|
1276
|
+
session_id: this.sessionId,
|
|
1277
|
+
properties: this.getMetadata(),
|
|
1278
|
+
});
|
|
1279
|
+
}, true);
|
|
1280
|
+
// Track form abandonment (on page exit with incomplete forms)
|
|
1281
|
+
window.addEventListener('beforeunload', () => {
|
|
1282
|
+
document.querySelectorAll('form').forEach((form) => {
|
|
1283
|
+
const inputs = form.querySelectorAll('input, textarea, select');
|
|
1284
|
+
const hasValue = Array.from(inputs).some((input) => input.value);
|
|
1285
|
+
if (hasValue && !trackedForms.has(form)) {
|
|
1286
|
+
this.addCustomEvent({
|
|
1287
|
+
name: 'form_abandoned',
|
|
1288
|
+
category: 'form',
|
|
1289
|
+
url: window.location.href,
|
|
1290
|
+
element_selector: this.getElementSelector(form),
|
|
1291
|
+
event_timestamp: new Date().toISOString(),
|
|
1292
|
+
session_id: this.sessionId,
|
|
1293
|
+
properties: this.getMetadata(),
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Track custom business events (flows, features, etc.)
|
|
1301
|
+
*/
|
|
1302
|
+
trackCustomEvent(name, category, properties = {}, elementSelector, elementText) {
|
|
1303
|
+
this.addCustomEvent({
|
|
1304
|
+
name,
|
|
1305
|
+
category,
|
|
1306
|
+
url: window.location.href,
|
|
1307
|
+
element_selector: elementSelector,
|
|
1308
|
+
element_text: elementText,
|
|
1309
|
+
event_timestamp: new Date().toISOString(),
|
|
1310
|
+
session_id: this.sessionId,
|
|
1311
|
+
properties: {
|
|
1312
|
+
...this.getMetadata(),
|
|
1313
|
+
...properties,
|
|
1314
|
+
},
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Add a custom event to the buffer
|
|
1319
|
+
*/
|
|
1320
|
+
addCustomEvent(event) {
|
|
1321
|
+
this.customEvents.push(event);
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Flush session replay batch
|
|
1325
|
+
*/
|
|
1326
|
+
flushSessionReplayBatch() {
|
|
1327
|
+
if (!this.sessionId)
|
|
1328
|
+
return;
|
|
1329
|
+
const eventsToSend = [...this.rrwebEvents];
|
|
1330
|
+
const customEventsToSend = [...this.customEvents];
|
|
1331
|
+
// Clear buffers
|
|
1332
|
+
this.rrwebEvents = [];
|
|
1333
|
+
this.customEvents = [];
|
|
1334
|
+
// Only send if there's data
|
|
1335
|
+
if (eventsToSend.length === 0 && customEventsToSend.length === 0) {
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const batch = {
|
|
1339
|
+
session_id: this.sessionId,
|
|
1340
|
+
events: eventsToSend,
|
|
1341
|
+
custom_events: customEventsToSend,
|
|
1342
|
+
};
|
|
1343
|
+
this.sendSessionReplayBatch(batch);
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Send session replay batch to backend
|
|
1347
|
+
*/
|
|
1348
|
+
async sendSessionReplayBatch(batch) {
|
|
1349
|
+
try {
|
|
1350
|
+
const response = await fetch(this.config.sessionReplayBackendUrl, {
|
|
1351
|
+
method: 'POST',
|
|
1352
|
+
headers: {
|
|
1353
|
+
'Content-Type': 'application/json',
|
|
1354
|
+
},
|
|
1355
|
+
body: JSON.stringify(batch),
|
|
1356
|
+
});
|
|
1357
|
+
if (!response.ok) {
|
|
1358
|
+
console.warn('[Cuoral Intelligence] Failed to send session replay batch');
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
catch (error) {
|
|
1362
|
+
console.warn('[Cuoral Intelligence] Error sending session replay batch:', error);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Get element selector (CSS selector)
|
|
1367
|
+
*/
|
|
1368
|
+
getElementSelector(element) {
|
|
1369
|
+
if (element.id) {
|
|
1370
|
+
return `#${element.id}`;
|
|
1371
|
+
}
|
|
1372
|
+
if (element.className && typeof element.className === 'string') {
|
|
1373
|
+
const classes = element.className.split(' ').filter(c => c).join('.');
|
|
1374
|
+
if (classes) {
|
|
1375
|
+
return `${element.tagName.toLowerCase()}.${classes}`;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return element.tagName.toLowerCase();
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Get metadata (user agent, screen resolution, viewport)
|
|
1382
|
+
*/
|
|
1383
|
+
getMetadata() {
|
|
1384
|
+
return {
|
|
1385
|
+
user_agent: navigator.userAgent,
|
|
1386
|
+
screen_resolution: `${screen.width}x${screen.height}`,
|
|
1387
|
+
viewport_size: `${window.innerWidth}x${window.innerHeight}`,
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1102
1390
|
}
|
|
1103
1391
|
|
|
1104
1392
|
/**
|
|
@@ -1248,6 +1536,32 @@ class Cuoral {
|
|
|
1248
1536
|
this.intelligence.trackError(message, stackTrace, metadata);
|
|
1249
1537
|
}
|
|
1250
1538
|
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Start native screen recording programmatically
|
|
1541
|
+
* @returns Promise<boolean> - true if recording started successfully
|
|
1542
|
+
*/
|
|
1543
|
+
async startRecording() {
|
|
1544
|
+
try {
|
|
1545
|
+
return await this.recorder.startRecording();
|
|
1546
|
+
}
|
|
1547
|
+
catch (error) {
|
|
1548
|
+
console.error('[Cuoral] Failed to start recording:', error);
|
|
1549
|
+
return false;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Stop native screen recording programmatically
|
|
1554
|
+
* @returns Promise<{filePath?: string; duration?: number} | null> - Recording result or null if failed
|
|
1555
|
+
*/
|
|
1556
|
+
async stopRecording() {
|
|
1557
|
+
try {
|
|
1558
|
+
return await this.recorder.stopRecording();
|
|
1559
|
+
}
|
|
1560
|
+
catch (error) {
|
|
1561
|
+
console.error('[Cuoral] Failed to stop recording:', error);
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1251
1565
|
/**
|
|
1252
1566
|
* Open the widget modal
|
|
1253
1567
|
*/
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|