@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/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * uuidv7: A JavaScript implementation of UUID version 7
3
3
  *
4
- * Copyright 2021-2024 LiosK
4
+ * Copyright 2021-2025 LiosK
5
5
  *
6
6
  * @license Apache-2.0
7
7
  * @packageDocumentation
@@ -230,7 +230,10 @@ class V7Generator {
230
230
  * number generator should be cryptographically strong and securely seeded.
231
231
  */
232
232
  constructor(randomNumberGenerator) {
233
- this.timestamp = 0;
233
+ /**
234
+ * Biased by one to distinguish zero (uninitialized) and zero (UNIX epoch).
235
+ */
236
+ this.timestamp_biased = 0;
234
237
  this.counter = 0;
235
238
  this.random = randomNumberGenerator !== null && randomNumberGenerator !== void 0 ? randomNumberGenerator : getDefaultRandom();
236
239
  }
@@ -276,13 +279,13 @@ class V7Generator {
276
279
  *
277
280
  * @param rollbackAllowance - The amount of `unixTsMs` rollback that is
278
281
  * considered significant. A suggested value is `10_000` (milliseconds).
279
- * @throws RangeError if `unixTsMs` is not a 48-bit positive integer.
282
+ * @throws RangeError if `unixTsMs` is not a 48-bit unsigned integer.
280
283
  */
281
284
  generateOrResetCore(unixTsMs, rollbackAllowance) {
282
285
  let value = this.generateOrAbortCore(unixTsMs, rollbackAllowance);
283
286
  if (value === undefined) {
284
287
  // reset state and resume
285
- this.timestamp = 0;
288
+ this.timestamp_biased = 0;
286
289
  value = this.generateOrAbortCore(unixTsMs, rollbackAllowance);
287
290
  }
288
291
  return value;
@@ -296,28 +299,29 @@ class V7Generator {
296
299
  *
297
300
  * @param rollbackAllowance - The amount of `unixTsMs` rollback that is
298
301
  * considered significant. A suggested value is `10_000` (milliseconds).
299
- * @throws RangeError if `unixTsMs` is not a 48-bit positive integer.
302
+ * @throws RangeError if `unixTsMs` is not a 48-bit unsigned integer.
300
303
  */
301
304
  generateOrAbortCore(unixTsMs, rollbackAllowance) {
302
305
  const MAX_COUNTER = 4398046511103;
303
306
  if (!Number.isInteger(unixTsMs) ||
304
- unixTsMs < 1 ||
307
+ unixTsMs < 0 ||
305
308
  unixTsMs > 281474976710655) {
306
- throw new RangeError("`unixTsMs` must be a 48-bit positive integer");
309
+ throw new RangeError("`unixTsMs` must be a 48-bit unsigned integer");
307
310
  }
308
311
  else if (rollbackAllowance < 0 || rollbackAllowance > 281474976710655) {
309
312
  throw new RangeError("`rollbackAllowance` out of reasonable range");
310
313
  }
311
- if (unixTsMs > this.timestamp) {
312
- this.timestamp = unixTsMs;
314
+ unixTsMs++;
315
+ if (unixTsMs > this.timestamp_biased) {
316
+ this.timestamp_biased = unixTsMs;
313
317
  this.resetCounter();
314
318
  }
315
- else if (unixTsMs + rollbackAllowance >= this.timestamp) {
319
+ else if (unixTsMs + rollbackAllowance >= this.timestamp_biased) {
316
320
  // go on with previous timestamp if new one is not much smaller
317
321
  this.counter++;
318
322
  if (this.counter > MAX_COUNTER) {
319
323
  // increment timestamp at counter overflow
320
- this.timestamp++;
324
+ this.timestamp_biased++;
321
325
  this.resetCounter();
322
326
  }
323
327
  }
@@ -325,7 +329,7 @@ class V7Generator {
325
329
  // abort if clock went backwards to unbearable extent
326
330
  return undefined;
327
331
  }
328
- return UUID.fromFieldsV7(this.timestamp, Math.trunc(this.counter / 2 ** 30), this.counter & (2 ** 30 - 1), this.random.nextUint32());
332
+ return UUID.fromFieldsV7(this.timestamp_biased - 1, Math.trunc(this.counter / 2 ** 30), this.counter & (2 ** 30 - 1), this.random.nextUint32());
329
333
  }
330
334
  /** Initializes the counter at a 42-bit random integer. */
331
335
  resetCounter() {
@@ -701,6 +705,9 @@ class JourniumClient {
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 @@ class JourniumClient {
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 @@ class JourniumClient {
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 @@ class JourniumClient {
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 @@ class JourniumClient {
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 @@ class JourniumClient {
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 @@ class JourniumClient {
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 @@ class PageviewTracker {
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 @@ class JourniumAnalytics {
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();