@checkflow/sdk 1.0.4 → 1.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.
@@ -69,10 +69,13 @@ export declare class AnalyticsTracker {
69
69
  private lastScrollTime;
70
70
  private isFirstPageView;
71
71
  private performanceObserver?;
72
+ private recordingUploadTimer;
73
+ private readonly RECORDING_UPLOAD_INTERVAL;
72
74
  constructor(apiClient: APIClient, options?: Partial<AnalyticsOptions>);
73
75
  startTracking(): Promise<void>;
74
76
  stopTracking(): Promise<void>;
75
77
  private saveSessionRecording;
78
+ private setupPeriodicRecordingUpload;
76
79
  private createSession;
77
80
  private updateSessionEnd;
78
81
  private setupEventListeners;
@@ -88,6 +91,7 @@ export declare class AnalyticsTracker {
88
91
  private setupSPATracking;
89
92
  private setupPerformanceTracking;
90
93
  private trackPageView;
94
+ private capturePageScreenshotForHeatmap;
91
95
  private trackPageLoadPerformance;
92
96
  private queueInteraction;
93
97
  private flushInteractions;
package/dist/index.esm.js CHANGED
@@ -608,14 +608,40 @@ async function captureScreenshot(options = {}) {
608
608
  }
609
609
  // Mask elements if specified
610
610
  const maskedElements = [];
611
- if (options.maskElements) {
612
- options.maskElements.forEach((selector) => {
613
- document.querySelectorAll(selector).forEach((el) => {
614
- maskedElements.push({ element: el, innerHTML: el.innerHTML });
615
- el.innerHTML = '••••••••';
616
- });
611
+ // Default sensitive selectors to mask
612
+ const defaultMaskSelectors = [
613
+ 'input[type="password"]',
614
+ 'input[type="email"]',
615
+ 'input[type="tel"]',
616
+ 'input[name*="password"]',
617
+ 'input[name*="email"]',
618
+ 'input[name*="phone"]',
619
+ 'input[placeholder*="password"]',
620
+ 'input[placeholder*="email"]',
621
+ 'textarea[name*="message"]',
622
+ '[data-sensitive]'
623
+ ];
624
+ const maskSelectors = [...defaultMaskSelectors, ...(options.maskElements || [])];
625
+ maskSelectors.forEach((selector) => {
626
+ document.querySelectorAll(selector).forEach((el) => {
627
+ maskedElements.push({ element: el, innerHTML: el.innerHTML });
628
+ // Mask based on element type
629
+ if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
630
+ const input = el;
631
+ if (input.value) {
632
+ input.setAttribute('data-original-value', input.value);
633
+ input.value = '*'.repeat(Math.min(input.value.length, 8));
634
+ }
635
+ }
636
+ else {
637
+ // Mask text content
638
+ const text = el.textContent || '';
639
+ if (text.trim()) {
640
+ el.innerHTML = '*'.repeat(Math.min(text.length, 10));
641
+ }
642
+ }
617
643
  });
618
- }
644
+ });
619
645
  // Wait for delay if specified
620
646
  if (options.delay) {
621
647
  await new Promise((resolve) => setTimeout(resolve, options.delay));
@@ -635,7 +661,17 @@ async function captureScreenshot(options = {}) {
635
661
  });
636
662
  // Restore masked elements
637
663
  maskedElements.forEach(({ element, innerHTML }) => {
638
- element.innerHTML = innerHTML;
664
+ if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
665
+ const input = element;
666
+ const originalValue = input.getAttribute('data-original-value');
667
+ if (originalValue) {
668
+ input.value = originalValue;
669
+ input.removeAttribute('data-original-value');
670
+ }
671
+ }
672
+ else {
673
+ element.innerHTML = innerHTML;
674
+ }
639
675
  });
640
676
  // Convert to base64
641
677
  const quality = (options.quality || 80) / 100;
@@ -723,6 +759,14 @@ class ContextCapture {
723
759
  }
724
760
  }
725
761
 
762
+ var contextCapture = /*#__PURE__*/Object.freeze({
763
+ __proto__: null,
764
+ ContextCapture: ContextCapture,
765
+ capturePageContext: capturePageContext,
766
+ capturePerformance: capturePerformance,
767
+ captureScreenshot: captureScreenshot
768
+ });
769
+
726
770
  /**
727
771
  * Error Capture Module
728
772
  * Automatic error capture and reporting
@@ -2898,7 +2942,7 @@ class SessionRecording {
2898
2942
  const isSensitive = this.isSensitiveElement(target);
2899
2943
  this.addEvent('input', {
2900
2944
  selector: this.getElementSelector(target),
2901
- value: isSensitive ? '••••••••' : target.value?.substring(0, 100),
2945
+ value: isSensitive ? '********' : target.value?.substring(0, 100),
2902
2946
  type: target.type || 'text',
2903
2947
  });
2904
2948
  };
@@ -3481,6 +3525,9 @@ class AnalyticsTracker {
3481
3525
  this.scrollEvents = 0;
3482
3526
  this.lastScrollTime = 0;
3483
3527
  this.isFirstPageView = true;
3528
+ // Session recording upload timer (every 10 minutes)
3529
+ this.recordingUploadTimer = null;
3530
+ this.RECORDING_UPLOAD_INTERVAL = 10 * 60 * 1000; // 10 minutes
3484
3531
  // ==================== Private Methods - Event Handlers ====================
3485
3532
  this.handleClick = (event) => {
3486
3533
  const target = event.target;
@@ -3562,6 +3609,7 @@ class AnalyticsTracker {
3562
3609
  this.queueInteraction(interaction);
3563
3610
  };
3564
3611
  this.handlePageUnload = () => {
3612
+ console.log('🎬 [AnalyticsTracker] Page unload detected - saving session recording and interactions');
3565
3613
  // Send any pending interactions synchronously
3566
3614
  if (this.interactionBuffer.length > 0) {
3567
3615
  // Use sendBeacon for reliable delivery
@@ -3571,11 +3619,51 @@ class AnalyticsTracker {
3571
3619
  navigator.sendBeacon(url, data);
3572
3620
  }
3573
3621
  }
3622
+ // Save session recording synchronously before page unloads
3623
+ try {
3624
+ const checkflowInstance = window.checkflow;
3625
+ if (checkflowInstance?.sessionRecording) {
3626
+ const recordingData = checkflowInstance.sessionRecording.getRecordingData();
3627
+ if (recordingData && recordingData.events.length > 0) {
3628
+ console.log('🎬 [AnalyticsTracker] Sending session recording via sendBeacon', {
3629
+ eventCount: recordingData.events.length,
3630
+ duration: recordingData.duration
3631
+ });
3632
+ // Send recording data via sendBeacon
3633
+ const recordingPayload = JSON.stringify({
3634
+ events: recordingData.events,
3635
+ durationMs: recordingData.duration * 1000
3636
+ });
3637
+ const recordingUrl = `${this.apiClient.getBaseUrl()}/api/v1/analytics/sessions/${this.sessionId}/recording`;
3638
+ if (navigator.sendBeacon) {
3639
+ const success = navigator.sendBeacon(recordingUrl, recordingPayload);
3640
+ console.log('🎬 [AnalyticsTracker] sendBeacon recording result:', success);
3641
+ }
3642
+ }
3643
+ else {
3644
+ console.warn('🎬 [AnalyticsTracker] No recording events to save on unload');
3645
+ }
3646
+ }
3647
+ else {
3648
+ console.warn('🎬 [AnalyticsTracker] No session recording instance found on unload');
3649
+ }
3650
+ }
3651
+ catch (error) {
3652
+ console.error('🎬 [AnalyticsTracker] Failed to save recording on unload:', error);
3653
+ }
3574
3654
  };
3575
3655
  this.handleVisibilityChange = () => {
3576
3656
  if (document.hidden) {
3577
- // Page became hidden - flush interactions
3657
+ console.log('🎬 [AnalyticsTracker] Tab became hidden - saving data immediately');
3658
+ // Page became hidden - flush interactions AND save session recording
3578
3659
  this.flushInteractions();
3660
+ // Save session recording immediately when tab becomes hidden
3661
+ this.saveSessionRecording().catch(error => {
3662
+ console.warn('🎬 [AnalyticsTracker] Failed to save recording on visibility change:', error);
3663
+ });
3664
+ }
3665
+ else {
3666
+ console.log('🎬 [AnalyticsTracker] Tab became visible again');
3579
3667
  }
3580
3668
  };
3581
3669
  this.apiClient = apiClient;
@@ -3610,6 +3698,8 @@ class AnalyticsTracker {
3610
3698
  this.setupEventListeners();
3611
3699
  // Track initial page view
3612
3700
  this.trackPageView();
3701
+ // Start periodic session recording upload timer (every 10 minutes)
3702
+ this.setupPeriodicRecordingUpload();
3613
3703
  this.isActive = true;
3614
3704
  this.log('Analytics tracking started', { sessionId: this.sessionId });
3615
3705
  }
@@ -3635,6 +3725,10 @@ class AnalyticsTracker {
3635
3725
  clearTimeout(this.batchTimer);
3636
3726
  this.batchTimer = null;
3637
3727
  }
3728
+ if (this.recordingUploadTimer) {
3729
+ clearInterval(this.recordingUploadTimer);
3730
+ this.recordingUploadTimer = null;
3731
+ }
3638
3732
  // Stop performance observer
3639
3733
  if (this.performanceObserver) {
3640
3734
  this.performanceObserver.disconnect();
@@ -3697,6 +3791,24 @@ class AnalyticsTracker {
3697
3791
  }
3698
3792
  }
3699
3793
  // ==================== Private Methods - Session Management ====================
3794
+ setupPeriodicRecordingUpload() {
3795
+ console.log('🎬 [AnalyticsTracker] Setting up periodic recording upload (every 10 minutes)');
3796
+ this.recordingUploadTimer = window.setInterval(async () => {
3797
+ try {
3798
+ console.log('🎬 [AnalyticsTracker] Periodic upload triggered');
3799
+ await this.saveSessionRecording();
3800
+ // Clear events from recording to avoid duplicate uploads
3801
+ const checkflowInstance = window.checkflow;
3802
+ if (checkflowInstance?.sessionRecording) {
3803
+ checkflowInstance.sessionRecording.clearEvents();
3804
+ console.log('🎬 [AnalyticsTracker] Cleared recording events after upload');
3805
+ }
3806
+ }
3807
+ catch (error) {
3808
+ console.error('🎬 [AnalyticsTracker] Periodic recording upload failed:', error);
3809
+ }
3810
+ }, this.RECORDING_UPLOAD_INTERVAL);
3811
+ }
3700
3812
  async createSession() {
3701
3813
  const sessionData = {
3702
3814
  sessionId: this.sessionId,
@@ -3827,7 +3939,7 @@ class AnalyticsTracker {
3827
3939
  this.log('Performance tracking setup failed:', error);
3828
3940
  }
3829
3941
  }
3830
- trackPageView() {
3942
+ async trackPageView() {
3831
3943
  const loadTime = this.isFirstPageView ? this.getPageLoadTime() : undefined;
3832
3944
  const domReadyTime = this.isFirstPageView ? this.getDOMReadyTime() : undefined;
3833
3945
  const interaction = {
@@ -3841,6 +3953,30 @@ class AnalyticsTracker {
3841
3953
  this.isFirstPageView = false;
3842
3954
  // Reset scroll tracking for new page
3843
3955
  this.scrollEvents = 0;
3956
+ // Capture page screenshot for heatmap background (async, don't wait)
3957
+ this.capturePageScreenshotForHeatmap().catch(error => {
3958
+ console.warn('🖼️ [AnalyticsTracker] Page screenshot capture failed:', error);
3959
+ });
3960
+ }
3961
+ async capturePageScreenshotForHeatmap() {
3962
+ try {
3963
+ console.log('🖼️ [AnalyticsTracker] Capturing page screenshot for heatmap...');
3964
+ // Dynamic import of html2canvas to avoid bundle bloat
3965
+ const { captureScreenshot } = await Promise.resolve().then(function () { return contextCapture; });
3966
+ const screenshot = await captureScreenshot({
3967
+ quality: 60, // Lower quality for heatmap backgrounds
3968
+ fullPage: false, // Capture viewport only for consistency
3969
+ });
3970
+ if (screenshot) {
3971
+ // Upload screenshot to backend for heatmap association
3972
+ await this.apiClient.uploadScreenshotOnly(screenshot, window.location.href);
3973
+ console.log('🖼️ [AnalyticsTracker] Page screenshot uploaded for heatmap');
3974
+ }
3975
+ }
3976
+ catch (error) {
3977
+ // Don't throw - screenshot capture is optional for heatmaps
3978
+ console.warn('🖼️ [AnalyticsTracker] Could not capture page screenshot:', error);
3979
+ }
3844
3980
  }
3845
3981
  trackPageLoadPerformance(timing) {
3846
3982
  const loadTime = Math.round(timing.loadEventEnd - timing.loadEventStart);