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 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
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
  */
@@ -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
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}