cryptique-sdk 1.1.5 → 1.1.7

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
@@ -3770,18 +3785,6 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
3770
3785
 
3771
3786
  // Save updated session
3772
3787
  StorageManager.saveSession(session);
3773
-
3774
- // Fire page_view auto event on every navigation (single source of truth with session tracking)
3775
- // This ensures SPAs get page_view on route changes without a separate history override
3776
- if (typeof CONFIG !== 'undefined' && CONFIG.AUTO_EVENTS && CONFIG.AUTO_EVENTS.enabled && typeof shouldTrackAutoEvents === 'function' && shouldTrackAutoEvents()) {
3777
- if (typeof AutoEventsTracker !== 'undefined') {
3778
- if (!AutoEventsTracker.isSetup) {
3779
- AutoEventsTracker.setup();
3780
- } else {
3781
- AutoEventsTracker.trackPageView();
3782
- }
3783
- }
3784
- }
3785
3788
  } catch (error) {
3786
3789
  console.error("Error tracking page visit:", error);
3787
3790
  }
@@ -4311,6 +4314,10 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
4311
4314
  // - Proper use of sendBeacon vs fetch
4312
4315
  // ============================================================================
4313
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
+
4314
4321
  /**
4315
4322
  * APIClient - Unified API communication
4316
4323
  *
@@ -4491,8 +4498,14 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
4491
4498
  // Don't retry aborted requests - they were intentionally cancelled
4492
4499
  return; // Silently return, don't throw
4493
4500
  }
4494
-
4495
- 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
+ }
4496
4509
 
4497
4510
  // Retry if retries > 0 (but not for abort errors)
4498
4511
  if (retries > 0) {
@@ -5899,6 +5912,12 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
5899
5912
 
5900
5913
  } catch (error) {
5901
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
+ }
5902
5921
  }
5903
5922
  }
5904
5923
  };
@@ -6111,9 +6130,17 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6111
6130
  }
6112
6131
  }
6113
6132
 
6133
+ // Wait for SDK init if not ready (handles race conditions)
6114
6134
  if (!runtimeState.isInitialized) {
6115
- console.warn('⚠️ [Events] SDK not initialized, skipping auto event:', eventName);
6116
- 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
+ }
6117
6144
  }
6118
6145
 
6119
6146
  // Get session from storage - it returns { id, userId, ... }
@@ -6688,8 +6715,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6688
6715
 
6689
6716
  /**
6690
6717
  * Setup auto events tracking
6718
+ * Waits for SDK initialization before proceeding (handles SPA navigation race)
6691
6719
  */
6692
- setup() {
6720
+ async setup() {
6693
6721
  // Check if auto events are enabled
6694
6722
  if (!CONFIG.AUTO_EVENTS.enabled) {
6695
6723
  return;
@@ -6705,6 +6733,17 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6705
6733
  }
6706
6734
 
6707
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
+
6708
6747
  // Track page views
6709
6748
  this.trackPageView();
6710
6749
 
@@ -7687,6 +7726,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
7687
7726
  ...window.Cryptique,
7688
7727
  version: CONFIG.VERSION,
7689
7728
 
7729
+ // Wait for SDK to be fully initialized (returns Promise)
7730
+ ready: () => _readyPromise,
7731
+
7690
7732
  // Events System
7691
7733
  track: EventsManager.trackEvent.bind(EventsManager),
7692
7734
  trackEvent: EventsManager.trackEvent.bind(EventsManager),
@@ -7817,6 +7859,46 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
7817
7859
  // Initialize SDK when DOM is ready
7818
7860
  // ============================================================================
7819
7861
 
7862
+ /**
7863
+ * Monitor path changes for Single Page Applications (SPAs)
7864
+ * Re-checks auto events configuration when navigation occurs
7865
+ */
7866
+ function setupSPANavigationMonitoring() {
7867
+ let lastPath = window.location.pathname;
7868
+
7869
+ // Check path changes
7870
+ function checkPathChange() {
7871
+ const currentPath = window.location.pathname;
7872
+ if (currentPath !== lastPath) {
7873
+ lastPath = currentPath;
7874
+
7875
+ // If auto events were disabled on previous page but enabled on new page, setup
7876
+ if (CONFIG.AUTO_EVENTS.enabled && shouldTrackAutoEvents() && !AutoEventsTracker.isSetup) {
7877
+ AutoEventsTracker.setup();
7878
+ }
7879
+ // Note: If auto events should be disabled on new page, trackAutoEvent will check
7880
+ // shouldTrackAutoEvents() before tracking, so no need to remove listeners
7881
+ }
7882
+ }
7883
+
7884
+ // Check on popstate (browser back/forward)
7885
+ window.addEventListener('popstate', checkPathChange);
7886
+
7887
+ // For SPAs using history.pushState/replaceState, monitor them
7888
+ const originalPushState = history.pushState;
7889
+ const originalReplaceState = history.replaceState;
7890
+
7891
+ history.pushState = function(...args) {
7892
+ originalPushState.apply(history, args);
7893
+ setTimeout(checkPathChange, 0);
7894
+ };
7895
+
7896
+ history.replaceState = function(...args) {
7897
+ originalReplaceState.apply(history, args);
7898
+ setTimeout(checkPathChange, 0);
7899
+ };
7900
+ }
7901
+
7820
7902
  // Initialize SDK
7821
7903
  async function initializeSDK() {
7822
7904
  await Initializer.initialize();
@@ -7826,6 +7908,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
7826
7908
  setTimeout(() => {
7827
7909
  AutoEventsTracker.setup();
7828
7910
  }, 2000); // Wait 2 seconds for initial session to be saved
7911
+
7912
+ // Setup SPA navigation monitoring for auto events
7913
+ setupSPANavigationMonitoring();
7829
7914
  }
7830
7915
 
7831
7916
  if (document.readyState === 'loading') {
@@ -7868,7 +7953,7 @@ const CryptiqueSDK = {
7868
7953
  init(options = {}) {
7869
7954
  if (typeof window === 'undefined') {
7870
7955
  console.warn('Cryptique SDK requires a browser environment');
7871
- return null;
7956
+ return Promise.resolve(null);
7872
7957
  }
7873
7958
 
7874
7959
  if (!options.siteId) {
@@ -7909,21 +7994,28 @@ const CryptiqueSDK = {
7909
7994
  return null;
7910
7995
  };
7911
7996
 
7912
- // If SDK is already loaded, configure immediately
7997
+ // If SDK is already loaded, configure and return ready promise
7913
7998
  if (window.Cryptique) {
7914
- return checkSDK();
7999
+ checkSDK();
8000
+ // Return ready() so await Cryptique.init() waits for full initialization
8001
+ return window.Cryptique.ready ? window.Cryptique.ready() : Promise.resolve(window.Cryptique);
7915
8002
  }
7916
8003
 
7917
- // Otherwise, wait a bit for it to initialize
8004
+ // Otherwise, wait for SDK to load then configure and wait for ready
7918
8005
  return new Promise((resolve) => {
7919
8006
  const maxAttempts = 50;
7920
8007
  let attempts = 0;
7921
8008
  const interval = setInterval(() => {
7922
8009
  attempts++;
7923
8010
  const sdk = checkSDK();
7924
- if (sdk || attempts >= maxAttempts) {
8011
+ if (sdk) {
8012
+ clearInterval(interval);
8013
+ // Wait for full initialization
8014
+ const readyPromise = sdk.ready ? sdk.ready() : Promise.resolve(sdk);
8015
+ resolve(readyPromise);
8016
+ } else if (attempts >= maxAttempts) {
7925
8017
  clearInterval(interval);
7926
- resolve(sdk);
8018
+ resolve(null);
7927
8019
  }
7928
8020
  }, 100);
7929
8021
  });
@@ -7940,6 +8032,19 @@ const CryptiqueSDK = {
7940
8032
  return null;
7941
8033
  },
7942
8034
 
8035
+ /**
8036
+ * Wait for SDK to be fully initialized
8037
+ * Returns a Promise that resolves when initialization is complete
8038
+ * @returns {Promise<void>}
8039
+ */
8040
+ ready() {
8041
+ const instance = this.getInstance();
8042
+ if (instance && instance.ready) {
8043
+ return instance.ready();
8044
+ }
8045
+ return Promise.resolve();
8046
+ },
8047
+
7943
8048
  /**
7944
8049
  * Identify a user with a unique identifier
7945
8050
  *
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
@@ -3768,18 +3783,6 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
3768
3783
 
3769
3784
  // Save updated session
3770
3785
  StorageManager.saveSession(session);
3771
-
3772
- // Fire page_view auto event on every navigation (single source of truth with session tracking)
3773
- // This ensures SPAs get page_view on route changes without a separate history override
3774
- if (typeof CONFIG !== 'undefined' && CONFIG.AUTO_EVENTS && CONFIG.AUTO_EVENTS.enabled && typeof shouldTrackAutoEvents === 'function' && shouldTrackAutoEvents()) {
3775
- if (typeof AutoEventsTracker !== 'undefined') {
3776
- if (!AutoEventsTracker.isSetup) {
3777
- AutoEventsTracker.setup();
3778
- } else {
3779
- AutoEventsTracker.trackPageView();
3780
- }
3781
- }
3782
- }
3783
3786
  } catch (error) {
3784
3787
  console.error("Error tracking page visit:", error);
3785
3788
  }
@@ -4309,6 +4312,10 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
4309
4312
  // - Proper use of sendBeacon vs fetch
4310
4313
  // ============================================================================
4311
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
+
4312
4319
  /**
4313
4320
  * APIClient - Unified API communication
4314
4321
  *
@@ -4489,8 +4496,14 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
4489
4496
  // Don't retry aborted requests - they were intentionally cancelled
4490
4497
  return; // Silently return, don't throw
4491
4498
  }
4492
-
4493
- 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
+ }
4494
4507
 
4495
4508
  // Retry if retries > 0 (but not for abort errors)
4496
4509
  if (retries > 0) {
@@ -5897,6 +5910,12 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
5897
5910
 
5898
5911
  } catch (error) {
5899
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
+ }
5900
5919
  }
5901
5920
  }
5902
5921
  };
@@ -6109,9 +6128,17 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6109
6128
  }
6110
6129
  }
6111
6130
 
6131
+ // Wait for SDK init if not ready (handles race conditions)
6112
6132
  if (!runtimeState.isInitialized) {
6113
- console.warn('⚠️ [Events] SDK not initialized, skipping auto event:', eventName);
6114
- 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
+ }
6115
6142
  }
6116
6143
 
6117
6144
  // Get session from storage - it returns { id, userId, ... }
@@ -6686,8 +6713,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6686
6713
 
6687
6714
  /**
6688
6715
  * Setup auto events tracking
6716
+ * Waits for SDK initialization before proceeding (handles SPA navigation race)
6689
6717
  */
6690
- setup() {
6718
+ async setup() {
6691
6719
  // Check if auto events are enabled
6692
6720
  if (!CONFIG.AUTO_EVENTS.enabled) {
6693
6721
  return;
@@ -6703,6 +6731,17 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
6703
6731
  }
6704
6732
 
6705
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
+
6706
6745
  // Track page views
6707
6746
  this.trackPageView();
6708
6747
 
@@ -7685,6 +7724,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
7685
7724
  ...window.Cryptique,
7686
7725
  version: CONFIG.VERSION,
7687
7726
 
7727
+ // Wait for SDK to be fully initialized (returns Promise)
7728
+ ready: () => _readyPromise,
7729
+
7688
7730
  // Events System
7689
7731
  track: EventsManager.trackEvent.bind(EventsManager),
7690
7732
  trackEvent: EventsManager.trackEvent.bind(EventsManager),
@@ -7815,6 +7857,46 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
7815
7857
  // Initialize SDK when DOM is ready
7816
7858
  // ============================================================================
7817
7859
 
7860
+ /**
7861
+ * Monitor path changes for Single Page Applications (SPAs)
7862
+ * Re-checks auto events configuration when navigation occurs
7863
+ */
7864
+ function setupSPANavigationMonitoring() {
7865
+ let lastPath = window.location.pathname;
7866
+
7867
+ // Check path changes
7868
+ function checkPathChange() {
7869
+ const currentPath = window.location.pathname;
7870
+ if (currentPath !== lastPath) {
7871
+ lastPath = currentPath;
7872
+
7873
+ // If auto events were disabled on previous page but enabled on new page, setup
7874
+ if (CONFIG.AUTO_EVENTS.enabled && shouldTrackAutoEvents() && !AutoEventsTracker.isSetup) {
7875
+ AutoEventsTracker.setup();
7876
+ }
7877
+ // Note: If auto events should be disabled on new page, trackAutoEvent will check
7878
+ // shouldTrackAutoEvents() before tracking, so no need to remove listeners
7879
+ }
7880
+ }
7881
+
7882
+ // Check on popstate (browser back/forward)
7883
+ window.addEventListener('popstate', checkPathChange);
7884
+
7885
+ // For SPAs using history.pushState/replaceState, monitor them
7886
+ const originalPushState = history.pushState;
7887
+ const originalReplaceState = history.replaceState;
7888
+
7889
+ history.pushState = function(...args) {
7890
+ originalPushState.apply(history, args);
7891
+ setTimeout(checkPathChange, 0);
7892
+ };
7893
+
7894
+ history.replaceState = function(...args) {
7895
+ originalReplaceState.apply(history, args);
7896
+ setTimeout(checkPathChange, 0);
7897
+ };
7898
+ }
7899
+
7818
7900
  // Initialize SDK
7819
7901
  async function initializeSDK() {
7820
7902
  await Initializer.initialize();
@@ -7824,6 +7906,9 @@ if (window.Cryptique && window.Cryptique.initialized) ; else {
7824
7906
  setTimeout(() => {
7825
7907
  AutoEventsTracker.setup();
7826
7908
  }, 2000); // Wait 2 seconds for initial session to be saved
7909
+
7910
+ // Setup SPA navigation monitoring for auto events
7911
+ setupSPANavigationMonitoring();
7827
7912
  }
7828
7913
 
7829
7914
  if (document.readyState === 'loading') {
@@ -7866,7 +7951,7 @@ const CryptiqueSDK = {
7866
7951
  init(options = {}) {
7867
7952
  if (typeof window === 'undefined') {
7868
7953
  console.warn('Cryptique SDK requires a browser environment');
7869
- return null;
7954
+ return Promise.resolve(null);
7870
7955
  }
7871
7956
 
7872
7957
  if (!options.siteId) {
@@ -7907,21 +7992,28 @@ const CryptiqueSDK = {
7907
7992
  return null;
7908
7993
  };
7909
7994
 
7910
- // If SDK is already loaded, configure immediately
7995
+ // If SDK is already loaded, configure and return ready promise
7911
7996
  if (window.Cryptique) {
7912
- return checkSDK();
7997
+ checkSDK();
7998
+ // Return ready() so await Cryptique.init() waits for full initialization
7999
+ return window.Cryptique.ready ? window.Cryptique.ready() : Promise.resolve(window.Cryptique);
7913
8000
  }
7914
8001
 
7915
- // Otherwise, wait a bit for it to initialize
8002
+ // Otherwise, wait for SDK to load then configure and wait for ready
7916
8003
  return new Promise((resolve) => {
7917
8004
  const maxAttempts = 50;
7918
8005
  let attempts = 0;
7919
8006
  const interval = setInterval(() => {
7920
8007
  attempts++;
7921
8008
  const sdk = checkSDK();
7922
- if (sdk || attempts >= maxAttempts) {
8009
+ if (sdk) {
8010
+ clearInterval(interval);
8011
+ // Wait for full initialization
8012
+ const readyPromise = sdk.ready ? sdk.ready() : Promise.resolve(sdk);
8013
+ resolve(readyPromise);
8014
+ } else if (attempts >= maxAttempts) {
7923
8015
  clearInterval(interval);
7924
- resolve(sdk);
8016
+ resolve(null);
7925
8017
  }
7926
8018
  }, 100);
7927
8019
  });
@@ -7938,6 +8030,19 @@ const CryptiqueSDK = {
7938
8030
  return null;
7939
8031
  },
7940
8032
 
8033
+ /**
8034
+ * Wait for SDK to be fully initialized
8035
+ * Returns a Promise that resolves when initialization is complete
8036
+ * @returns {Promise<void>}
8037
+ */
8038
+ ready() {
8039
+ const instance = this.getInstance();
8040
+ if (instance && instance.ready) {
8041
+ return instance.ready();
8042
+ }
8043
+ return Promise.resolve();
8044
+ },
8045
+
7941
8046
  /**
7942
8047
  * Identify a user with a unique identifier
7943
8048
  *
@@ -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
@@ -3774,18 +3789,6 @@
3774
3789
 
3775
3790
  // Save updated session
3776
3791
  StorageManager.saveSession(session);
3777
-
3778
- // Fire page_view auto event on every navigation (single source of truth with session tracking)
3779
- // This ensures SPAs get page_view on route changes without a separate history override
3780
- if (typeof CONFIG !== 'undefined' && CONFIG.AUTO_EVENTS && CONFIG.AUTO_EVENTS.enabled && typeof shouldTrackAutoEvents === 'function' && shouldTrackAutoEvents()) {
3781
- if (typeof AutoEventsTracker !== 'undefined') {
3782
- if (!AutoEventsTracker.isSetup) {
3783
- AutoEventsTracker.setup();
3784
- } else {
3785
- AutoEventsTracker.trackPageView();
3786
- }
3787
- }
3788
- }
3789
3792
  } catch (error) {
3790
3793
  console.error("Error tracking page visit:", error);
3791
3794
  }
@@ -4315,6 +4318,10 @@
4315
4318
  // - Proper use of sendBeacon vs fetch
4316
4319
  // ============================================================================
4317
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
+
4318
4325
  /**
4319
4326
  * APIClient - Unified API communication
4320
4327
  *
@@ -4495,8 +4502,14 @@
4495
4502
  // Don't retry aborted requests - they were intentionally cancelled
4496
4503
  return; // Silently return, don't throw
4497
4504
  }
4498
-
4499
- 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
+ }
4500
4513
 
4501
4514
  // Retry if retries > 0 (but not for abort errors)
4502
4515
  if (retries > 0) {
@@ -5903,6 +5916,12 @@
5903
5916
 
5904
5917
  } catch (error) {
5905
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
+ }
5906
5925
  }
5907
5926
  }
5908
5927
  };
@@ -6115,9 +6134,17 @@
6115
6134
  }
6116
6135
  }
6117
6136
 
6137
+ // Wait for SDK init if not ready (handles race conditions)
6118
6138
  if (!runtimeState.isInitialized) {
6119
- console.warn('⚠️ [Events] SDK not initialized, skipping auto event:', eventName);
6120
- 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
+ }
6121
6148
  }
6122
6149
 
6123
6150
  // Get session from storage - it returns { id, userId, ... }
@@ -6692,8 +6719,9 @@
6692
6719
 
6693
6720
  /**
6694
6721
  * Setup auto events tracking
6722
+ * Waits for SDK initialization before proceeding (handles SPA navigation race)
6695
6723
  */
6696
- setup() {
6724
+ async setup() {
6697
6725
  // Check if auto events are enabled
6698
6726
  if (!CONFIG.AUTO_EVENTS.enabled) {
6699
6727
  return;
@@ -6709,6 +6737,17 @@
6709
6737
  }
6710
6738
 
6711
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
+
6712
6751
  // Track page views
6713
6752
  this.trackPageView();
6714
6753
 
@@ -7691,6 +7730,9 @@
7691
7730
  ...window.Cryptique,
7692
7731
  version: CONFIG.VERSION,
7693
7732
 
7733
+ // Wait for SDK to be fully initialized (returns Promise)
7734
+ ready: () => _readyPromise,
7735
+
7694
7736
  // Events System
7695
7737
  track: EventsManager.trackEvent.bind(EventsManager),
7696
7738
  trackEvent: EventsManager.trackEvent.bind(EventsManager),
@@ -7821,6 +7863,46 @@
7821
7863
  // Initialize SDK when DOM is ready
7822
7864
  // ============================================================================
7823
7865
 
7866
+ /**
7867
+ * Monitor path changes for Single Page Applications (SPAs)
7868
+ * Re-checks auto events configuration when navigation occurs
7869
+ */
7870
+ function setupSPANavigationMonitoring() {
7871
+ let lastPath = window.location.pathname;
7872
+
7873
+ // Check path changes
7874
+ function checkPathChange() {
7875
+ const currentPath = window.location.pathname;
7876
+ if (currentPath !== lastPath) {
7877
+ lastPath = currentPath;
7878
+
7879
+ // If auto events were disabled on previous page but enabled on new page, setup
7880
+ if (CONFIG.AUTO_EVENTS.enabled && shouldTrackAutoEvents() && !AutoEventsTracker.isSetup) {
7881
+ AutoEventsTracker.setup();
7882
+ }
7883
+ // Note: If auto events should be disabled on new page, trackAutoEvent will check
7884
+ // shouldTrackAutoEvents() before tracking, so no need to remove listeners
7885
+ }
7886
+ }
7887
+
7888
+ // Check on popstate (browser back/forward)
7889
+ window.addEventListener('popstate', checkPathChange);
7890
+
7891
+ // For SPAs using history.pushState/replaceState, monitor them
7892
+ const originalPushState = history.pushState;
7893
+ const originalReplaceState = history.replaceState;
7894
+
7895
+ history.pushState = function(...args) {
7896
+ originalPushState.apply(history, args);
7897
+ setTimeout(checkPathChange, 0);
7898
+ };
7899
+
7900
+ history.replaceState = function(...args) {
7901
+ originalReplaceState.apply(history, args);
7902
+ setTimeout(checkPathChange, 0);
7903
+ };
7904
+ }
7905
+
7824
7906
  // Initialize SDK
7825
7907
  async function initializeSDK() {
7826
7908
  await Initializer.initialize();
@@ -7830,6 +7912,9 @@
7830
7912
  setTimeout(() => {
7831
7913
  AutoEventsTracker.setup();
7832
7914
  }, 2000); // Wait 2 seconds for initial session to be saved
7915
+
7916
+ // Setup SPA navigation monitoring for auto events
7917
+ setupSPANavigationMonitoring();
7833
7918
  }
7834
7919
 
7835
7920
  if (document.readyState === 'loading') {
@@ -7872,7 +7957,7 @@
7872
7957
  init(options = {}) {
7873
7958
  if (typeof window === 'undefined') {
7874
7959
  console.warn('Cryptique SDK requires a browser environment');
7875
- return null;
7960
+ return Promise.resolve(null);
7876
7961
  }
7877
7962
 
7878
7963
  if (!options.siteId) {
@@ -7913,21 +7998,28 @@
7913
7998
  return null;
7914
7999
  };
7915
8000
 
7916
- // If SDK is already loaded, configure immediately
8001
+ // If SDK is already loaded, configure and return ready promise
7917
8002
  if (window.Cryptique) {
7918
- return checkSDK();
8003
+ checkSDK();
8004
+ // Return ready() so await Cryptique.init() waits for full initialization
8005
+ return window.Cryptique.ready ? window.Cryptique.ready() : Promise.resolve(window.Cryptique);
7919
8006
  }
7920
8007
 
7921
- // Otherwise, wait a bit for it to initialize
8008
+ // Otherwise, wait for SDK to load then configure and wait for ready
7922
8009
  return new Promise((resolve) => {
7923
8010
  const maxAttempts = 50;
7924
8011
  let attempts = 0;
7925
8012
  const interval = setInterval(() => {
7926
8013
  attempts++;
7927
8014
  const sdk = checkSDK();
7928
- if (sdk || attempts >= maxAttempts) {
8015
+ if (sdk) {
8016
+ clearInterval(interval);
8017
+ // Wait for full initialization
8018
+ const readyPromise = sdk.ready ? sdk.ready() : Promise.resolve(sdk);
8019
+ resolve(readyPromise);
8020
+ } else if (attempts >= maxAttempts) {
7929
8021
  clearInterval(interval);
7930
- resolve(sdk);
8022
+ resolve(null);
7931
8023
  }
7932
8024
  }, 100);
7933
8025
  });
@@ -7944,6 +8036,19 @@
7944
8036
  return null;
7945
8037
  },
7946
8038
 
8039
+ /**
8040
+ * Wait for SDK to be fully initialized
8041
+ * Returns a Promise that resolves when initialization is complete
8042
+ * @returns {Promise<void>}
8043
+ */
8044
+ ready() {
8045
+ const instance = this.getInstance();
8046
+ if (instance && instance.ready) {
8047
+ return instance.ready();
8048
+ }
8049
+ return Promise.resolve();
8050
+ },
8051
+
7947
8052
  /**
7948
8053
  * Identify a user with a unique identifier
7949
8054
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cryptique-sdk",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
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",