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