@journium/react 1.0.6 → 1.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/dist/index.mjs CHANGED
@@ -699,14 +699,23 @@ Logger.isDebugEnabled = false;
699
699
 
700
700
  class JourniumClient {
701
701
  constructor(config) {
702
- var _a;
702
+ var _a, _b, _c, _d;
703
703
  this.queue = [];
704
+ this.stagedEvents = [];
704
705
  this.flushTimer = null;
705
- this.initialized = false;
706
- // Validate required configuration
707
- if (!config.publishableKey) {
706
+ this.initializationComplete = false;
707
+ this.initializationFailed = false;
708
+ this.disabled = false;
709
+ this.optionsChangeCallbacks = new Set();
710
+ // Validate required configuration - put in disabled state if invalid
711
+ if (!config.publishableKey || config.publishableKey.trim() === '') {
712
+ this.disabled = true;
708
713
  Logger.setDebug(true);
709
- Logger.error('Journium: publishableKey is required but not provided. SDK will not function.');
714
+ Logger.error('Journium: publishableKey is required but not provided or is empty. SDK will not function.');
715
+ // Create minimal config to prevent crashes
716
+ this.config = { publishableKey: '', apiHost: 'https://events.journium.app' };
717
+ this.effectiveOptions = { debug: true };
718
+ this.optionsStorageKey = 'jrnm_invalid_options';
710
719
  return;
711
720
  }
712
721
  // Set default apiHost if not provided
@@ -716,25 +725,15 @@ class JourniumClient {
716
725
  };
717
726
  // Generate storage key for options caching
718
727
  this.optionsStorageKey = `jrnm_${config.publishableKey}_options`;
719
- // Generate default values
720
- const defaultOptions = {
721
- debug: false,
722
- flushAt: 20,
723
- flushInterval: 10000,
724
- sessionTimeout: 30 * 60 * 1000, // 30 minutes
725
- };
726
- // Initialize effective options with local options taking precedence over defaults
727
- this.effectiveOptions = { ...defaultOptions };
728
- if (this.config.options) {
729
- this.effectiveOptions = mergeOptions(defaultOptions, this.config.options);
730
- }
731
- // Initialize Logger with debug setting
732
- Logger.setDebug((_a = this.effectiveOptions.debug) !== null && _a !== void 0 ? _a : false);
733
- // Initialize identity manager
734
- this.identityManager = new BrowserIdentityManager(this.effectiveOptions.sessionTimeout, this.config.publishableKey);
735
- // Initialize synchronously with cached config, fetch fresh config in background
736
- this.initializeSync();
737
- this.fetchRemoteOptionsAsync();
728
+ // Initialize with minimal defaults for identity manager
729
+ const fallbackSessionTimeout = 30 * 60 * 1000; // 30 minutes
730
+ this.effectiveOptions = {}; // Will be set after remote config
731
+ // Initialize Logger with local debug setting or false
732
+ Logger.setDebug((_b = (_a = this.config.options) === null || _a === void 0 ? void 0 : _a.debug) !== null && _b !== void 0 ? _b : false);
733
+ // Initialize identity manager with fallback timeout
734
+ this.identityManager = new BrowserIdentityManager((_d = (_c = this.config.options) === null || _c === void 0 ? void 0 : _c.sessionTimeout) !== null && _d !== void 0 ? _d : fallbackSessionTimeout, this.config.publishableKey);
735
+ // Initialize asynchronously - wait for remote config first
736
+ this.initializeAsync();
738
737
  }
739
738
  loadCachedOptions() {
740
739
  if (typeof window === 'undefined' || !window.localStorage) {
@@ -760,57 +759,159 @@ class JourniumClient {
760
759
  Logger.warn('Journium: Failed to save config to cache:', error);
761
760
  }
762
761
  }
763
- initializeSync() {
764
- // Step 1: Load cached remote options from localStorage (synchronous)
765
- const cachedRemoteOptions = this.loadCachedOptions();
766
- // Step 2: If no local options provided, use cached remote options
767
- if (!this.config.options && cachedRemoteOptions) {
768
- this.effectiveOptions = cachedRemoteOptions;
769
- Logger.log('Journium: Using cached remote options:', cachedRemoteOptions);
770
- }
771
- // Step 3: Mark as initialized immediately - no need to wait for remote fetch
772
- this.initialized = true;
773
- // Step 4: Start flush timer immediately
774
- if (this.effectiveOptions.flushInterval && this.effectiveOptions.flushInterval > 0) {
775
- this.startFlushTimer();
776
- }
777
- Logger.log('Journium: Client initialized with effective options:', this.effectiveOptions);
778
- }
779
- async fetchRemoteOptionsAsync() {
780
- // Fetch fresh config in background
781
- if (this.config.publishableKey) {
782
- await this.fetchAndCacheRemoteOptions();
783
- }
784
- }
785
- async fetchAndCacheRemoteOptions() {
762
+ async initializeAsync() {
786
763
  var _a;
787
764
  try {
788
- Logger.log('Journium: Fetching remote configuration in background...');
789
- const remoteOptionsResponse = await fetchRemoteOptions(this.config.apiHost, this.config.publishableKey);
790
- if (remoteOptionsResponse && remoteOptionsResponse.success) {
791
- // Save remote config to cache for next session
792
- this.saveCachedOptions(remoteOptionsResponse.config);
793
- // Update effective options: local options (if provided) overrides fresh remote options
794
- if (!this.config.options) {
795
- // No local options provided, use fresh remote options
796
- this.effectiveOptions = remoteOptionsResponse.config;
765
+ Logger.log('Journium: Starting initialization - fetching fresh remote config...');
766
+ // Step 1: Try to fetch fresh remote config with timeout and retry
767
+ const remoteOptions = await this.fetchRemoteOptionsWithRetry();
768
+ if (remoteOptions) {
769
+ // Step 2: Cache the fresh remote config
770
+ this.saveCachedOptions(remoteOptions);
771
+ // Step 3: Merge local options over remote config (local overrides remote)
772
+ if (this.config.options) {
773
+ this.effectiveOptions = mergeOptions(this.config.options, remoteOptions);
774
+ Logger.log('Journium: Using fresh remote config merged with local options:', this.effectiveOptions);
797
775
  }
798
776
  else {
799
- // Local options provided, merge it over fresh remote options
800
- this.effectiveOptions = mergeOptions(remoteOptionsResponse.config, this.config.options);
777
+ this.effectiveOptions = remoteOptions;
778
+ Logger.log('Journium: Using fresh remote config:', this.effectiveOptions);
801
779
  }
802
- // Update session timeout if provided in fresh effective options
803
- if (this.effectiveOptions.sessionTimeout) {
804
- this.identityManager.updateSessionTimeout(this.effectiveOptions.sessionTimeout);
780
+ }
781
+ else {
782
+ // Step 4: Fallback to cached config if fresh fetch failed
783
+ const cachedRemoteOptions = this.loadCachedOptions();
784
+ if (cachedRemoteOptions) {
785
+ if (this.config.options) {
786
+ this.effectiveOptions = mergeOptions(this.config.options, cachedRemoteOptions);
787
+ Logger.log('Journium: Fresh config failed, using cached remote config merged with local options:', this.effectiveOptions);
788
+ }
789
+ else {
790
+ this.effectiveOptions = cachedRemoteOptions;
791
+ Logger.log('Journium: Fresh config failed, using cached remote config:', this.effectiveOptions);
792
+ }
793
+ }
794
+ else {
795
+ // Step 5: No remote config and no cached config - initialization fails
796
+ Logger.error('Journium: Initialization failed - no remote config available and no cached config found');
797
+ this.initializationFailed = true;
798
+ this.initializationComplete = false;
799
+ return;
805
800
  }
806
- Logger.log('Journium: Background remote options applied:', remoteOptionsResponse.config);
807
- Logger.log('Journium: New effective options:', this.effectiveOptions);
808
- // Update Logger debug setting with new options
809
- Logger.setDebug((_a = this.effectiveOptions.debug) !== null && _a !== void 0 ? _a : false);
810
801
  }
802
+ // Step 6: Update identity manager session timeout if provided
803
+ if (this.effectiveOptions.sessionTimeout) {
804
+ this.identityManager.updateSessionTimeout(this.effectiveOptions.sessionTimeout);
805
+ }
806
+ // Step 7: Update Logger debug setting
807
+ Logger.setDebug((_a = this.effectiveOptions.debug) !== null && _a !== void 0 ? _a : false);
808
+ // Step 8: Mark initialization as complete
809
+ this.initializationComplete = true;
810
+ this.initializationFailed = false;
811
+ // Step 9: Process any staged events
812
+ this.processStagedEvents();
813
+ // Step 10: Start flush timer
814
+ if (this.effectiveOptions.flushInterval && this.effectiveOptions.flushInterval > 0) {
815
+ this.startFlushTimer();
816
+ }
817
+ Logger.log('Journium: Initialization complete with options:', this.effectiveOptions);
818
+ // Step 11: Notify callbacks about options
819
+ this.notifyOptionsChange();
811
820
  }
812
821
  catch (error) {
813
- Logger.warn('Journium: Background remote options fetch failed:', error);
822
+ Logger.error('Journium: Initialization failed:', error);
823
+ this.initializationFailed = true;
824
+ this.initializationComplete = false;
825
+ }
826
+ }
827
+ async fetchRemoteOptionsWithRetry() {
828
+ const maxRetries = 2;
829
+ const timeoutMs = 15000; // 15 seconds
830
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
831
+ try {
832
+ Logger.log(`Journium: Fetching remote config (attempt ${attempt}/${maxRetries})...`);
833
+ // Create timeout promise
834
+ const timeoutPromise = new Promise((_, reject) => {
835
+ setTimeout(() => reject(new Error('Timeout')), timeoutMs);
836
+ });
837
+ // Race fetch against timeout
838
+ const fetchPromise = fetchRemoteOptions(this.config.apiHost, this.config.publishableKey);
839
+ const remoteOptionsResponse = await Promise.race([fetchPromise, timeoutPromise]);
840
+ if (remoteOptionsResponse && remoteOptionsResponse.success) {
841
+ Logger.log('Journium: Successfully fetched fresh remote config:', remoteOptionsResponse.config);
842
+ return remoteOptionsResponse.config;
843
+ }
844
+ else {
845
+ throw new Error('Remote config fetch unsuccessful');
846
+ }
847
+ }
848
+ catch (error) {
849
+ Logger.warn(`Journium: Remote config fetch attempt ${attempt} failed:`, error);
850
+ if (attempt === maxRetries) {
851
+ Logger.warn('Journium: All remote config fetch attempts failed, falling back to cached config');
852
+ return null;
853
+ }
854
+ // Wait 1 second before retry (except on last attempt)
855
+ if (attempt < maxRetries) {
856
+ await new Promise(resolve => setTimeout(resolve, 1000));
857
+ }
858
+ }
859
+ }
860
+ return null;
861
+ }
862
+ /**
863
+ * Register a callback to be notified when effective options change (e.g., when remote options are fetched)
864
+ */
865
+ onOptionsChange(callback) {
866
+ this.optionsChangeCallbacks.add(callback);
867
+ // Return unsubscribe function
868
+ return () => {
869
+ this.optionsChangeCallbacks.delete(callback);
870
+ };
871
+ }
872
+ notifyOptionsChange() {
873
+ this.optionsChangeCallbacks.forEach(callback => {
874
+ try {
875
+ callback(this.effectiveOptions);
876
+ }
877
+ catch (error) {
878
+ Logger.warn('Journium: Error in options change callback:', error);
879
+ }
880
+ });
881
+ }
882
+ processStagedEvents() {
883
+ if (this.stagedEvents.length === 0)
884
+ return;
885
+ Logger.log(`Journium: Processing ${this.stagedEvents.length} staged events`);
886
+ // Move staged events to main queue, adding identity properties now
887
+ const identity = this.identityManager.getIdentity();
888
+ const userAgentInfo = this.identityManager.getUserAgentInfo();
889
+ for (const stagedEvent of this.stagedEvents) {
890
+ // Add identity properties that weren't available during staging
891
+ const eventWithIdentity = {
892
+ ...stagedEvent,
893
+ properties: {
894
+ $device_id: identity === null || identity === void 0 ? void 0 : identity.$device_id,
895
+ distinct_id: identity === null || identity === void 0 ? void 0 : identity.distinct_id,
896
+ $session_id: identity === null || identity === void 0 ? void 0 : identity.$session_id,
897
+ $is_identified: (identity === null || identity === void 0 ? void 0 : identity.$user_state) === 'identified',
898
+ $current_url: typeof window !== 'undefined' ? window.location.href : '',
899
+ $pathname: typeof window !== 'undefined' ? window.location.pathname : '',
900
+ ...userAgentInfo,
901
+ $lib_version: '0.1.0', // TODO: Get from package.json
902
+ $platform: 'web',
903
+ ...stagedEvent.properties, // Original properties override system properties
904
+ },
905
+ };
906
+ this.queue.push(eventWithIdentity);
907
+ }
908
+ // Clear staged events
909
+ this.stagedEvents = [];
910
+ Logger.log('Journium: Staged events processed and moved to main queue');
911
+ // Check if we should flush immediately
912
+ if (this.queue.length >= this.effectiveOptions.flushAt) {
913
+ // console.log('1 Journium: Flushing events...'+JSON.stringify(this.effectiveOptions));
914
+ this.flush();
814
915
  }
815
916
  }
816
917
  startFlushTimer() {
@@ -819,6 +920,7 @@ class JourniumClient {
819
920
  }
820
921
  // Use universal setInterval (works in both browser and Node.js)
821
922
  this.flushTimer = setInterval(() => {
923
+ // console.log('2 Journium: Flushing events...'+JSON.stringify(this.effectiveOptions));
822
924
  this.flush();
823
925
  }, this.effectiveOptions.flushInterval);
824
926
  }
@@ -847,9 +949,14 @@ class JourniumClient {
847
949
  }
848
950
  }
849
951
  identify(distinctId, attributes = {}) {
850
- // Don't identify if SDK is not properly configured
851
- if (!this.config || !this.config.publishableKey || !this.initialized) {
852
- Logger.warn('Journium: identify() call rejected - SDK not ready');
952
+ // Don't identify if SDK is not properly configured or disabled
953
+ if (this.disabled || !this.config || !this.config.publishableKey) {
954
+ Logger.warn('Journium: identify() call rejected - SDK not ready or disabled');
955
+ return;
956
+ }
957
+ // Don't identify if initialization failed
958
+ if (this.initializationFailed) {
959
+ Logger.warn('Journium: identify() call rejected - initialization failed');
853
960
  return;
854
961
  }
855
962
  // Call identify on identity manager to get previous distinct ID
@@ -863,9 +970,14 @@ class JourniumClient {
863
970
  Logger.log('Journium: User identified', { distinctId, attributes, previousDistinctId });
864
971
  }
865
972
  reset() {
866
- // Don't reset if SDK is not properly configured
867
- if (!this.config || !this.config.publishableKey || !this.initialized) {
868
- Logger.warn('Journium: reset() call rejected - SDK not ready');
973
+ // Don't reset if SDK is not properly configured or disabled
974
+ if (this.disabled || !this.config || !this.config.publishableKey) {
975
+ Logger.warn('Journium: reset() call rejected - SDK not ready or disabled');
976
+ return;
977
+ }
978
+ // Don't reset if initialization failed
979
+ if (this.initializationFailed) {
980
+ Logger.warn('Journium: reset() call rejected - initialization failed');
869
981
  return;
870
982
  }
871
983
  // Reset identity in identity manager
@@ -873,36 +985,58 @@ class JourniumClient {
873
985
  Logger.log('Journium: User identity reset');
874
986
  }
875
987
  track(event, properties = {}) {
876
- // Don't track if SDK is not properly configured
877
- if (!this.config || !this.config.publishableKey || !this.initialized) {
878
- Logger.warn('Journium: track() call rejected - SDK not ready');
988
+ // Don't track if SDK is not properly configured or disabled
989
+ if (this.disabled || !this.config || !this.config.publishableKey) {
990
+ Logger.warn('Journium: track() call rejected - SDK not ready or disabled');
879
991
  return;
880
992
  }
881
- const identity = this.identityManager.getIdentity();
882
- const userAgentInfo = this.identityManager.getUserAgentInfo();
883
- // Create standardized event properties
884
- const eventProperties = {
885
- $device_id: identity === null || identity === void 0 ? void 0 : identity.$device_id,
886
- distinct_id: identity === null || identity === void 0 ? void 0 : identity.distinct_id,
887
- $session_id: identity === null || identity === void 0 ? void 0 : identity.$session_id,
888
- $is_identified: (identity === null || identity === void 0 ? void 0 : identity.$user_state) === 'identified',
889
- $current_url: typeof window !== 'undefined' ? window.location.href : '',
890
- $pathname: typeof window !== 'undefined' ? window.location.pathname : '',
891
- ...userAgentInfo,
892
- $lib_version: '0.1.0', // TODO: Get from package.json
893
- $platform: 'web',
894
- ...properties, // User-provided properties override defaults
895
- };
993
+ // Create minimal event without identity properties (will be added later if staging)
896
994
  const journiumEvent = {
897
995
  uuid: generateUuidv7(),
898
996
  ingestion_key: this.config.publishableKey,
899
997
  client_timestamp: getCurrentTimestamp(),
900
998
  event,
901
- properties: eventProperties,
999
+ properties: { ...properties }, // Only user properties for now
902
1000
  };
903
- this.queue.push(journiumEvent);
904
- Logger.log('Journium: Event tracked', journiumEvent);
905
- if (this.queue.length >= this.effectiveOptions.flushAt) {
1001
+ // Stage events during initialization, add to queue after initialization
1002
+ if (!this.initializationComplete) {
1003
+ // If initialization failed, reject events
1004
+ if (this.initializationFailed) {
1005
+ Logger.warn('Journium: track() call rejected - initialization failed');
1006
+ return;
1007
+ }
1008
+ this.stagedEvents.push(journiumEvent);
1009
+ Logger.log('Journium: Event staged during initialization', journiumEvent);
1010
+ return;
1011
+ }
1012
+ // If initialization failed, reject events
1013
+ if (this.initializationFailed) {
1014
+ Logger.warn('Journium: track() call rejected - initialization failed');
1015
+ return;
1016
+ }
1017
+ // Add identity properties for immediate events (after initialization)
1018
+ const identity = this.identityManager.getIdentity();
1019
+ const userAgentInfo = this.identityManager.getUserAgentInfo();
1020
+ const eventWithIdentity = {
1021
+ ...journiumEvent,
1022
+ properties: {
1023
+ $device_id: identity === null || identity === void 0 ? void 0 : identity.$device_id,
1024
+ distinct_id: identity === null || identity === void 0 ? void 0 : identity.distinct_id,
1025
+ $session_id: identity === null || identity === void 0 ? void 0 : identity.$session_id,
1026
+ $is_identified: (identity === null || identity === void 0 ? void 0 : identity.$user_state) === 'identified',
1027
+ $current_url: typeof window !== 'undefined' ? window.location.href : '',
1028
+ $pathname: typeof window !== 'undefined' ? window.location.pathname : '',
1029
+ ...userAgentInfo,
1030
+ $lib_version: '0.1.0', // TODO: Get from package.json
1031
+ $platform: 'web',
1032
+ ...properties, // User-provided properties override system properties
1033
+ },
1034
+ };
1035
+ this.queue.push(eventWithIdentity);
1036
+ Logger.log('Journium: Event tracked', eventWithIdentity);
1037
+ // Only flush if we have effective options (after initialization)
1038
+ if (this.effectiveOptions.flushAt && this.queue.length >= this.effectiveOptions.flushAt) {
1039
+ // console.log('3 Journium: Flushing events...'+JSON.stringify(this.effectiveOptions));
906
1040
  this.flush();
907
1041
  }
908
1042
  }
@@ -911,6 +1045,11 @@ class JourniumClient {
911
1045
  if (!this.config || !this.config.publishableKey) {
912
1046
  return;
913
1047
  }
1048
+ // Don't flush if initialization failed
1049
+ if (this.initializationFailed) {
1050
+ Logger.warn('Journium: flush() call rejected - initialization failed');
1051
+ return;
1052
+ }
914
1053
  if (this.queue.length === 0)
915
1054
  return;
916
1055
  const events = [...this.queue];
@@ -962,7 +1101,7 @@ class PageviewTracker {
962
1101
  * Start automatic autocapture for pageviews
963
1102
  * @returns void
964
1103
  */
965
- startAutocapture() {
1104
+ startAutoPageviewTracking() {
966
1105
  this.capturePageview();
967
1106
  if (typeof window !== 'undefined') {
968
1107
  // Store original methods for cleanup
@@ -1024,6 +1163,31 @@ class AutocaptureTracker {
1024
1163
  ...options,
1025
1164
  };
1026
1165
  }
1166
+ /**
1167
+ * Update autocapture options and restart if currently active
1168
+ */
1169
+ updateOptions(options) {
1170
+ const wasActive = this.isActive;
1171
+ // Stop if currently active
1172
+ if (wasActive) {
1173
+ this.stop();
1174
+ }
1175
+ // Update options
1176
+ this.options = {
1177
+ captureClicks: true,
1178
+ captureFormSubmits: true,
1179
+ captureFormChanges: true,
1180
+ captureTextSelection: false,
1181
+ ignoreClasses: ['journium-ignore'],
1182
+ ignoreElements: ['script', 'style', 'noscript'],
1183
+ captureContentText: true,
1184
+ ...options,
1185
+ };
1186
+ // Restart if it was active before
1187
+ if (wasActive) {
1188
+ this.start();
1189
+ }
1190
+ }
1027
1191
  start() {
1028
1192
  if (!isBrowser() || this.isActive) {
1029
1193
  return;
@@ -1353,14 +1517,22 @@ class AutocaptureTracker {
1353
1517
 
1354
1518
  class JourniumAnalytics {
1355
1519
  constructor(config) {
1356
- var _a, _b;
1520
+ this.autocaptureStarted = false;
1357
1521
  this.config = config;
1358
1522
  this.client = new JourniumClient(config);
1359
1523
  this.pageviewTracker = new PageviewTracker(this.client);
1360
- const autocaptureOptions = this.resolveAutocaptureOptions((_a = config.options) === null || _a === void 0 ? void 0 : _a.autocapture);
1361
- this.autocaptureTracker = new AutocaptureTracker(this.client, autocaptureOptions);
1362
- // Store resolved autocapture state for startAutocapture method
1363
- this.autocaptureEnabled = ((_b = config.options) === null || _b === void 0 ? void 0 : _b.autocapture) !== false;
1524
+ // Initialize autocapture tracker with effective options (may include cached remote options)
1525
+ // This ensures we use the correct initial state even if cached remote options exist
1526
+ const initialEffectiveOptions = this.client.getEffectiveOptions();
1527
+ const initialAutocaptureOptions = this.resolveAutocaptureOptions(initialEffectiveOptions.autocapture);
1528
+ this.autocaptureTracker = new AutocaptureTracker(this.client, initialAutocaptureOptions);
1529
+ // Listen for options changes (e.g., when fresh remote options are fetched)
1530
+ this.unsubscribeOptionsChange = this.client.onOptionsChange((effectiveOptions) => {
1531
+ this.handleOptionsChange(effectiveOptions);
1532
+ });
1533
+ // Start automatic autocapture immediately if initial options support it
1534
+ // This handles cached remote options or local options with autocapture enabled
1535
+ this.startAutocaptureIfEnabled(initialEffectiveOptions);
1364
1536
  }
1365
1537
  resolveAutocaptureOptions(autocapture) {
1366
1538
  if (autocapture === false) {
@@ -1389,19 +1561,86 @@ class JourniumAnalytics {
1389
1561
  this.pageviewTracker.capturePageview(properties);
1390
1562
  }
1391
1563
  startAutocapture() {
1392
- // Check if automatic pageview tracking is enabled (defaults to true)
1564
+ // Always check effective options (which may include remote options)
1393
1565
  const effectiveOptions = this.client.getEffectiveOptions();
1394
- const autoTrackPageviews = effectiveOptions.autoTrackPageviews !== false;
1566
+ // Only enable if effectiveOptions are loaded and autoTrackPageviews is not explicitly false
1567
+ const autoTrackPageviews = effectiveOptions && Object.keys(effectiveOptions).length > 0
1568
+ ? effectiveOptions.autoTrackPageviews !== false
1569
+ : false;
1570
+ const autocaptureEnabled = effectiveOptions && Object.keys(effectiveOptions).length > 0
1571
+ ? effectiveOptions.autocapture !== false
1572
+ : false;
1573
+ // Update autocapture tracker options if they've changed
1574
+ const autocaptureOptions = this.resolveAutocaptureOptions(effectiveOptions.autocapture);
1575
+ this.autocaptureTracker.updateOptions(autocaptureOptions);
1395
1576
  if (autoTrackPageviews) {
1396
- this.pageviewTracker.startAutocapture();
1577
+ this.pageviewTracker.startAutoPageviewTracking();
1397
1578
  }
1398
- if (this.autocaptureEnabled) {
1579
+ if (autocaptureEnabled) {
1399
1580
  this.autocaptureTracker.start();
1400
1581
  }
1582
+ this.autocaptureStarted = true;
1401
1583
  }
1402
1584
  stopAutocapture() {
1403
1585
  this.pageviewTracker.stopAutocapture();
1404
1586
  this.autocaptureTracker.stop();
1587
+ this.autocaptureStarted = false;
1588
+ }
1589
+ /**
1590
+ * Automatically start autocapture if enabled in options
1591
+ * Handles both initial options and empty options during remote-first initialization
1592
+ */
1593
+ startAutocaptureIfEnabled(effectiveOptions) {
1594
+ // Skip if autocapture was already started manually
1595
+ if (this.autocaptureStarted) {
1596
+ return;
1597
+ }
1598
+ // During remote-first initialization, effective options might be empty initially
1599
+ // Only auto-start if we have actual options loaded, not empty options
1600
+ const hasActualOptions = effectiveOptions && Object.keys(effectiveOptions).length > 0;
1601
+ if (hasActualOptions) {
1602
+ // Use same logic as manual startAutocapture() but only start automatically
1603
+ const autoTrackPageviews = effectiveOptions.autoTrackPageviews !== false;
1604
+ const autocaptureEnabled = effectiveOptions.autocapture !== false;
1605
+ // Update autocapture tracker options
1606
+ const autocaptureOptions = this.resolveAutocaptureOptions(effectiveOptions.autocapture);
1607
+ this.autocaptureTracker.updateOptions(autocaptureOptions);
1608
+ if (autoTrackPageviews) {
1609
+ this.pageviewTracker.startAutoPageviewTracking();
1610
+ }
1611
+ if (autocaptureEnabled) {
1612
+ this.autocaptureTracker.start();
1613
+ }
1614
+ if (autoTrackPageviews || autocaptureEnabled) {
1615
+ this.autocaptureStarted = true;
1616
+ }
1617
+ }
1618
+ // If options are empty (during initialization), wait for options change callback
1619
+ }
1620
+ /**
1621
+ * Handle effective options change (e.g., when remote options are fetched)
1622
+ */
1623
+ handleOptionsChange(effectiveOptions) {
1624
+ // Stop current autocapture if it was already started
1625
+ if (this.autocaptureStarted) {
1626
+ this.pageviewTracker.stopAutocapture();
1627
+ this.autocaptureTracker.stop();
1628
+ this.autocaptureStarted = false;
1629
+ }
1630
+ // Evaluate if autocapture should be enabled with new options
1631
+ const autoTrackPageviews = effectiveOptions.autoTrackPageviews !== false;
1632
+ const autocaptureEnabled = effectiveOptions.autocapture !== false;
1633
+ // Update autocapture tracker options
1634
+ const autocaptureOptions = this.resolveAutocaptureOptions(effectiveOptions.autocapture);
1635
+ this.autocaptureTracker.updateOptions(autocaptureOptions);
1636
+ // Start autocapture based on new options (even if it wasn't started before)
1637
+ if (autoTrackPageviews) {
1638
+ this.pageviewTracker.startAutoPageviewTracking();
1639
+ }
1640
+ if (autocaptureEnabled) {
1641
+ this.autocaptureTracker.start();
1642
+ }
1643
+ this.autocaptureStarted = autoTrackPageviews || autocaptureEnabled;
1405
1644
  }
1406
1645
  async flush() {
1407
1646
  return this.client.flush();
@@ -1409,9 +1648,18 @@ class JourniumAnalytics {
1409
1648
  getEffectiveOptions() {
1410
1649
  return this.client.getEffectiveOptions();
1411
1650
  }
1651
+ /**
1652
+ * Register a callback to be notified when effective options change
1653
+ */
1654
+ onOptionsChange(callback) {
1655
+ return this.client.onOptionsChange(callback);
1656
+ }
1412
1657
  destroy() {
1413
1658
  this.pageviewTracker.stopAutocapture();
1414
1659
  this.autocaptureTracker.stop();
1660
+ if (this.unsubscribeOptionsChange) {
1661
+ this.unsubscribeOptionsChange();
1662
+ }
1415
1663
  this.client.destroy();
1416
1664
  }
1417
1665
  }
@@ -1419,21 +1667,27 @@ const init = (config) => {
1419
1667
  return new JourniumAnalytics(config);
1420
1668
  };
1421
1669
 
1422
- const JourniumContext = createContext({ analytics: null, config: null, effectiveOptions: null });
1670
+ const JourniumContext = createContext(undefined);
1423
1671
  const JourniumProvider = ({ children, config, }) => {
1424
1672
  const [analytics, setAnalytics] = useState(null);
1425
1673
  const [effectiveOptions, setEffectiveOptions] = useState(null);
1426
1674
  useEffect(() => {
1427
1675
  const analyticsInstance = new JourniumAnalytics(config);
1428
- // Get effective options and check if autocapture is enabled
1429
- const effective = analyticsInstance.getEffectiveOptions();
1430
- setEffectiveOptions(effective);
1431
- const autocaptureEnabled = effective.autocapture !== false;
1432
- if (autocaptureEnabled) {
1433
- analyticsInstance.startAutocapture();
1434
- }
1676
+ // Get initial effective options (may be empty during remote-first initialization)
1677
+ const initialEffective = analyticsInstance.getEffectiveOptions();
1678
+ setEffectiveOptions(initialEffective);
1679
+ // Don't start autocapture immediately with potentially empty options
1680
+ // Let the analytics instance handle autocapture after initialization completes
1435
1681
  setAnalytics(analyticsInstance);
1682
+ // Listen for options changes (when remote options are fetched)
1683
+ // The JourniumAnalytics will automatically start autocapture when initialization completes
1684
+ const unsubscribe = analyticsInstance.onOptionsChange((newOptions) => {
1685
+ setEffectiveOptions(newOptions);
1686
+ });
1436
1687
  return () => {
1688
+ if (unsubscribe) {
1689
+ unsubscribe();
1690
+ }
1437
1691
  analyticsInstance.destroy();
1438
1692
  setAnalytics(null);
1439
1693
  setEffectiveOptions(null);