cuoral-ionic 0.0.8 → 0.1.0
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/CuoralIonic.podspec +17 -0
- package/README.md +136 -6
- package/dist/cuoral.d.ts +1 -0
- package/dist/cuoral.d.ts.map +1 -1
- package/dist/cuoral.js +12 -5
- package/dist/index.esm.js +219 -18
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +219 -18
- package/dist/index.js.map +1 -1
- package/dist/intelligence.d.ts +8 -0
- package/dist/intelligence.d.ts.map +1 -1
- package/dist/intelligence.js +201 -8
- package/dist/modal.d.ts +2 -1
- package/dist/modal.d.ts.map +1 -1
- package/dist/modal.js +6 -5
- package/package.json +3 -2
- package/src/cuoral.ts +16 -5
- package/src/intelligence.ts +231 -8
- package/src/modal.ts +7 -5
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'CuoralIonic'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.license = package['license']
|
|
10
|
+
s.homepage = package['repository']['url']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
|
|
13
|
+
s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
|
|
14
|
+
s.ios.deployment_target = '13.0'
|
|
15
|
+
s.dependency 'Capacitor'
|
|
16
|
+
s.swift_version = '5.1'
|
|
17
|
+
end
|
package/README.md
CHANGED
|
@@ -57,6 +57,33 @@ Add to your `Info.plist`:
|
|
|
57
57
|
|
|
58
58
|
**Note:** Screen recording permission is requested automatically by iOS when recording starts. No additional permissions are needed for crash tracking or intelligence features.
|
|
59
59
|
|
|
60
|
+
### 📱 Google Play Store Declaration (Android Only)
|
|
61
|
+
|
|
62
|
+
**IMPORTANT:** When submitting your app to Google Play Store, you'll be asked to declare why your app uses `FOREGROUND_SERVICE_MEDIA_PROJECTION` (screen recording permission).
|
|
63
|
+
|
|
64
|
+
**What to Do:**
|
|
65
|
+
|
|
66
|
+
1. Go to Play Console → **Policy** → **App Content** → **Foreground Service Types**
|
|
67
|
+
2. Find `FOREGROUND_SERVICE_MEDIA_PROJECTION` → Click **Manage**
|
|
68
|
+
3. Select category: **Customer Support / Bug Reporting**
|
|
69
|
+
4. Provide justification:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
Our app uses screen recording exclusively for customer support purposes when users
|
|
73
|
+
initiate support requests. Users explicitly control when recording starts/stops via
|
|
74
|
+
the support widget. All recordings are transmitted securely to our support platform
|
|
75
|
+
to help resolve user-reported issues. No recording occurs without user action.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Why This is Required:**
|
|
79
|
+
|
|
80
|
+
- Screen recording is a "sensitive permission" that requires declaration
|
|
81
|
+
- Google wants to ensure it's used appropriately (which it is - user-initiated support)
|
|
82
|
+
- This is a simple form, not a special approval process
|
|
83
|
+
- No code changes needed - your app is already configured correctly
|
|
84
|
+
|
|
85
|
+
**Note:** iOS App Store does not require this declaration.
|
|
86
|
+
|
|
60
87
|
## Quick Start
|
|
61
88
|
|
|
62
89
|
### Option 1: Modal with Floating Button (Recommended)
|
|
@@ -313,9 +340,37 @@ interface CuoralOptions {
|
|
|
313
340
|
widgetBaseUrl?: string; // Optional: Custom widget URL (default: CDN)
|
|
314
341
|
showFloatingButton?: boolean; // Optional: Show floating chat button (default: true)
|
|
315
342
|
useModal?: boolean; // Optional: Use modal display mode (default: true)
|
|
343
|
+
primaryColor?: string; // Optional: Custom primary color (auto-fetched from backend if not provided)
|
|
316
344
|
}
|
|
317
345
|
```
|
|
318
346
|
|
|
347
|
+
### Branding & Colors
|
|
348
|
+
|
|
349
|
+
The floating button automatically uses your organization's brand color configured in the Cuoral dashboard. During `initialize()`, the SDK fetches your organization's configuration from the backend:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
// Automatic (Recommended):
|
|
353
|
+
const cuoral = new Cuoral({ publicKey: 'your-key' });
|
|
354
|
+
await cuoral.initialize(); // Fetches your brand color from backend
|
|
355
|
+
|
|
356
|
+
// Manual override (Optional):
|
|
357
|
+
const cuoral = new Cuoral({
|
|
358
|
+
publicKey: 'your-key',
|
|
359
|
+
primaryColor: '#FF5733', // Force specific color
|
|
360
|
+
});
|
|
361
|
+
await cuoral.initialize();
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**How it works:**
|
|
365
|
+
|
|
366
|
+
- SDK fetches session configuration (`POST /conversation/session/get`) during initialization
|
|
367
|
+
- Extracts `color` from `configuration` object in response
|
|
368
|
+
- Applies color to floating button background and shadows
|
|
369
|
+
- Falls back to default blue (#007AFF) if color not provided
|
|
370
|
+
|
|
371
|
+
**Configure your brand color:**
|
|
372
|
+
Go to Cuoral Dashboard → Settings → Branding → Primary Color
|
|
373
|
+
|
|
319
374
|
### Widget URL
|
|
320
375
|
|
|
321
376
|
By default, the widget loads from `https://js.cuoral.com/mobile.html` (production CDN).
|
|
@@ -406,6 +461,7 @@ async stopUserRecording() {
|
|
|
406
461
|
```
|
|
407
462
|
|
|
408
463
|
**Use Cases:**
|
|
464
|
+
|
|
409
465
|
- Allow users to record their issue before contacting support
|
|
410
466
|
- Implement custom recording UI in your app
|
|
411
467
|
- Record specific user flows programmatically
|
|
@@ -413,6 +469,81 @@ async stopUserRecording() {
|
|
|
413
469
|
|
|
414
470
|
**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
471
|
|
|
472
|
+
## Dual Session Capture
|
|
473
|
+
|
|
474
|
+
Cuoral provides **two complementary ways** to capture user sessions:
|
|
475
|
+
|
|
476
|
+
### 📹 Native Video Screen Recording (Mobile)
|
|
477
|
+
|
|
478
|
+
**When:** User explicitly taps "Record" button in widget or via programmatic API
|
|
479
|
+
|
|
480
|
+
✅ **What it captures:**
|
|
481
|
+
|
|
482
|
+
- Exact visual output (pixel-perfect replay)
|
|
483
|
+
- All UI elements including Shadow DOM components
|
|
484
|
+
- Animations, transitions, gestures
|
|
485
|
+
- Everything the user sees
|
|
486
|
+
|
|
487
|
+
📹 **File Size:** ~11 MB per minute (1.5 Mbps bitrate)
|
|
488
|
+
📤 **Upload:** Automatic to secure backend
|
|
489
|
+
🎯 **Best for:** Visual bug reproduction, UI issues, specific user flows
|
|
490
|
+
|
|
491
|
+
### 🎬 DOM Replay Recording (rrweb - Always On)
|
|
492
|
+
|
|
493
|
+
**When:** Automatically running in background during entire session
|
|
494
|
+
|
|
495
|
+
✅ **What it captures:**
|
|
496
|
+
|
|
497
|
+
- DOM structure and mutations
|
|
498
|
+
- User interactions (clicks, scrolls, inputs)
|
|
499
|
+
- Network requests and console errors
|
|
500
|
+
- Page navigation and state changes
|
|
501
|
+
- Custom events (rage clicks, form submissions)
|
|
502
|
+
|
|
503
|
+
💾 **Data Size:** Lightweight (~100-500 KB per session)
|
|
504
|
+
📤 **Upload:** Batched every 10 seconds
|
|
505
|
+
🎯 **Best for:** Understanding user journey, behavior analytics, session context
|
|
506
|
+
|
|
507
|
+
### 🔄 How They Work Together
|
|
508
|
+
|
|
509
|
+
Both features run **simultaneously** to give you complete visibility:
|
|
510
|
+
|
|
511
|
+
1. **DOM Replay (rrweb)** - Always recording in background, captures user journey and interactions
|
|
512
|
+
2. **Video Recording** - User-initiated, captures visual output when they hit an issue
|
|
513
|
+
|
|
514
|
+
**Example Workflow:**
|
|
515
|
+
|
|
516
|
+
- User navigates app → rrweb captures all interactions
|
|
517
|
+
- User hits a bug → User taps "Record" → Native video captures visual issue
|
|
518
|
+
- Support team sees both: The journey (rrweb) + The exact visual problem (video)
|
|
519
|
+
|
|
520
|
+
**Privacy:** DOM replay masks password inputs. Other fields captured for support context. Native video captures exact screen content when user explicitly records.
|
|
521
|
+
|
|
522
|
+
### 🎨 Shadow DOM Handling (Ionic Components)
|
|
523
|
+
|
|
524
|
+
**Problem:** Ionic uses Shadow DOM which rrweb cannot natively access.
|
|
525
|
+
**Solution:** The SDK automatically captures Shadow DOM content every 5 seconds.
|
|
526
|
+
|
|
527
|
+
**How it works:**
|
|
528
|
+
|
|
529
|
+
1. SDK scans all elements with Shadow DOM (ion-button, ion-content, etc.)
|
|
530
|
+
2. Serializes Shadow DOM innerHTML to `data-cuoral-shadow-*` attributes
|
|
531
|
+
3. rrweb captures these attributes (it can see regular attributes)
|
|
532
|
+
4. Your replay dashboard uses these attributes to reconstruct the UI
|
|
533
|
+
|
|
534
|
+
**To verify it's working:**
|
|
535
|
+
Open DevTools → Inspect any Ionic element → Should see `data-cuoral-shadow-html` attribute
|
|
536
|
+
|
|
537
|
+
**If pages still show black:**
|
|
538
|
+
See [SHADOW_DOM_CAPTURE_GUIDE.md](SHADOW_DOM_CAPTURE_GUIDE.md) for optimization tips:
|
|
539
|
+
|
|
540
|
+
- Use open Shadow DOM (not closed)
|
|
541
|
+
- Add data-content attributes to critical elements
|
|
542
|
+
- Use semantic CSS classes
|
|
543
|
+
- The guide includes code examples and troubleshooting
|
|
544
|
+
|
|
545
|
+
**Note:** Native video recording (user-initiated) always works perfectly - no Shadow DOM limitations.
|
|
546
|
+
|
|
416
547
|
### Manual Intelligence Tracking
|
|
417
548
|
|
|
418
549
|
Track custom events beyond automatic tracking:
|
|
@@ -421,15 +552,14 @@ Track custom events beyond automatic tracking:
|
|
|
421
552
|
// Track page/screen view
|
|
422
553
|
this.cuoral.trackPageView('/checkout', {
|
|
423
554
|
cart_items: 3,
|
|
424
|
-
total_value: 99.99
|
|
555
|
+
total_value: 99.99,
|
|
425
556
|
});
|
|
426
557
|
|
|
427
558
|
// Track error manually
|
|
428
|
-
this.cuoral.trackError(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
);
|
|
559
|
+
this.cuoral.trackError('Payment failed', error.stack, {
|
|
560
|
+
payment_method: 'credit_card',
|
|
561
|
+
amount: 99.99,
|
|
562
|
+
});
|
|
433
563
|
```
|
|
434
564
|
|
|
435
565
|
## What Gets Handled Automatically
|
package/dist/cuoral.d.ts
CHANGED
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;
|
|
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;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAkBD;;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;IAyDlC;;OAEG;IACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BxC;;OAEG;YACW,sBAAsB;IAoDpC;;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;;;;;OAKG;IACU,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA0C7E;;;OAGG;IACU,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC;IAS/C;;;;OAIG;IACU,aAAa,IAAI,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAC,GAAG,IAAI,CAAC;IAiBxG;;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;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAsB5B;;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;CAyG7B"}
|
package/dist/cuoral.js
CHANGED
|
@@ -39,10 +39,7 @@ export class Cuoral {
|
|
|
39
39
|
if (options.lastName)
|
|
40
40
|
params.set('last_name', options.lastName);
|
|
41
41
|
const widgetUrl = `${baseUrl}?${params.toString()}`;
|
|
42
|
-
//
|
|
43
|
-
if (this.options.useModal) {
|
|
44
|
-
this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton);
|
|
45
|
-
}
|
|
42
|
+
// Note: Modal will be created in initialize() after fetching primary color from backend
|
|
46
43
|
// Initialize bridge and recorder
|
|
47
44
|
this.bridge = new CuoralBridge({
|
|
48
45
|
widgetUrl,
|
|
@@ -63,6 +60,7 @@ export class Cuoral {
|
|
|
63
60
|
*/
|
|
64
61
|
async initialize() {
|
|
65
62
|
// Fetch session configuration and initialize intelligence if enabled by backend
|
|
63
|
+
// This also fetches the organization's primary color
|
|
66
64
|
await this.initializeIntelligence();
|
|
67
65
|
console.log('[Cuoral] Initialize - Session ID:', localStorage.getItem('__x_loadID'));
|
|
68
66
|
// Setup localStorage listener to detect when widget changes session
|
|
@@ -71,7 +69,7 @@ export class Cuoral {
|
|
|
71
69
|
// Recreate modal if it was destroyed (e.g., after clearSession)
|
|
72
70
|
if (this.options.useModal && !this.modal) {
|
|
73
71
|
const widgetUrl = this.getWidgetUrl();
|
|
74
|
-
this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton);
|
|
72
|
+
this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton, this.options.primaryColor);
|
|
75
73
|
}
|
|
76
74
|
// Update modal URL with session ID
|
|
77
75
|
if (this.modal) {
|
|
@@ -96,6 +94,15 @@ export class Cuoral {
|
|
|
96
94
|
}
|
|
97
95
|
// Fetch session configuration
|
|
98
96
|
const config = await this.fetchSessionConfiguration(sessionId);
|
|
97
|
+
// Extract organization's primary color from config if available
|
|
98
|
+
if (!this.options.primaryColor && config?.color) {
|
|
99
|
+
this.options.primaryColor = config.color;
|
|
100
|
+
console.log('[Cuoral] Using organization primary color:', this.options.primaryColor);
|
|
101
|
+
}
|
|
102
|
+
else if (!this.options.primaryColor) {
|
|
103
|
+
this.options.primaryColor = '#007AFF';
|
|
104
|
+
console.log('[Cuoral] Using default primary color');
|
|
105
|
+
}
|
|
99
106
|
// If session was invalid/expired, try to initiate a new one
|
|
100
107
|
if (!config && !localStorage.getItem('__x_loadID')) {
|
|
101
108
|
sessionId = await this.initiateSession();
|
package/dist/index.esm.js
CHANGED
|
@@ -361,10 +361,11 @@ class CuoralBridge {
|
|
|
361
361
|
* Handles full-screen modal display with floating button
|
|
362
362
|
*/
|
|
363
363
|
class CuoralModal {
|
|
364
|
-
constructor(widgetUrl, showFloatingButton = true) {
|
|
364
|
+
constructor(widgetUrl, showFloatingButton = true, primaryColor = '#007AFF') {
|
|
365
365
|
this.isOpen = false;
|
|
366
366
|
this.widgetUrl = widgetUrl;
|
|
367
367
|
this.showFloatingButton = showFloatingButton;
|
|
368
|
+
this.primaryColor = primaryColor;
|
|
368
369
|
}
|
|
369
370
|
/**
|
|
370
371
|
* Update the widget URL (e.g., to include new session_id)
|
|
@@ -432,12 +433,12 @@ class CuoralModal {
|
|
|
432
433
|
width: '56px',
|
|
433
434
|
height: '56px',
|
|
434
435
|
borderRadius: '50%',
|
|
435
|
-
backgroundColor:
|
|
436
|
+
backgroundColor: this.primaryColor,
|
|
436
437
|
display: 'flex',
|
|
437
438
|
alignItems: 'center',
|
|
438
439
|
justifyContent: 'center',
|
|
439
440
|
cursor: 'pointer',
|
|
440
|
-
boxShadow:
|
|
441
|
+
boxShadow: `0 4px 12px ${this.primaryColor}66`, // 40% opacity
|
|
441
442
|
zIndex: '999999',
|
|
442
443
|
transition: 'transform 0.2s, box-shadow 0.2s'
|
|
443
444
|
});
|
|
@@ -445,13 +446,13 @@ class CuoralModal {
|
|
|
445
446
|
this.floatingButton.addEventListener('mouseenter', () => {
|
|
446
447
|
if (this.floatingButton) {
|
|
447
448
|
this.floatingButton.style.transform = 'scale(1.1)';
|
|
448
|
-
this.floatingButton.style.boxShadow =
|
|
449
|
+
this.floatingButton.style.boxShadow = `0 6px 16px ${this.primaryColor}80`; // 50% opacity
|
|
449
450
|
}
|
|
450
451
|
});
|
|
451
452
|
this.floatingButton.addEventListener('mouseleave', () => {
|
|
452
453
|
if (this.floatingButton) {
|
|
453
454
|
this.floatingButton.style.transform = 'scale(1)';
|
|
454
|
-
this.floatingButton.style.boxShadow =
|
|
455
|
+
this.floatingButton.style.boxShadow = `0 4px 12px ${this.primaryColor}66`; // 40% opacity
|
|
455
456
|
}
|
|
456
457
|
});
|
|
457
458
|
// Click to open modal
|
|
@@ -650,11 +651,13 @@ class CuoralIntelligence {
|
|
|
650
651
|
// Session replay state
|
|
651
652
|
this.rrwebStopFn = null;
|
|
652
653
|
this.rrwebEvents = [];
|
|
654
|
+
this.rrwebEmit = null; // Store emit function for custom events
|
|
653
655
|
this.customEvents = [];
|
|
654
656
|
this.sessionReplayTimer = null;
|
|
655
657
|
this.clickTimestamps = new Map();
|
|
656
658
|
this.rageClickThreshold = 5; // 5 rapid clicks
|
|
657
659
|
this.rageClickWindowMs = 2000; // Within 2 seconds
|
|
660
|
+
this.shadowDOMNodeMap = new Map(); // Map elements to unique IDs
|
|
658
661
|
this.sessionId = sessionId;
|
|
659
662
|
}
|
|
660
663
|
/**
|
|
@@ -1164,27 +1167,65 @@ class CuoralIntelligence {
|
|
|
1164
1167
|
}
|
|
1165
1168
|
/**
|
|
1166
1169
|
* Setup session replay with rrweb
|
|
1170
|
+
* Captures everything for maximum replay fidelity
|
|
1167
1171
|
*/
|
|
1168
1172
|
setupSessionReplay() {
|
|
1169
1173
|
if (!this.sessionId) {
|
|
1170
1174
|
console.warn('[Cuoral Intelligence] Session replay requires a session ID');
|
|
1171
1175
|
return;
|
|
1172
1176
|
}
|
|
1173
|
-
// Start rrweb recording
|
|
1177
|
+
// Start rrweb recording with aggressive capture settings
|
|
1174
1178
|
this.rrwebStopFn = rrweb.record({
|
|
1175
1179
|
emit: (event) => {
|
|
1176
1180
|
this.rrwebEvents.push(event);
|
|
1181
|
+
this.rrwebEmit = this.rrwebEmit || ((e) => this.rrwebEvents.push(e)); // Store emit reference
|
|
1177
1182
|
},
|
|
1178
|
-
|
|
1183
|
+
// Take full snapshots more frequently to catch dynamic content
|
|
1184
|
+
checkoutEveryNms: 30000, // Every 30 seconds (was 60s)
|
|
1185
|
+
// Capture everything - minimize sampling throttling
|
|
1179
1186
|
sampling: {
|
|
1180
|
-
scroll:
|
|
1181
|
-
media: 800
|
|
1182
|
-
input: 'last', // Only record final input value (privacy)
|
|
1187
|
+
scroll: 50, // Capture scroll very frequently (was 150)
|
|
1188
|
+
media: 200, // Capture media interactions frequently (was 800)
|
|
1189
|
+
input: 'last', // Only record final input value (privacy for passwords)
|
|
1190
|
+
mousemove: true, // Capture all mouse movements
|
|
1191
|
+
mouseInteraction: true, // Capture all mouse interactions
|
|
1192
|
+
},
|
|
1193
|
+
// Privacy: Only mask sensitive inputs (passwords, credit cards)
|
|
1194
|
+
// Everything else is captured for maximum replay fidelity
|
|
1195
|
+
maskAllInputs: false, // Don't mask all inputs
|
|
1196
|
+
maskInputOptions: {
|
|
1197
|
+
password: true, // Mask password fields
|
|
1198
|
+
email: false, // Capture emails
|
|
1199
|
+
text: false, // Capture text inputs
|
|
1200
|
+
textarea: false, // Capture textareas
|
|
1201
|
+
select: false, // Capture select dropdowns
|
|
1202
|
+
// Mask credit card patterns
|
|
1203
|
+
},
|
|
1204
|
+
// Capture all content types
|
|
1205
|
+
recordCanvas: true, // Capture canvas elements (charts, games, etc.)
|
|
1206
|
+
// Inline everything for perfect replay
|
|
1207
|
+
inlineStylesheet: true, // Inline all external stylesheets
|
|
1208
|
+
inlineImages: true, // Inline images as base64 (larger but more reliable)
|
|
1209
|
+
collectFonts: true, // Capture custom fonts
|
|
1210
|
+
// Block/ignore classes - customers can add these to sensitive elements
|
|
1211
|
+
blockClass: 'cuoral-block', // Add to elements that should be blocked (replaced with placeholder)
|
|
1212
|
+
ignoreClass: 'cuoral-ignore', // Add to elements that should be ignored (not captured)
|
|
1213
|
+
// Capture mutations aggressively
|
|
1214
|
+
slimDOMOptions: {
|
|
1215
|
+
// Don't slim anything - capture full fidelity
|
|
1216
|
+
script: false, // Keep scripts
|
|
1217
|
+
comment: false, // Keep comments
|
|
1218
|
+
headFavicon: false, // Keep favicons
|
|
1219
|
+
headWhitespace: false, // Keep whitespace
|
|
1220
|
+
headMetaSocial: false, // Keep meta tags
|
|
1221
|
+
headMetaRobots: false, // Keep robots meta
|
|
1222
|
+
headMetaHttpEquiv: false, // Keep http-equiv
|
|
1223
|
+
headMetaAuthorship: false, // Keep authorship
|
|
1224
|
+
headMetaDescKeywords: false, // Keep description/keywords
|
|
1183
1225
|
},
|
|
1184
|
-
maskAllInputs: true, // Mask sensitive inputs (privacy)
|
|
1185
|
-
blockClass: 'cuoral-block',
|
|
1186
|
-
ignoreClass: 'cuoral-ignore',
|
|
1187
1226
|
});
|
|
1227
|
+
// Setup Shadow DOM observer to capture Ionic components
|
|
1228
|
+
this.setupShadowDOMObserver();
|
|
1188
1229
|
// Setup custom event tracking
|
|
1189
1230
|
this.setupClickTracking();
|
|
1190
1231
|
this.setupScrollTracking();
|
|
@@ -1336,6 +1377,159 @@ class CuoralIntelligence {
|
|
|
1336
1377
|
});
|
|
1337
1378
|
});
|
|
1338
1379
|
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Setup Shadow DOM observer to capture Ionic component content
|
|
1382
|
+
* Emits custom rrweb events with Shadow DOM snapshots for viewer reconstruction
|
|
1383
|
+
*/
|
|
1384
|
+
setupShadowDOMObserver() {
|
|
1385
|
+
let shadowNodeIdCounter = 1;
|
|
1386
|
+
let captureTimeout = null;
|
|
1387
|
+
const captureShadowDOM = (immediate = false) => {
|
|
1388
|
+
// Debounce rapid calls (e.g., during scroll)
|
|
1389
|
+
if (!immediate && captureTimeout) {
|
|
1390
|
+
clearTimeout(captureTimeout);
|
|
1391
|
+
}
|
|
1392
|
+
const doCapture = () => {
|
|
1393
|
+
if (!this.rrwebEmit)
|
|
1394
|
+
return;
|
|
1395
|
+
const shadowDOMData = [];
|
|
1396
|
+
const elementsWithShadowDOM = document.querySelectorAll('*');
|
|
1397
|
+
elementsWithShadowDOM.forEach((element) => {
|
|
1398
|
+
if (element.shadowRoot) {
|
|
1399
|
+
try {
|
|
1400
|
+
// Assign unique ID to element if it doesn't have one
|
|
1401
|
+
if (!this.shadowDOMNodeMap.has(element)) {
|
|
1402
|
+
this.shadowDOMNodeMap.set(element, shadowNodeIdCounter++);
|
|
1403
|
+
}
|
|
1404
|
+
const nodeId = this.shadowDOMNodeMap.get(element);
|
|
1405
|
+
const tagName = element.tagName.toLowerCase();
|
|
1406
|
+
// Capture Shadow DOM HTML
|
|
1407
|
+
const shadowHTML = element.shadowRoot.innerHTML;
|
|
1408
|
+
// Capture ALL styles from Shadow DOM
|
|
1409
|
+
const styleSheets = [];
|
|
1410
|
+
// 1. Capture inline <style> tags
|
|
1411
|
+
const shadowStyles = element.shadowRoot.querySelectorAll('style');
|
|
1412
|
+
shadowStyles.forEach((styleEl) => {
|
|
1413
|
+
if (styleEl.textContent) {
|
|
1414
|
+
styleSheets.push(styleEl.textContent);
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
// 2. Capture adoptedStyleSheets (modern CSS API)
|
|
1418
|
+
if (element.shadowRoot.adoptedStyleSheets) {
|
|
1419
|
+
try {
|
|
1420
|
+
element.shadowRoot.adoptedStyleSheets.forEach((sheet) => {
|
|
1421
|
+
try {
|
|
1422
|
+
const rules = Array.from(sheet.cssRules).map(rule => rule.cssText).join('\n');
|
|
1423
|
+
if (rules)
|
|
1424
|
+
styleSheets.push(rules);
|
|
1425
|
+
}
|
|
1426
|
+
catch (e) {
|
|
1427
|
+
// CORS blocked stylesheet
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
catch (e) {
|
|
1432
|
+
// Browser doesn't support adoptedStyleSheets
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
// 3. Capture external <link> stylesheets in Shadow DOM
|
|
1436
|
+
const shadowLinks = element.shadowRoot.querySelectorAll('link[rel="stylesheet"]');
|
|
1437
|
+
shadowLinks.forEach((linkEl) => {
|
|
1438
|
+
const href = linkEl.href;
|
|
1439
|
+
if (href) {
|
|
1440
|
+
// Store link href so dashboard can also load it
|
|
1441
|
+
styleSheets.push(`/* @import url("${href}"); */`);
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
// 4. Capture CSS custom properties from host element
|
|
1445
|
+
// Ionic components use CSS variables extensively (--ion-color-*, etc.)
|
|
1446
|
+
const computedStyle = window.getComputedStyle(element);
|
|
1447
|
+
const cssVariables = [];
|
|
1448
|
+
// Get all CSS custom properties from host
|
|
1449
|
+
for (let i = 0; i < computedStyle.length; i++) {
|
|
1450
|
+
const propertyName = computedStyle[i];
|
|
1451
|
+
if (propertyName.startsWith('--')) {
|
|
1452
|
+
const propertyValue = computedStyle.getPropertyValue(propertyName).trim();
|
|
1453
|
+
if (propertyValue) {
|
|
1454
|
+
cssVariables.push(`${propertyName}: ${propertyValue};`);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
// Add CSS variables as a :host rule
|
|
1459
|
+
if (cssVariables.length > 0) {
|
|
1460
|
+
styleSheets.push(`:host { ${cssVariables.join(' ')} }`);
|
|
1461
|
+
}
|
|
1462
|
+
// Get element's CSS selector for reconstruction
|
|
1463
|
+
const selector = this.getElementSelector(element);
|
|
1464
|
+
// Capture element attributes (Ionic uses color="primary", mode="ios", etc.)
|
|
1465
|
+
const attributes = {};
|
|
1466
|
+
for (let i = 0; i < element.attributes.length; i++) {
|
|
1467
|
+
const attr = element.attributes[i];
|
|
1468
|
+
// Skip internal attributes
|
|
1469
|
+
if (!attr.name.startsWith('data-cuoral') && !attr.name.startsWith('ng-')) {
|
|
1470
|
+
attributes[attr.name] = attr.value;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
// Create Shadow DOM snapshot
|
|
1474
|
+
shadowDOMData.push({
|
|
1475
|
+
nodeId,
|
|
1476
|
+
tagName,
|
|
1477
|
+
selector,
|
|
1478
|
+
attributes, // NEW: Include element attributes
|
|
1479
|
+
shadowHTML,
|
|
1480
|
+
styleSheets,
|
|
1481
|
+
timestamp: Date.now(),
|
|
1482
|
+
});
|
|
1483
|
+
// Also set data attribute as fallback
|
|
1484
|
+
element.setAttribute('data-cuoral-shadow-id', String(nodeId));
|
|
1485
|
+
}
|
|
1486
|
+
catch (error) {
|
|
1487
|
+
// Shadow DOM might be closed
|
|
1488
|
+
console.warn('[Cuoral Intelligence] Failed to capture Shadow DOM:', error);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
// Emit custom event with Shadow DOM data
|
|
1493
|
+
if (shadowDOMData.length > 0 && this.rrwebEmit) {
|
|
1494
|
+
this.rrwebEmit({
|
|
1495
|
+
type: 5, // CustomEvent type in rrweb
|
|
1496
|
+
data: {
|
|
1497
|
+
tag: 'cuoral-shadow-dom',
|
|
1498
|
+
payload: {
|
|
1499
|
+
shadows: shadowDOMData,
|
|
1500
|
+
url: window.location.href,
|
|
1501
|
+
},
|
|
1502
|
+
},
|
|
1503
|
+
timestamp: Date.now(),
|
|
1504
|
+
});
|
|
1505
|
+
console.log(`[Cuoral Intelligence] Captured ${shadowDOMData.length} Shadow DOM elements`);
|
|
1506
|
+
}
|
|
1507
|
+
};
|
|
1508
|
+
if (immediate) {
|
|
1509
|
+
doCapture();
|
|
1510
|
+
}
|
|
1511
|
+
else {
|
|
1512
|
+
// Debounce: wait 500ms after last call
|
|
1513
|
+
captureTimeout = setTimeout(doCapture, 500);
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
// Run immediately on init
|
|
1517
|
+
captureShadowDOM(true);
|
|
1518
|
+
// Observe DOM mutations to catch dynamically added Shadow DOM elements
|
|
1519
|
+
const observer = new MutationObserver(() => {
|
|
1520
|
+
captureShadowDOM(false); // Debounced
|
|
1521
|
+
});
|
|
1522
|
+
observer.observe(document.body, {
|
|
1523
|
+
childList: true,
|
|
1524
|
+
subtree: true,
|
|
1525
|
+
});
|
|
1526
|
+
// Capture on scroll events (debounced)
|
|
1527
|
+
window.addEventListener('scroll', () => captureShadowDOM(false), { passive: true });
|
|
1528
|
+
// Also listen for Ionic scroll events
|
|
1529
|
+
document.addEventListener('ionScroll', () => captureShadowDOM(false), { passive: true });
|
|
1530
|
+
// Periodic capture as fallback (every 5 seconds now, since we have scroll capture)
|
|
1531
|
+
setInterval(() => captureShadowDOM(true), 5000);
|
|
1532
|
+
}
|
|
1339
1533
|
/**
|
|
1340
1534
|
* Track custom business events (flows, features, etc.)
|
|
1341
1535
|
*/
|
|
@@ -1464,10 +1658,7 @@ class Cuoral {
|
|
|
1464
1658
|
if (options.lastName)
|
|
1465
1659
|
params.set('last_name', options.lastName);
|
|
1466
1660
|
const widgetUrl = `${baseUrl}?${params.toString()}`;
|
|
1467
|
-
//
|
|
1468
|
-
if (this.options.useModal) {
|
|
1469
|
-
this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton);
|
|
1470
|
-
}
|
|
1661
|
+
// Note: Modal will be created in initialize() after fetching primary color from backend
|
|
1471
1662
|
// Initialize bridge and recorder
|
|
1472
1663
|
this.bridge = new CuoralBridge({
|
|
1473
1664
|
widgetUrl,
|
|
@@ -1488,6 +1679,7 @@ class Cuoral {
|
|
|
1488
1679
|
*/
|
|
1489
1680
|
async initialize() {
|
|
1490
1681
|
// Fetch session configuration and initialize intelligence if enabled by backend
|
|
1682
|
+
// This also fetches the organization's primary color
|
|
1491
1683
|
await this.initializeIntelligence();
|
|
1492
1684
|
console.log('[Cuoral] Initialize - Session ID:', localStorage.getItem('__x_loadID'));
|
|
1493
1685
|
// Setup localStorage listener to detect when widget changes session
|
|
@@ -1496,7 +1688,7 @@ class Cuoral {
|
|
|
1496
1688
|
// Recreate modal if it was destroyed (e.g., after clearSession)
|
|
1497
1689
|
if (this.options.useModal && !this.modal) {
|
|
1498
1690
|
const widgetUrl = this.getWidgetUrl();
|
|
1499
|
-
this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton);
|
|
1691
|
+
this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton, this.options.primaryColor);
|
|
1500
1692
|
}
|
|
1501
1693
|
// Update modal URL with session ID
|
|
1502
1694
|
if (this.modal) {
|
|
@@ -1521,6 +1713,15 @@ class Cuoral {
|
|
|
1521
1713
|
}
|
|
1522
1714
|
// Fetch session configuration
|
|
1523
1715
|
const config = await this.fetchSessionConfiguration(sessionId);
|
|
1716
|
+
// Extract organization's primary color from config if available
|
|
1717
|
+
if (!this.options.primaryColor && config?.color) {
|
|
1718
|
+
this.options.primaryColor = config.color;
|
|
1719
|
+
console.log('[Cuoral] Using organization primary color:', this.options.primaryColor);
|
|
1720
|
+
}
|
|
1721
|
+
else if (!this.options.primaryColor) {
|
|
1722
|
+
this.options.primaryColor = '#007AFF';
|
|
1723
|
+
console.log('[Cuoral] Using default primary color');
|
|
1724
|
+
}
|
|
1524
1725
|
// If session was invalid/expired, try to initiate a new one
|
|
1525
1726
|
if (!config && !localStorage.getItem('__x_loadID')) {
|
|
1526
1727
|
sessionId = await this.initiateSession();
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|