cryptique-sdk 1.1.6 → 1.1.8

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/lib/cjs/index.js CHANGED
@@ -164,6 +164,21 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
164
164
  eip6963Providers: [] // EIP-6963 discovered wallet providers
165
165
  };
166
166
 
167
+ // Ready promise - resolves when SDK is fully initialized (allows consumers to await init)
168
+ let _readyPromiseResolve;
169
+ const _readyPromise = new Promise((resolve) => {
170
+ _readyPromiseResolve = resolve;
171
+ });
172
+
173
+ // Helper to wait for SDK ready with timeout (avoids hanging if init fails)
174
+ const _waitForReady = (timeoutMs = 5000) => {
175
+ if (runtimeState.isInitialized) return Promise.resolve();
176
+ return Promise.race([
177
+ _readyPromise,
178
+ new Promise((_, reject) => setTimeout(() => reject(new Error('SDK init timeout')), timeoutMs))
179
+ ]);
180
+ };
181
+
167
182
  // Initialize EIP-6963 wallet provider detection (if enabled)
168
183
  {
169
184
  // Listen for providers being announced
@@ -4299,6 +4314,10 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
4299
4314
  // - Proper use of sendBeacon vs fetch
4300
4315
  // ============================================================================
4301
4316
 
4317
+ // Throttle network error logs (e.g. "Failed to fetch" from ad blockers) - log at most once per 30s
4318
+ let _lastNetworkErrorLog = 0;
4319
+ const _NETWORK_ERROR_THROTTLE_MS = 30000;
4320
+
4302
4321
  /**
4303
4322
  * APIClient - Unified API communication
4304
4323
  *
@@ -4479,8 +4498,14 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
4479
4498
  // Don't retry aborted requests - they were intentionally cancelled
4480
4499
  return; // Silently return, don't throw
4481
4500
  }
4482
-
4483
- console.error('❌ Error sending data:', error);
4501
+
4502
+ // Throttle "Failed to fetch" logs (common with ad blockers, CORS, network issues)
4503
+ const isNetworkError = error.name === 'TypeError' && (error.message === 'Failed to fetch' || error.message?.includes('fetch'));
4504
+ const now = Date.now();
4505
+ if (isNetworkError && (now - _lastNetworkErrorLog) < _NETWORK_ERROR_THROTTLE_MS) ; else {
4506
+ if (isNetworkError) _lastNetworkErrorLog = now;
4507
+ console.error('❌ Error sending data:', error);
4508
+ }
4484
4509
 
4485
4510
  // Retry if retries > 0 (but not for abort errors)
4486
4511
  if (retries > 0) {
@@ -5887,6 +5912,12 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
5887
5912
 
5888
5913
  } catch (error) {
5889
5914
  console.error('Error initializing Cryptique SDK:', error);
5915
+ } finally {
5916
+ // Always resolve ready promise so awaiters don't hang (even if init failed)
5917
+ if (typeof _readyPromiseResolve === 'function') {
5918
+ _readyPromiseResolve();
5919
+ _readyPromiseResolve = null; // Prevent multiple resolves
5920
+ }
5890
5921
  }
5891
5922
  }
5892
5923
  };
@@ -6099,9 +6130,17 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6099
6130
  }
6100
6131
  }
6101
6132
 
6133
+ // Wait for SDK init if not ready (handles race conditions)
6102
6134
  if (!runtimeState.isInitialized) {
6103
- console.warn('⚠️ [Events] SDK not initialized, skipping auto event:', eventName);
6104
- return;
6135
+ try {
6136
+ await _waitForReady();
6137
+ } catch (e) {
6138
+ // Init timeout or failed - silently skip
6139
+ return;
6140
+ }
6141
+ if (!runtimeState.isInitialized) {
6142
+ return;
6143
+ }
6105
6144
  }
6106
6145
 
6107
6146
  // Get session from storage - it returns { id, userId, ... }
@@ -6676,8 +6715,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6676
6715
 
6677
6716
  /**
6678
6717
  * Setup auto events tracking
6718
+ * Waits for SDK initialization before proceeding (handles SPA navigation race)
6679
6719
  */
6680
- setup() {
6720
+ async setup() {
6681
6721
  // Check if auto events are enabled
6682
6722
  if (!CONFIG.AUTO_EVENTS.enabled) {
6683
6723
  return;
@@ -6693,6 +6733,17 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6693
6733
  }
6694
6734
 
6695
6735
  try {
6736
+ // Wait for SDK to be fully initialized (handles SPA nav before init completes)
6737
+ await _waitForReady().catch(() => {
6738
+ // Init failed or timed out - silently skip setup
6739
+ return;
6740
+ });
6741
+
6742
+ // Double-check after wait (init may have failed)
6743
+ if (!runtimeState.isInitialized) {
6744
+ return;
6745
+ }
6746
+
6696
6747
  // Track page views
6697
6748
  this.trackPageView();
6698
6749
 
@@ -6852,11 +6903,15 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6852
6903
  const isInteractive = isInteractiveElement(element);
6853
6904
 
6854
6905
  if (!isInteractive) {
6855
- // Capture coordinates before setTimeout
6906
+ // Capture coordinates and page context before setTimeout (for heatmaps)
6856
6907
  const clickX = event.clientX;
6857
6908
  const clickY = event.clientY;
6858
6909
  const clickElement = element;
6859
-
6910
+ const scrollX = window.scrollX != null ? window.scrollX : window.pageXOffset;
6911
+ const scrollY = window.scrollY != null ? window.scrollY : window.pageYOffset;
6912
+ const docHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
6913
+ const docWidth = Math.max(document.body.scrollWidth, document.documentElement.scrollWidth);
6914
+
6860
6915
  // Mark this click as potentially dead
6861
6916
  const clickId = `${now}_${Math.random().toString(36).substr(2, 9)}`;
6862
6917
  pendingDeadClicks.set(clickId, {
@@ -6866,7 +6921,13 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6866
6921
  timestamp: now,
6867
6922
  url: window.location.href,
6868
6923
  clickX,
6869
- clickY
6924
+ clickY,
6925
+ page_x: event.pageX,
6926
+ page_y: event.pageY,
6927
+ scroll_x: scrollX,
6928
+ scroll_y: scrollY,
6929
+ document_height: docHeight,
6930
+ document_width: docWidth
6870
6931
  });
6871
6932
 
6872
6933
  // Check after 1 second if navigation occurred or if it's still a dead click
@@ -6880,6 +6941,12 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6880
6941
 
6881
6942
  EventsManager.trackAutoEvent('dead_click', {
6882
6943
  click_coordinates: { x: pendingClick.clickX, y: pendingClick.clickY },
6944
+ page_x: pendingClick.page_x,
6945
+ page_y: pendingClick.page_y,
6946
+ scroll_x: pendingClick.scroll_x,
6947
+ scroll_y: pendingClick.scroll_y,
6948
+ document_height: pendingClick.document_height,
6949
+ document_width: pendingClick.document_width,
6883
6950
  element_area: clickElement.offsetWidth * clickElement.offsetHeight,
6884
6951
  element_category: pendingClick.elementCategory,
6885
6952
  element_has_onclick: !!clickElement.onclick,
@@ -6895,9 +6962,19 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6895
6962
  }, 1000);
6896
6963
  }
6897
6964
 
6898
- // Track regular click with enhanced data
6965
+ // Track regular click with enhanced data (viewport + page-relative for heatmaps)
6966
+ const scrollX = window.scrollX != null ? window.scrollX : window.pageXOffset;
6967
+ const scrollY = window.scrollY != null ? window.scrollY : window.pageYOffset;
6968
+ const docHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
6969
+ const docWidth = Math.max(document.body.scrollWidth, document.documentElement.scrollWidth);
6899
6970
  EventsManager.trackAutoEvent('element_click', {
6900
6971
  click_coordinates: { x: event.clientX, y: event.clientY },
6972
+ page_x: event.pageX,
6973
+ page_y: event.pageY,
6974
+ scroll_x: scrollX,
6975
+ scroll_y: scrollY,
6976
+ document_height: docHeight,
6977
+ document_width: docWidth,
6901
6978
  double_click: event.detail === 2,
6902
6979
  element_category: elementCategory
6903
6980
  }, elementData).catch(err => {
@@ -6922,11 +6999,12 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6922
6999
  clearTimeout(scrollTimeout);
6923
7000
 
6924
7001
  scrollTimeout = setTimeout(() => {
6925
- const scrollDepth = Math.round((window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100);
6926
-
7002
+ const maxScroll = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) - window.innerHeight;
7003
+ const scrollDepth = maxScroll <= 0 ? 100 : Math.round((window.scrollY / maxScroll) * 100);
7004
+
6927
7005
  if (scrollDepth > maxScrollDepth) {
6928
7006
  maxScrollDepth = scrollDepth;
6929
-
7007
+
6930
7008
  EventsManager.trackAutoEvent('page_scroll', {
6931
7009
  scroll_depth: scrollDepth,
6932
7010
  max_scroll_reached: maxScrollDepth,
@@ -7675,6 +7753,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
7675
7753
  ...window.Cryptique,
7676
7754
  version: CONFIG.VERSION,
7677
7755
 
7756
+ // Wait for SDK to be fully initialized (returns Promise)
7757
+ ready: () => _readyPromise,
7758
+
7678
7759
  // Events System
7679
7760
  track: EventsManager.trackEvent.bind(EventsManager),
7680
7761
  trackEvent: EventsManager.trackEvent.bind(EventsManager),
@@ -7899,7 +7980,7 @@ const CryptiqueSDK = {
7899
7980
  init(options = {}) {
7900
7981
  if (typeof window === 'undefined') {
7901
7982
  console.warn('Cryptique SDK requires a browser environment');
7902
- return null;
7983
+ return Promise.resolve(null);
7903
7984
  }
7904
7985
 
7905
7986
  if (!options.siteId) {
@@ -7940,21 +8021,28 @@ const CryptiqueSDK = {
7940
8021
  return null;
7941
8022
  };
7942
8023
 
7943
- // If SDK is already loaded, configure immediately
8024
+ // If SDK is already loaded, configure and return ready promise
7944
8025
  if (window.Cryptique) {
7945
- return checkSDK();
8026
+ checkSDK();
8027
+ // Return ready() so await Cryptique.init() waits for full initialization
8028
+ return window.Cryptique.ready ? window.Cryptique.ready() : Promise.resolve(window.Cryptique);
7946
8029
  }
7947
8030
 
7948
- // Otherwise, wait a bit for it to initialize
8031
+ // Otherwise, wait for SDK to load then configure and wait for ready
7949
8032
  return new Promise((resolve) => {
7950
8033
  const maxAttempts = 50;
7951
8034
  let attempts = 0;
7952
8035
  const interval = setInterval(() => {
7953
8036
  attempts++;
7954
8037
  const sdk = checkSDK();
7955
- if (sdk || attempts >= maxAttempts) {
8038
+ if (sdk) {
8039
+ clearInterval(interval);
8040
+ // Wait for full initialization
8041
+ const readyPromise = sdk.ready ? sdk.ready() : Promise.resolve(sdk);
8042
+ resolve(readyPromise);
8043
+ } else if (attempts >= maxAttempts) {
7956
8044
  clearInterval(interval);
7957
- resolve(sdk);
8045
+ resolve(null);
7958
8046
  }
7959
8047
  }, 100);
7960
8048
  });
@@ -7971,6 +8059,19 @@ const CryptiqueSDK = {
7971
8059
  return null;
7972
8060
  },
7973
8061
 
8062
+ /**
8063
+ * Wait for SDK to be fully initialized
8064
+ * Returns a Promise that resolves when initialization is complete
8065
+ * @returns {Promise<void>}
8066
+ */
8067
+ ready() {
8068
+ const instance = this.getInstance();
8069
+ if (instance && instance.ready) {
8070
+ return instance.ready();
8071
+ }
8072
+ return Promise.resolve();
8073
+ },
8074
+
7974
8075
  /**
7975
8076
  * Identify a user with a unique identifier
7976
8077
  *
package/lib/esm/index.js CHANGED
@@ -162,6 +162,21 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
162
162
  eip6963Providers: [] // EIP-6963 discovered wallet providers
163
163
  };
164
164
 
165
+ // Ready promise - resolves when SDK is fully initialized (allows consumers to await init)
166
+ let _readyPromiseResolve;
167
+ const _readyPromise = new Promise((resolve) => {
168
+ _readyPromiseResolve = resolve;
169
+ });
170
+
171
+ // Helper to wait for SDK ready with timeout (avoids hanging if init fails)
172
+ const _waitForReady = (timeoutMs = 5000) => {
173
+ if (runtimeState.isInitialized) return Promise.resolve();
174
+ return Promise.race([
175
+ _readyPromise,
176
+ new Promise((_, reject) => setTimeout(() => reject(new Error('SDK init timeout')), timeoutMs))
177
+ ]);
178
+ };
179
+
165
180
  // Initialize EIP-6963 wallet provider detection (if enabled)
166
181
  {
167
182
  // Listen for providers being announced
@@ -4297,6 +4312,10 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
4297
4312
  // - Proper use of sendBeacon vs fetch
4298
4313
  // ============================================================================
4299
4314
 
4315
+ // Throttle network error logs (e.g. "Failed to fetch" from ad blockers) - log at most once per 30s
4316
+ let _lastNetworkErrorLog = 0;
4317
+ const _NETWORK_ERROR_THROTTLE_MS = 30000;
4318
+
4300
4319
  /**
4301
4320
  * APIClient - Unified API communication
4302
4321
  *
@@ -4477,8 +4496,14 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
4477
4496
  // Don't retry aborted requests - they were intentionally cancelled
4478
4497
  return; // Silently return, don't throw
4479
4498
  }
4480
-
4481
- console.error('❌ Error sending data:', error);
4499
+
4500
+ // Throttle "Failed to fetch" logs (common with ad blockers, CORS, network issues)
4501
+ const isNetworkError = error.name === 'TypeError' && (error.message === 'Failed to fetch' || error.message?.includes('fetch'));
4502
+ const now = Date.now();
4503
+ if (isNetworkError && (now - _lastNetworkErrorLog) < _NETWORK_ERROR_THROTTLE_MS) ; else {
4504
+ if (isNetworkError) _lastNetworkErrorLog = now;
4505
+ console.error('❌ Error sending data:', error);
4506
+ }
4482
4507
 
4483
4508
  // Retry if retries > 0 (but not for abort errors)
4484
4509
  if (retries > 0) {
@@ -5885,6 +5910,12 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
5885
5910
 
5886
5911
  } catch (error) {
5887
5912
  console.error('Error initializing Cryptique SDK:', error);
5913
+ } finally {
5914
+ // Always resolve ready promise so awaiters don't hang (even if init failed)
5915
+ if (typeof _readyPromiseResolve === 'function') {
5916
+ _readyPromiseResolve();
5917
+ _readyPromiseResolve = null; // Prevent multiple resolves
5918
+ }
5888
5919
  }
5889
5920
  }
5890
5921
  };
@@ -6097,9 +6128,17 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6097
6128
  }
6098
6129
  }
6099
6130
 
6131
+ // Wait for SDK init if not ready (handles race conditions)
6100
6132
  if (!runtimeState.isInitialized) {
6101
- console.warn('⚠️ [Events] SDK not initialized, skipping auto event:', eventName);
6102
- return;
6133
+ try {
6134
+ await _waitForReady();
6135
+ } catch (e) {
6136
+ // Init timeout or failed - silently skip
6137
+ return;
6138
+ }
6139
+ if (!runtimeState.isInitialized) {
6140
+ return;
6141
+ }
6103
6142
  }
6104
6143
 
6105
6144
  // Get session from storage - it returns { id, userId, ... }
@@ -6674,8 +6713,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6674
6713
 
6675
6714
  /**
6676
6715
  * Setup auto events tracking
6716
+ * Waits for SDK initialization before proceeding (handles SPA navigation race)
6677
6717
  */
6678
- setup() {
6718
+ async setup() {
6679
6719
  // Check if auto events are enabled
6680
6720
  if (!CONFIG.AUTO_EVENTS.enabled) {
6681
6721
  return;
@@ -6691,6 +6731,17 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6691
6731
  }
6692
6732
 
6693
6733
  try {
6734
+ // Wait for SDK to be fully initialized (handles SPA nav before init completes)
6735
+ await _waitForReady().catch(() => {
6736
+ // Init failed or timed out - silently skip setup
6737
+ return;
6738
+ });
6739
+
6740
+ // Double-check after wait (init may have failed)
6741
+ if (!runtimeState.isInitialized) {
6742
+ return;
6743
+ }
6744
+
6694
6745
  // Track page views
6695
6746
  this.trackPageView();
6696
6747
 
@@ -6850,11 +6901,15 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6850
6901
  const isInteractive = isInteractiveElement(element);
6851
6902
 
6852
6903
  if (!isInteractive) {
6853
- // Capture coordinates before setTimeout
6904
+ // Capture coordinates and page context before setTimeout (for heatmaps)
6854
6905
  const clickX = event.clientX;
6855
6906
  const clickY = event.clientY;
6856
6907
  const clickElement = element;
6857
-
6908
+ const scrollX = window.scrollX != null ? window.scrollX : window.pageXOffset;
6909
+ const scrollY = window.scrollY != null ? window.scrollY : window.pageYOffset;
6910
+ const docHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
6911
+ const docWidth = Math.max(document.body.scrollWidth, document.documentElement.scrollWidth);
6912
+
6858
6913
  // Mark this click as potentially dead
6859
6914
  const clickId = `${now}_${Math.random().toString(36).substr(2, 9)}`;
6860
6915
  pendingDeadClicks.set(clickId, {
@@ -6864,7 +6919,13 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6864
6919
  timestamp: now,
6865
6920
  url: window.location.href,
6866
6921
  clickX,
6867
- clickY
6922
+ clickY,
6923
+ page_x: event.pageX,
6924
+ page_y: event.pageY,
6925
+ scroll_x: scrollX,
6926
+ scroll_y: scrollY,
6927
+ document_height: docHeight,
6928
+ document_width: docWidth
6868
6929
  });
6869
6930
 
6870
6931
  // Check after 1 second if navigation occurred or if it's still a dead click
@@ -6878,6 +6939,12 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6878
6939
 
6879
6940
  EventsManager.trackAutoEvent('dead_click', {
6880
6941
  click_coordinates: { x: pendingClick.clickX, y: pendingClick.clickY },
6942
+ page_x: pendingClick.page_x,
6943
+ page_y: pendingClick.page_y,
6944
+ scroll_x: pendingClick.scroll_x,
6945
+ scroll_y: pendingClick.scroll_y,
6946
+ document_height: pendingClick.document_height,
6947
+ document_width: pendingClick.document_width,
6881
6948
  element_area: clickElement.offsetWidth * clickElement.offsetHeight,
6882
6949
  element_category: pendingClick.elementCategory,
6883
6950
  element_has_onclick: !!clickElement.onclick,
@@ -6893,9 +6960,19 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6893
6960
  }, 1000);
6894
6961
  }
6895
6962
 
6896
- // Track regular click with enhanced data
6963
+ // Track regular click with enhanced data (viewport + page-relative for heatmaps)
6964
+ const scrollX = window.scrollX != null ? window.scrollX : window.pageXOffset;
6965
+ const scrollY = window.scrollY != null ? window.scrollY : window.pageYOffset;
6966
+ const docHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
6967
+ const docWidth = Math.max(document.body.scrollWidth, document.documentElement.scrollWidth);
6897
6968
  EventsManager.trackAutoEvent('element_click', {
6898
6969
  click_coordinates: { x: event.clientX, y: event.clientY },
6970
+ page_x: event.pageX,
6971
+ page_y: event.pageY,
6972
+ scroll_x: scrollX,
6973
+ scroll_y: scrollY,
6974
+ document_height: docHeight,
6975
+ document_width: docWidth,
6899
6976
  double_click: event.detail === 2,
6900
6977
  element_category: elementCategory
6901
6978
  }, elementData).catch(err => {
@@ -6920,11 +6997,12 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6920
6997
  clearTimeout(scrollTimeout);
6921
6998
 
6922
6999
  scrollTimeout = setTimeout(() => {
6923
- const scrollDepth = Math.round((window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100);
6924
-
7000
+ const maxScroll = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) - window.innerHeight;
7001
+ const scrollDepth = maxScroll <= 0 ? 100 : Math.round((window.scrollY / maxScroll) * 100);
7002
+
6925
7003
  if (scrollDepth > maxScrollDepth) {
6926
7004
  maxScrollDepth = scrollDepth;
6927
-
7005
+
6928
7006
  EventsManager.trackAutoEvent('page_scroll', {
6929
7007
  scroll_depth: scrollDepth,
6930
7008
  max_scroll_reached: maxScrollDepth,
@@ -7673,6 +7751,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
7673
7751
  ...window.Cryptique,
7674
7752
  version: CONFIG.VERSION,
7675
7753
 
7754
+ // Wait for SDK to be fully initialized (returns Promise)
7755
+ ready: () => _readyPromise,
7756
+
7676
7757
  // Events System
7677
7758
  track: EventsManager.trackEvent.bind(EventsManager),
7678
7759
  trackEvent: EventsManager.trackEvent.bind(EventsManager),
@@ -7897,7 +7978,7 @@ const CryptiqueSDK = {
7897
7978
  init(options = {}) {
7898
7979
  if (typeof window === 'undefined') {
7899
7980
  console.warn('Cryptique SDK requires a browser environment');
7900
- return null;
7981
+ return Promise.resolve(null);
7901
7982
  }
7902
7983
 
7903
7984
  if (!options.siteId) {
@@ -7938,21 +8019,28 @@ const CryptiqueSDK = {
7938
8019
  return null;
7939
8020
  };
7940
8021
 
7941
- // If SDK is already loaded, configure immediately
8022
+ // If SDK is already loaded, configure and return ready promise
7942
8023
  if (window.Cryptique) {
7943
- return checkSDK();
8024
+ checkSDK();
8025
+ // Return ready() so await Cryptique.init() waits for full initialization
8026
+ return window.Cryptique.ready ? window.Cryptique.ready() : Promise.resolve(window.Cryptique);
7944
8027
  }
7945
8028
 
7946
- // Otherwise, wait a bit for it to initialize
8029
+ // Otherwise, wait for SDK to load then configure and wait for ready
7947
8030
  return new Promise((resolve) => {
7948
8031
  const maxAttempts = 50;
7949
8032
  let attempts = 0;
7950
8033
  const interval = setInterval(() => {
7951
8034
  attempts++;
7952
8035
  const sdk = checkSDK();
7953
- if (sdk || attempts >= maxAttempts) {
8036
+ if (sdk) {
8037
+ clearInterval(interval);
8038
+ // Wait for full initialization
8039
+ const readyPromise = sdk.ready ? sdk.ready() : Promise.resolve(sdk);
8040
+ resolve(readyPromise);
8041
+ } else if (attempts >= maxAttempts) {
7954
8042
  clearInterval(interval);
7955
- resolve(sdk);
8043
+ resolve(null);
7956
8044
  }
7957
8045
  }, 100);
7958
8046
  });
@@ -7969,6 +8057,19 @@ const CryptiqueSDK = {
7969
8057
  return null;
7970
8058
  },
7971
8059
 
8060
+ /**
8061
+ * Wait for SDK to be fully initialized
8062
+ * Returns a Promise that resolves when initialization is complete
8063
+ * @returns {Promise<void>}
8064
+ */
8065
+ ready() {
8066
+ const instance = this.getInstance();
8067
+ if (instance && instance.ready) {
8068
+ return instance.ready();
8069
+ }
8070
+ return Promise.resolve();
8071
+ },
8072
+
7972
8073
  /**
7973
8074
  * Identify a user with a unique identifier
7974
8075
  *
@@ -99,14 +99,20 @@ export interface PeopleManager {
99
99
  export default class CryptiqueSDK {
100
100
  /**
101
101
  * Initialize the SDK programmatically
102
+ * Returns a Promise that resolves when the SDK is fully initialized
102
103
  */
103
- init(options: CryptiqueOptions): Promise<any> | any;
104
+ init(options: CryptiqueOptions): Promise<any>;
104
105
 
105
106
  /**
106
107
  * Get the SDK instance
107
108
  */
108
109
  getInstance(): any | null;
109
110
 
111
+ /**
112
+ * Wait for SDK to be fully initialized
113
+ */
114
+ ready(): Promise<void>;
115
+
110
116
  /**
111
117
  * Track a custom event
112
118
  */
package/lib/umd/index.js CHANGED
@@ -168,6 +168,21 @@
168
168
  eip6963Providers: [] // EIP-6963 discovered wallet providers
169
169
  };
170
170
 
171
+ // Ready promise - resolves when SDK is fully initialized (allows consumers to await init)
172
+ let _readyPromiseResolve;
173
+ const _readyPromise = new Promise((resolve) => {
174
+ _readyPromiseResolve = resolve;
175
+ });
176
+
177
+ // Helper to wait for SDK ready with timeout (avoids hanging if init fails)
178
+ const _waitForReady = (timeoutMs = 5000) => {
179
+ if (runtimeState.isInitialized) return Promise.resolve();
180
+ return Promise.race([
181
+ _readyPromise,
182
+ new Promise((_, reject) => setTimeout(() => reject(new Error('SDK init timeout')), timeoutMs))
183
+ ]);
184
+ };
185
+
171
186
  // Initialize EIP-6963 wallet provider detection (if enabled)
172
187
  {
173
188
  // Listen for providers being announced
@@ -4303,6 +4318,10 @@
4303
4318
  // - Proper use of sendBeacon vs fetch
4304
4319
  // ============================================================================
4305
4320
 
4321
+ // Throttle network error logs (e.g. "Failed to fetch" from ad blockers) - log at most once per 30s
4322
+ let _lastNetworkErrorLog = 0;
4323
+ const _NETWORK_ERROR_THROTTLE_MS = 30000;
4324
+
4306
4325
  /**
4307
4326
  * APIClient - Unified API communication
4308
4327
  *
@@ -4483,8 +4502,14 @@
4483
4502
  // Don't retry aborted requests - they were intentionally cancelled
4484
4503
  return; // Silently return, don't throw
4485
4504
  }
4486
-
4487
- console.error('❌ Error sending data:', error);
4505
+
4506
+ // Throttle "Failed to fetch" logs (common with ad blockers, CORS, network issues)
4507
+ const isNetworkError = error.name === 'TypeError' && (error.message === 'Failed to fetch' || error.message?.includes('fetch'));
4508
+ const now = Date.now();
4509
+ if (isNetworkError && (now - _lastNetworkErrorLog) < _NETWORK_ERROR_THROTTLE_MS) ; else {
4510
+ if (isNetworkError) _lastNetworkErrorLog = now;
4511
+ console.error('❌ Error sending data:', error);
4512
+ }
4488
4513
 
4489
4514
  // Retry if retries > 0 (but not for abort errors)
4490
4515
  if (retries > 0) {
@@ -5891,6 +5916,12 @@
5891
5916
 
5892
5917
  } catch (error) {
5893
5918
  console.error('Error initializing Cryptique SDK:', error);
5919
+ } finally {
5920
+ // Always resolve ready promise so awaiters don't hang (even if init failed)
5921
+ if (typeof _readyPromiseResolve === 'function') {
5922
+ _readyPromiseResolve();
5923
+ _readyPromiseResolve = null; // Prevent multiple resolves
5924
+ }
5894
5925
  }
5895
5926
  }
5896
5927
  };
@@ -6103,9 +6134,17 @@
6103
6134
  }
6104
6135
  }
6105
6136
 
6137
+ // Wait for SDK init if not ready (handles race conditions)
6106
6138
  if (!runtimeState.isInitialized) {
6107
- console.warn('⚠️ [Events] SDK not initialized, skipping auto event:', eventName);
6108
- return;
6139
+ try {
6140
+ await _waitForReady();
6141
+ } catch (e) {
6142
+ // Init timeout or failed - silently skip
6143
+ return;
6144
+ }
6145
+ if (!runtimeState.isInitialized) {
6146
+ return;
6147
+ }
6109
6148
  }
6110
6149
 
6111
6150
  // Get session from storage - it returns { id, userId, ... }
@@ -6680,8 +6719,9 @@
6680
6719
 
6681
6720
  /**
6682
6721
  * Setup auto events tracking
6722
+ * Waits for SDK initialization before proceeding (handles SPA navigation race)
6683
6723
  */
6684
- setup() {
6724
+ async setup() {
6685
6725
  // Check if auto events are enabled
6686
6726
  if (!CONFIG.AUTO_EVENTS.enabled) {
6687
6727
  return;
@@ -6697,6 +6737,17 @@
6697
6737
  }
6698
6738
 
6699
6739
  try {
6740
+ // Wait for SDK to be fully initialized (handles SPA nav before init completes)
6741
+ await _waitForReady().catch(() => {
6742
+ // Init failed or timed out - silently skip setup
6743
+ return;
6744
+ });
6745
+
6746
+ // Double-check after wait (init may have failed)
6747
+ if (!runtimeState.isInitialized) {
6748
+ return;
6749
+ }
6750
+
6700
6751
  // Track page views
6701
6752
  this.trackPageView();
6702
6753
 
@@ -6856,11 +6907,15 @@
6856
6907
  const isInteractive = isInteractiveElement(element);
6857
6908
 
6858
6909
  if (!isInteractive) {
6859
- // Capture coordinates before setTimeout
6910
+ // Capture coordinates and page context before setTimeout (for heatmaps)
6860
6911
  const clickX = event.clientX;
6861
6912
  const clickY = event.clientY;
6862
6913
  const clickElement = element;
6863
-
6914
+ const scrollX = window.scrollX != null ? window.scrollX : window.pageXOffset;
6915
+ const scrollY = window.scrollY != null ? window.scrollY : window.pageYOffset;
6916
+ const docHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
6917
+ const docWidth = Math.max(document.body.scrollWidth, document.documentElement.scrollWidth);
6918
+
6864
6919
  // Mark this click as potentially dead
6865
6920
  const clickId = `${now}_${Math.random().toString(36).substr(2, 9)}`;
6866
6921
  pendingDeadClicks.set(clickId, {
@@ -6870,7 +6925,13 @@
6870
6925
  timestamp: now,
6871
6926
  url: window.location.href,
6872
6927
  clickX,
6873
- clickY
6928
+ clickY,
6929
+ page_x: event.pageX,
6930
+ page_y: event.pageY,
6931
+ scroll_x: scrollX,
6932
+ scroll_y: scrollY,
6933
+ document_height: docHeight,
6934
+ document_width: docWidth
6874
6935
  });
6875
6936
 
6876
6937
  // Check after 1 second if navigation occurred or if it's still a dead click
@@ -6884,6 +6945,12 @@
6884
6945
 
6885
6946
  EventsManager.trackAutoEvent('dead_click', {
6886
6947
  click_coordinates: { x: pendingClick.clickX, y: pendingClick.clickY },
6948
+ page_x: pendingClick.page_x,
6949
+ page_y: pendingClick.page_y,
6950
+ scroll_x: pendingClick.scroll_x,
6951
+ scroll_y: pendingClick.scroll_y,
6952
+ document_height: pendingClick.document_height,
6953
+ document_width: pendingClick.document_width,
6887
6954
  element_area: clickElement.offsetWidth * clickElement.offsetHeight,
6888
6955
  element_category: pendingClick.elementCategory,
6889
6956
  element_has_onclick: !!clickElement.onclick,
@@ -6899,9 +6966,19 @@
6899
6966
  }, 1000);
6900
6967
  }
6901
6968
 
6902
- // Track regular click with enhanced data
6969
+ // Track regular click with enhanced data (viewport + page-relative for heatmaps)
6970
+ const scrollX = window.scrollX != null ? window.scrollX : window.pageXOffset;
6971
+ const scrollY = window.scrollY != null ? window.scrollY : window.pageYOffset;
6972
+ const docHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
6973
+ const docWidth = Math.max(document.body.scrollWidth, document.documentElement.scrollWidth);
6903
6974
  EventsManager.trackAutoEvent('element_click', {
6904
6975
  click_coordinates: { x: event.clientX, y: event.clientY },
6976
+ page_x: event.pageX,
6977
+ page_y: event.pageY,
6978
+ scroll_x: scrollX,
6979
+ scroll_y: scrollY,
6980
+ document_height: docHeight,
6981
+ document_width: docWidth,
6905
6982
  double_click: event.detail === 2,
6906
6983
  element_category: elementCategory
6907
6984
  }, elementData).catch(err => {
@@ -6926,11 +7003,12 @@
6926
7003
  clearTimeout(scrollTimeout);
6927
7004
 
6928
7005
  scrollTimeout = setTimeout(() => {
6929
- const scrollDepth = Math.round((window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100);
6930
-
7006
+ const maxScroll = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) - window.innerHeight;
7007
+ const scrollDepth = maxScroll <= 0 ? 100 : Math.round((window.scrollY / maxScroll) * 100);
7008
+
6931
7009
  if (scrollDepth > maxScrollDepth) {
6932
7010
  maxScrollDepth = scrollDepth;
6933
-
7011
+
6934
7012
  EventsManager.trackAutoEvent('page_scroll', {
6935
7013
  scroll_depth: scrollDepth,
6936
7014
  max_scroll_reached: maxScrollDepth,
@@ -7679,6 +7757,9 @@
7679
7757
  ...window.Cryptique,
7680
7758
  version: CONFIG.VERSION,
7681
7759
 
7760
+ // Wait for SDK to be fully initialized (returns Promise)
7761
+ ready: () => _readyPromise,
7762
+
7682
7763
  // Events System
7683
7764
  track: EventsManager.trackEvent.bind(EventsManager),
7684
7765
  trackEvent: EventsManager.trackEvent.bind(EventsManager),
@@ -7903,7 +7984,7 @@
7903
7984
  init(options = {}) {
7904
7985
  if (typeof window === 'undefined') {
7905
7986
  console.warn('Cryptique SDK requires a browser environment');
7906
- return null;
7987
+ return Promise.resolve(null);
7907
7988
  }
7908
7989
 
7909
7990
  if (!options.siteId) {
@@ -7944,21 +8025,28 @@
7944
8025
  return null;
7945
8026
  };
7946
8027
 
7947
- // If SDK is already loaded, configure immediately
8028
+ // If SDK is already loaded, configure and return ready promise
7948
8029
  if (window.Cryptique) {
7949
- return checkSDK();
8030
+ checkSDK();
8031
+ // Return ready() so await Cryptique.init() waits for full initialization
8032
+ return window.Cryptique.ready ? window.Cryptique.ready() : Promise.resolve(window.Cryptique);
7950
8033
  }
7951
8034
 
7952
- // Otherwise, wait a bit for it to initialize
8035
+ // Otherwise, wait for SDK to load then configure and wait for ready
7953
8036
  return new Promise((resolve) => {
7954
8037
  const maxAttempts = 50;
7955
8038
  let attempts = 0;
7956
8039
  const interval = setInterval(() => {
7957
8040
  attempts++;
7958
8041
  const sdk = checkSDK();
7959
- if (sdk || attempts >= maxAttempts) {
8042
+ if (sdk) {
8043
+ clearInterval(interval);
8044
+ // Wait for full initialization
8045
+ const readyPromise = sdk.ready ? sdk.ready() : Promise.resolve(sdk);
8046
+ resolve(readyPromise);
8047
+ } else if (attempts >= maxAttempts) {
7960
8048
  clearInterval(interval);
7961
- resolve(sdk);
8049
+ resolve(null);
7962
8050
  }
7963
8051
  }, 100);
7964
8052
  });
@@ -7975,6 +8063,19 @@
7975
8063
  return null;
7976
8064
  },
7977
8065
 
8066
+ /**
8067
+ * Wait for SDK to be fully initialized
8068
+ * Returns a Promise that resolves when initialization is complete
8069
+ * @returns {Promise<void>}
8070
+ */
8071
+ ready() {
8072
+ const instance = this.getInstance();
8073
+ if (instance && instance.ready) {
8074
+ return instance.ready();
8075
+ }
8076
+ return Promise.resolve();
8077
+ },
8078
+
7978
8079
  /**
7979
8080
  * Identify a user with a unique identifier
7980
8081
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cryptique-sdk",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "type": "module",
5
5
  "description": "Cryptique Analytics SDK - Comprehensive web analytics and user tracking for modern web applications",
6
6
  "main": "lib/cjs/index.js",