@journium/js 1.1.1 → 1.2.1

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/journium.js CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * uuidv7: A JavaScript implementation of UUID version 7
7
7
  *
8
- * Copyright 2021-2024 LiosK
8
+ * Copyright 2021-2025 LiosK
9
9
  *
10
10
  * @license Apache-2.0
11
11
  * @packageDocumentation
@@ -234,7 +234,10 @@
234
234
  * number generator should be cryptographically strong and securely seeded.
235
235
  */
236
236
  constructor(randomNumberGenerator) {
237
- this.timestamp = 0;
237
+ /**
238
+ * Biased by one to distinguish zero (uninitialized) and zero (UNIX epoch).
239
+ */
240
+ this.timestamp_biased = 0;
238
241
  this.counter = 0;
239
242
  this.random = randomNumberGenerator !== null && randomNumberGenerator !== void 0 ? randomNumberGenerator : getDefaultRandom();
240
243
  }
@@ -280,13 +283,13 @@
280
283
  *
281
284
  * @param rollbackAllowance - The amount of `unixTsMs` rollback that is
282
285
  * considered significant. A suggested value is `10_000` (milliseconds).
283
- * @throws RangeError if `unixTsMs` is not a 48-bit positive integer.
286
+ * @throws RangeError if `unixTsMs` is not a 48-bit unsigned integer.
284
287
  */
285
288
  generateOrResetCore(unixTsMs, rollbackAllowance) {
286
289
  let value = this.generateOrAbortCore(unixTsMs, rollbackAllowance);
287
290
  if (value === undefined) {
288
291
  // reset state and resume
289
- this.timestamp = 0;
292
+ this.timestamp_biased = 0;
290
293
  value = this.generateOrAbortCore(unixTsMs, rollbackAllowance);
291
294
  }
292
295
  return value;
@@ -300,28 +303,29 @@
300
303
  *
301
304
  * @param rollbackAllowance - The amount of `unixTsMs` rollback that is
302
305
  * considered significant. A suggested value is `10_000` (milliseconds).
303
- * @throws RangeError if `unixTsMs` is not a 48-bit positive integer.
306
+ * @throws RangeError if `unixTsMs` is not a 48-bit unsigned integer.
304
307
  */
305
308
  generateOrAbortCore(unixTsMs, rollbackAllowance) {
306
309
  const MAX_COUNTER = 4398046511103;
307
310
  if (!Number.isInteger(unixTsMs) ||
308
- unixTsMs < 1 ||
311
+ unixTsMs < 0 ||
309
312
  unixTsMs > 281474976710655) {
310
- throw new RangeError("`unixTsMs` must be a 48-bit positive integer");
313
+ throw new RangeError("`unixTsMs` must be a 48-bit unsigned integer");
311
314
  }
312
315
  else if (rollbackAllowance < 0 || rollbackAllowance > 281474976710655) {
313
316
  throw new RangeError("`rollbackAllowance` out of reasonable range");
314
317
  }
315
- if (unixTsMs > this.timestamp) {
316
- this.timestamp = unixTsMs;
318
+ unixTsMs++;
319
+ if (unixTsMs > this.timestamp_biased) {
320
+ this.timestamp_biased = unixTsMs;
317
321
  this.resetCounter();
318
322
  }
319
- else if (unixTsMs + rollbackAllowance >= this.timestamp) {
323
+ else if (unixTsMs + rollbackAllowance >= this.timestamp_biased) {
320
324
  // go on with previous timestamp if new one is not much smaller
321
325
  this.counter++;
322
326
  if (this.counter > MAX_COUNTER) {
323
327
  // increment timestamp at counter overflow
324
- this.timestamp++;
328
+ this.timestamp_biased++;
325
329
  this.resetCounter();
326
330
  }
327
331
  }
@@ -329,7 +333,7 @@
329
333
  // abort if clock went backwards to unbearable extent
330
334
  return undefined;
331
335
  }
332
- return UUID.fromFieldsV7(this.timestamp, Math.trunc(this.counter / 2 ** 30), this.counter & (2 ** 30 - 1), this.random.nextUint32());
336
+ return UUID.fromFieldsV7(this.timestamp_biased - 1, Math.trunc(this.counter / 2 ** 30), this.counter & (2 ** 30 - 1), this.random.nextUint32());
333
337
  }
334
338
  /** Initializes the counter at a 42-bit random integer. */
335
339
  resetCounter() {
@@ -701,6 +705,9 @@
701
705
  this.queue = [];
702
706
  this.stagedEvents = [];
703
707
  this.flushTimer = null;
708
+ this.remoteOptionsRefreshTimer = null;
709
+ this.isRefreshing = false;
710
+ this.lastRemoteOptions = null;
704
711
  this.initializationComplete = false;
705
712
  this.initializationFailed = false;
706
713
  this.optionsChangeCallbacks = new Set();
@@ -754,61 +761,24 @@
754
761
  }
755
762
  }
756
763
  async initializeAsync() {
757
- var _a;
758
764
  try {
759
765
  Logger.log('Journium: Starting initialization - fetching fresh remote config...');
760
- // Step 1: Try to fetch fresh remote config with timeout and retry
761
766
  const remoteOptions = await this.fetchRemoteOptionsWithRetry();
762
- if (remoteOptions) {
763
- // Step 2: Cache the fresh remote config
764
- this.saveCachedOptions(remoteOptions);
765
- // Step 3: Merge local options over remote config (local overrides remote)
766
- if (this.config.options) {
767
- this.effectiveOptions = mergeOptions(this.config.options, remoteOptions);
768
- Logger.log('Journium: Using fresh remote config merged with local options:', this.effectiveOptions);
769
- }
770
- else {
771
- this.effectiveOptions = remoteOptions;
772
- Logger.log('Journium: Using fresh remote config:', this.effectiveOptions);
773
- }
774
- }
775
- else {
776
- // Step 4: Fallback to cached config if fresh fetch failed
777
- /* const cachedRemoteOptions = this.loadCachedOptions();
778
-
779
- if (cachedRemoteOptions) {
780
- if (this.config.options) {
781
- this.effectiveOptions = mergeOptions(this.config.options, cachedRemoteOptions);
782
- Logger.log('Journium: Fresh config failed, using cached remote config merged with local options:', this.effectiveOptions);
783
- } else {
784
- this.effectiveOptions = cachedRemoteOptions;
785
- Logger.log('Journium: Fresh config failed, using cached remote config:', this.effectiveOptions);
786
- }
787
- } else {
788
- // Step 5: No remote config and no cached config - initialization fails
789
- Logger.error('Journium: Initialization failed - no remote config available and no cached config found');
790
- this.initializationFailed = true;
791
- this.initializationComplete = false;
792
- return;
793
- } */
794
- }
795
- // Step 6: Update identity manager session timeout if provided
796
- if (this.effectiveOptions.sessionTimeout) {
797
- this.identityManager.updateSessionTimeout(this.effectiveOptions.sessionTimeout);
767
+ if (!remoteOptions) {
768
+ Logger.error('Journium: Initialization failed - no remote config available');
769
+ this.initializationFailed = true;
770
+ return;
798
771
  }
799
- // Step 7: Update Logger debug setting
800
- Logger.setDebug((_a = this.effectiveOptions.debug) !== null && _a !== void 0 ? _a : false);
801
- // Step 8: Mark initialization as complete
772
+ this.applyRemoteOptions(remoteOptions);
773
+ Logger.log('Journium: Effective options after init:', this.effectiveOptions);
802
774
  this.initializationComplete = true;
803
775
  this.initializationFailed = false;
804
- // Step 9: Process any staged events
805
776
  this.processStagedEvents();
806
- // Step 10: Start flush timer
807
777
  if (this.effectiveOptions.flushInterval && this.effectiveOptions.flushInterval > 0) {
808
778
  this.startFlushTimer();
809
779
  }
810
- Logger.log('Journium: Initialization complete with options:', this.effectiveOptions);
811
- // Step 11: Notify callbacks about options
780
+ this.startRemoteOptionsRefreshTimer();
781
+ Logger.log('Journium: Initialization complete');
812
782
  this.notifyOptionsChange();
813
783
  }
814
784
  catch (error) {
@@ -879,35 +849,21 @@
879
849
  processStagedEvents() {
880
850
  if (this.stagedEvents.length === 0)
881
851
  return;
852
+ if (this.ingestionPaused) {
853
+ Logger.warn(`Journium: Ingestion is paused — discarding ${this.stagedEvents.length} staged events`);
854
+ this.stagedEvents = [];
855
+ return;
856
+ }
882
857
  Logger.log(`Journium: Processing ${this.stagedEvents.length} staged events`);
883
- // Move staged events to main queue, adding identity properties now
884
- const identity = this.identityManager.getIdentity();
885
- const userAgentInfo = this.identityManager.getUserAgentInfo();
886
858
  for (const stagedEvent of this.stagedEvents) {
887
- // Add identity properties that weren't available during staging
888
- const eventWithIdentity = {
859
+ this.queue.push({
889
860
  ...stagedEvent,
890
- properties: {
891
- $device_id: identity === null || identity === void 0 ? void 0 : identity.$device_id,
892
- distinct_id: identity === null || identity === void 0 ? void 0 : identity.distinct_id,
893
- $session_id: identity === null || identity === void 0 ? void 0 : identity.$session_id,
894
- $is_identified: (identity === null || identity === void 0 ? void 0 : identity.$user_state) === 'identified',
895
- $current_url: typeof window !== 'undefined' ? window.location.href : '',
896
- $pathname: typeof window !== 'undefined' ? window.location.pathname : '',
897
- ...userAgentInfo,
898
- $lib_version: '0.1.0', // TODO: Get from package.json
899
- $platform: 'web',
900
- ...stagedEvent.properties, // Original properties override system properties
901
- },
902
- };
903
- this.queue.push(eventWithIdentity);
861
+ properties: this.buildIdentityProperties(stagedEvent.properties),
862
+ });
904
863
  }
905
- // Clear staged events
906
864
  this.stagedEvents = [];
907
865
  Logger.log('Journium: Staged events processed and moved to main queue');
908
- // Check if we should flush immediately
909
866
  if (this.queue.length >= this.effectiveOptions.flushAt) {
910
- // console.log('1 Journium: Flushing events...'+JSON.stringify(this.effectiveOptions));
911
867
  this.flush();
912
868
  }
913
869
  }
@@ -915,12 +871,88 @@
915
871
  if (this.flushTimer) {
916
872
  clearInterval(this.flushTimer);
917
873
  }
918
- // Use universal setInterval (works in both browser and Node.js)
919
874
  this.flushTimer = setInterval(() => {
920
- // console.log('2 Journium: Flushing events...'+JSON.stringify(this.effectiveOptions));
921
875
  this.flush();
922
876
  }, this.effectiveOptions.flushInterval);
923
877
  }
878
+ startRemoteOptionsRefreshTimer() {
879
+ // Clear any existing timer to prevent duplicate intervals
880
+ if (this.remoteOptionsRefreshTimer) {
881
+ clearInterval(this.remoteOptionsRefreshTimer);
882
+ this.remoteOptionsRefreshTimer = null;
883
+ }
884
+ this.remoteOptionsRefreshTimer = setInterval(() => {
885
+ this.refreshRemoteOptions();
886
+ }, JourniumClient.REMOTE_OPTIONS_REFRESH_INTERVAL);
887
+ Logger.log(`Journium: Scheduling remote options refresh every ${JourniumClient.REMOTE_OPTIONS_REFRESH_INTERVAL}ms`);
888
+ }
889
+ async refreshRemoteOptions() {
890
+ if (this.isRefreshing) {
891
+ Logger.log('Journium: Remote options refresh already in progress, skipping');
892
+ return;
893
+ }
894
+ this.isRefreshing = true;
895
+ Logger.log('Journium: Periodic remote options refresh triggered');
896
+ try {
897
+ const remoteOptions = await this.fetchRemoteOptionsWithRetry();
898
+ if (!remoteOptions) {
899
+ Logger.warn('Journium: Periodic remote options refresh failed, keeping current options');
900
+ return;
901
+ }
902
+ const prevRemoteSnapshot = JSON.stringify(this.lastRemoteOptions);
903
+ const prevFlushInterval = this.effectiveOptions.flushInterval;
904
+ this.applyRemoteOptions(remoteOptions);
905
+ if (prevRemoteSnapshot === JSON.stringify(this.lastRemoteOptions)) {
906
+ Logger.log('Journium: Remote options unchanged after refresh, no update needed');
907
+ return;
908
+ }
909
+ Logger.log('Journium: Remote options updated after refresh:', this.effectiveOptions);
910
+ if (this.effectiveOptions.flushInterval !== prevFlushInterval) {
911
+ if (this.effectiveOptions.flushInterval && this.effectiveOptions.flushInterval > 0) {
912
+ this.startFlushTimer();
913
+ }
914
+ else if (this.flushTimer) {
915
+ clearInterval(this.flushTimer);
916
+ this.flushTimer = null;
917
+ }
918
+ }
919
+ this.notifyOptionsChange();
920
+ }
921
+ catch (error) {
922
+ Logger.error('Journium: Periodic remote options refresh encountered an error:', error);
923
+ }
924
+ finally {
925
+ this.isRefreshing = false;
926
+ }
927
+ }
928
+ applyRemoteOptions(remoteOptions) {
929
+ var _a;
930
+ this.lastRemoteOptions = remoteOptions;
931
+ this.effectiveOptions = this.config.options
932
+ ? mergeOptions(this.config.options, remoteOptions)
933
+ : remoteOptions;
934
+ this.saveCachedOptions(remoteOptions);
935
+ if (this.effectiveOptions.sessionTimeout) {
936
+ this.identityManager.updateSessionTimeout(this.effectiveOptions.sessionTimeout);
937
+ }
938
+ Logger.setDebug((_a = this.effectiveOptions.debug) !== null && _a !== void 0 ? _a : false);
939
+ }
940
+ buildIdentityProperties(userProperties = {}) {
941
+ const identity = this.identityManager.getIdentity();
942
+ const userAgentInfo = this.identityManager.getUserAgentInfo();
943
+ return {
944
+ $device_id: identity === null || identity === void 0 ? void 0 : identity.$device_id,
945
+ distinct_id: identity === null || identity === void 0 ? void 0 : identity.distinct_id,
946
+ $session_id: identity === null || identity === void 0 ? void 0 : identity.$session_id,
947
+ $is_identified: (identity === null || identity === void 0 ? void 0 : identity.$user_state) === 'identified',
948
+ $current_url: typeof window !== 'undefined' ? window.location.href : '',
949
+ $pathname: typeof window !== 'undefined' ? window.location.pathname : '',
950
+ ...userAgentInfo,
951
+ $lib_version: '0.1.0', // TODO: Get from package.json
952
+ $platform: 'web',
953
+ ...userProperties,
954
+ };
955
+ }
924
956
  async sendEvents(events) {
925
957
  if (!events.length)
926
958
  return;
@@ -980,9 +1012,7 @@
980
1012
  event,
981
1013
  properties: { ...properties }, // Only user properties for now
982
1014
  };
983
- // Stage events during initialization, add to queue after initialization
984
1015
  if (!this.initializationComplete) {
985
- // If initialization failed, reject events
986
1016
  if (this.initializationFailed) {
987
1017
  Logger.warn('Journium: track() call rejected - initialization failed');
988
1018
  return;
@@ -991,34 +1021,17 @@
991
1021
  Logger.log('Journium: Event staged during initialization', journiumEvent);
992
1022
  return;
993
1023
  }
994
- // If initialization failed, reject events
995
- if (this.initializationFailed) {
996
- Logger.warn('Journium: track() call rejected - initialization failed');
1024
+ if (this.ingestionPaused) {
1025
+ Logger.warn('Journium: Ingestion is paused — event dropped:', journiumEvent.event);
997
1026
  return;
998
1027
  }
999
- // Add identity properties for immediate events (after initialization)
1000
- const identity = this.identityManager.getIdentity();
1001
- const userAgentInfo = this.identityManager.getUserAgentInfo();
1002
1028
  const eventWithIdentity = {
1003
1029
  ...journiumEvent,
1004
- properties: {
1005
- $device_id: identity === null || identity === void 0 ? void 0 : identity.$device_id,
1006
- distinct_id: identity === null || identity === void 0 ? void 0 : identity.distinct_id,
1007
- $session_id: identity === null || identity === void 0 ? void 0 : identity.$session_id,
1008
- $is_identified: (identity === null || identity === void 0 ? void 0 : identity.$user_state) === 'identified',
1009
- $current_url: typeof window !== 'undefined' ? window.location.href : '',
1010
- $pathname: typeof window !== 'undefined' ? window.location.pathname : '',
1011
- ...userAgentInfo,
1012
- $lib_version: '0.1.0', // TODO: Get from package.json
1013
- $platform: 'web',
1014
- ...properties, // User-provided properties override system properties
1015
- },
1030
+ properties: this.buildIdentityProperties(properties),
1016
1031
  };
1017
1032
  this.queue.push(eventWithIdentity);
1018
1033
  Logger.log('Journium: Event tracked', eventWithIdentity);
1019
- // Only flush if we have effective options (after initialization)
1020
1034
  if (this.effectiveOptions.flushAt && this.queue.length >= this.effectiveOptions.flushAt) {
1021
- // console.log('3 Journium: Flushing events...'+JSON.stringify(this.effectiveOptions));
1022
1035
  this.flush();
1023
1036
  }
1024
1037
  }
@@ -1045,12 +1058,20 @@
1045
1058
  clearInterval(this.flushTimer);
1046
1059
  this.flushTimer = null;
1047
1060
  }
1061
+ if (this.remoteOptionsRefreshTimer) {
1062
+ clearInterval(this.remoteOptionsRefreshTimer);
1063
+ this.remoteOptionsRefreshTimer = null;
1064
+ }
1048
1065
  this.flush();
1049
1066
  }
1050
1067
  getEffectiveOptions() {
1051
1068
  return this.effectiveOptions;
1052
1069
  }
1070
+ get ingestionPaused() {
1071
+ return this.effectiveOptions['ingestionPaused'] === true;
1072
+ }
1053
1073
  }
1074
+ JourniumClient.REMOTE_OPTIONS_REFRESH_INTERVAL = 15 * 60 * 1000; // 15 minutes
1054
1075
 
1055
1076
  class PageviewTracker {
1056
1077
  constructor(client) {
@@ -1077,10 +1098,13 @@
1077
1098
  }
1078
1099
  /**
1079
1100
  * Start automatic autocapture for pageviews
1080
- * @returns void
1101
+ * @param captureInitialPageview - whether to fire a $pageview immediately on start (default: true).
1102
+ * Pass false when restarting after a remote options update to avoid a spurious pageview.
1081
1103
  */
1082
- startAutoPageviewTracking() {
1083
- this.capturePageview();
1104
+ startAutoPageviewTracking(captureInitialPageview = true) {
1105
+ if (captureInitialPageview) {
1106
+ this.capturePageview();
1107
+ }
1084
1108
  if (typeof window !== 'undefined') {
1085
1109
  // Store original methods for cleanup
1086
1110
  this.originalPushState = window.history.pushState;
@@ -1599,21 +1623,22 @@
1599
1623
  * Handle effective options change (e.g., when remote options are fetched)
1600
1624
  */
1601
1625
  handleOptionsChange(effectiveOptions) {
1602
- // Stop current autocapture if it was already started
1626
+ // If autocapture was never started before, this is the initial options application
1627
+ // (async init completed) — treat it like a page load and capture a pageview.
1628
+ // If it was already started, this is a periodic remote options update — only
1629
+ // re-register listeners without emitting a spurious pageview.
1630
+ const isFirstStart = !this.autocaptureStarted;
1603
1631
  if (this.autocaptureStarted) {
1604
1632
  this.pageviewTracker.stopAutocapture();
1605
1633
  this.autocaptureTracker.stop();
1606
1634
  this.autocaptureStarted = false;
1607
1635
  }
1608
- // Evaluate if autocapture should be enabled with new options
1609
1636
  const autoTrackPageviews = effectiveOptions.autoTrackPageviews !== false;
1610
1637
  const autocaptureEnabled = effectiveOptions.autocapture !== false;
1611
- // Update autocapture tracker options
1612
1638
  const autocaptureOptions = this.resolveAutocaptureOptions(effectiveOptions.autocapture);
1613
1639
  this.autocaptureTracker.updateOptions(autocaptureOptions);
1614
- // Start autocapture based on new options (even if it wasn't started before)
1615
1640
  if (autoTrackPageviews) {
1616
- this.pageviewTracker.startAutoPageviewTracking();
1641
+ this.pageviewTracker.startAutoPageviewTracking(isFirstStart);
1617
1642
  }
1618
1643
  if (autocaptureEnabled) {
1619
1644
  this.autocaptureTracker.start();