cuoral-ionic 0.0.4 → 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 +131 -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
|
@@ -240,6 +240,65 @@ async login(email: string, firstName: string, lastName: string) {
|
|
|
240
240
|
- Create a new Cuoral instance for each user login
|
|
241
241
|
- Email, first name, and last name are all required for identified users
|
|
242
242
|
|
|
243
|
+
## Push Notifications
|
|
244
|
+
|
|
245
|
+
Cuoral integrates seamlessly with your existing push notification system using webhooks. When an agent sends a message, Cuoral sends a webhook to your backend, allowing you to send push notifications to users when the widget is closed.
|
|
246
|
+
|
|
247
|
+
**How it works:**
|
|
248
|
+
|
|
249
|
+
1. **Track widget state** in your app (open/closed)
|
|
250
|
+
2. **Configure webhooks** in your Cuoral dashboard
|
|
251
|
+
3. **Receive webhook** when agent sends message
|
|
252
|
+
4. **Send FCM/APNs push** if widget is closed
|
|
253
|
+
|
|
254
|
+
**Example Implementation:**
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
export class MyApp {
|
|
258
|
+
private isCuoralWidgetOpen = false;
|
|
259
|
+
|
|
260
|
+
// When user opens support
|
|
261
|
+
openSupport() {
|
|
262
|
+
this.isCuoralWidgetOpen = true;
|
|
263
|
+
this.cuoral.openModal();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// When user closes support
|
|
267
|
+
closeSupport() {
|
|
268
|
+
this.isCuoralWidgetOpen = false;
|
|
269
|
+
this.cuoral.closeModal();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Handle incoming push notifications (your existing FCM handler)
|
|
273
|
+
async handlePushNotification(message: any) {
|
|
274
|
+
if (message.data.type === 'cuoral_message') {
|
|
275
|
+
// Don't show notification if widget is already open
|
|
276
|
+
if (this.isCuoralWidgetOpen) {
|
|
277
|
+
return; // User already sees the message
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Show notification to user
|
|
281
|
+
await this.showNotification({
|
|
282
|
+
title: 'New message from Support',
|
|
283
|
+
body: message.data.text,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// When user taps notification, open widget
|
|
287
|
+
this.openSupport();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Setup:**
|
|
294
|
+
|
|
295
|
+
1. Configure your webhook URL in Cuoral Dashboard → Settings → Webhooks
|
|
296
|
+
2. Cuoral sends webhooks for all new messages
|
|
297
|
+
3. Your backend decides whether to send push based on your app's state
|
|
298
|
+
4. User taps notification → your app calls `cuoral.openModal()`
|
|
299
|
+
|
|
300
|
+
This approach works with your existing FCM/APNs setup - no additional SDK configuration needed!
|
|
301
|
+
|
|
243
302
|
## Configuration
|
|
244
303
|
|
|
245
304
|
### CuoralOptions
|
|
@@ -284,7 +343,19 @@ new Cuoral({
|
|
|
284
343
|
|
|
285
344
|
```typescript
|
|
286
345
|
// Initialize Cuoral
|
|
287
|
-
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>
|
|
288
359
|
|
|
289
360
|
// Get widget URL for iframe embedding
|
|
290
361
|
cuoral.getWidgetUrl(): string
|
|
@@ -298,10 +369,69 @@ cuoral.closeModal(): void
|
|
|
298
369
|
// Check if modal is open
|
|
299
370
|
cuoral.isModalOpen(): boolean
|
|
300
371
|
|
|
372
|
+
// Clear and end current session (call before logout)
|
|
373
|
+
cuoral.clearSession(): Promise<void>
|
|
374
|
+
|
|
301
375
|
// Clean up resources
|
|
302
376
|
cuoral.destroy(): void
|
|
303
377
|
```
|
|
304
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
|
+
|
|
305
435
|
## What Gets Handled Automatically
|
|
306
436
|
|
|
307
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|