@leanbase-giangnd/js 0.2.2 → 0.2.4

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.
@@ -2846,7 +2846,7 @@ var leanbase = (function () {
2846
2846
  }
2847
2847
  };
2848
2848
 
2849
- var version = "0.2.2";
2849
+ var version = "0.2.4";
2850
2850
  var packageInfo = {
2851
2851
  version: version};
2852
2852
 
@@ -6106,34 +6106,49 @@ var leanbase = (function () {
6106
6106
  }
6107
6107
  return [id, node];
6108
6108
  };
6109
- this._getNode = id => this._rrweb.mirror.getNode(id);
6109
+ this._getNode = id => {
6110
+ // eslint-disable-next-line posthog-js/no-direct-undefined-check, posthog-js/no-direct-null-check
6111
+ if (id === null || id === undefined) {
6112
+ return null;
6113
+ }
6114
+ try {
6115
+ return this._rrweb.mirror.getNode(id) ?? null;
6116
+ } catch {
6117
+ return null;
6118
+ }
6119
+ };
6110
6120
  this._numberOfChanges = data => {
6111
6121
  return (data.removes?.length ?? 0) + (data.attributes?.length ?? 0) + (data.texts?.length ?? 0) + (data.adds?.length ?? 0);
6112
6122
  };
6113
6123
  this.throttleMutations = event => {
6114
- if (event.type !== INCREMENTAL_SNAPSHOT_EVENT_TYPE || event.data.source !== MUTATION_SOURCE_TYPE) {
6124
+ try {
6125
+ if (event.type !== INCREMENTAL_SNAPSHOT_EVENT_TYPE || event.data.source !== MUTATION_SOURCE_TYPE) {
6126
+ return event;
6127
+ }
6128
+ const data = event.data;
6129
+ const initialMutationCount = this._numberOfChanges(data);
6130
+ if (data.attributes) {
6131
+ // Most problematic mutations come from attrs where the style or minor properties are changed rapidly
6132
+ data.attributes = data.attributes.filter(attr => {
6133
+ const id = attr?.id;
6134
+ if (typeof id !== 'number') {
6135
+ return true;
6136
+ }
6137
+ const [nodeId] = this._getNodeOrRelevantParent(id);
6138
+ const isRateLimited = this._rateLimiter.consumeRateLimit(nodeId);
6139
+ return !isRateLimited;
6140
+ });
6141
+ }
6142
+ // Check if every part of the mutation is empty in which case there is nothing to do
6143
+ const mutationCount = this._numberOfChanges(data);
6144
+ if (mutationCount === 0 && initialMutationCount !== mutationCount) {
6145
+ // If we have modified the mutation count and the remaining count is 0, then we don't need the event.
6146
+ return;
6147
+ }
6148
+ return event;
6149
+ } catch {
6115
6150
  return event;
6116
6151
  }
6117
- const data = event.data;
6118
- const initialMutationCount = this._numberOfChanges(data);
6119
- if (data.attributes) {
6120
- // Most problematic mutations come from attrs where the style or minor properties are changed rapidly
6121
- data.attributes = data.attributes.filter(attr => {
6122
- const [nodeId] = this._getNodeOrRelevantParent(attr.id);
6123
- const isRateLimited = this._rateLimiter.consumeRateLimit(nodeId);
6124
- if (isRateLimited) {
6125
- return false;
6126
- }
6127
- return attr;
6128
- });
6129
- }
6130
- // Check if every part of the mutation is empty in which case there is nothing to do
6131
- const mutationCount = this._numberOfChanges(data);
6132
- if (mutationCount === 0 && initialMutationCount !== mutationCount) {
6133
- // If we have modified the mutation count and the remaining count is 0, then we don't need the event.
6134
- return;
6135
- }
6136
- return event;
6137
6152
  };
6138
6153
  const configuredBucketSize = this._options.bucketSize ?? 100;
6139
6154
  const effectiveBucketSize = Math.max(configuredBucketSize - 1, 1);
@@ -6371,6 +6386,9 @@ var leanbase = (function () {
6371
6386
  */
6372
6387
  this._queuedRRWebEvents = [];
6373
6388
  this._isIdle = 'unknown';
6389
+ // Replay should not process rrweb emits until all critical dependencies are ready.
6390
+ this._isFullyReady = false;
6391
+ this._loggedMissingEndpointFor = false;
6374
6392
  // we need to be able to check the state of the event and url triggers separately
6375
6393
  // as we make some decisions based on them without referencing LinkedFlag etc
6376
6394
  this._triggerMatching = new PendingTriggerMatching();
@@ -6551,7 +6569,11 @@ var leanbase = (function () {
6551
6569
  }
6552
6570
  }
6553
6571
  _tryAddCustomEvent(tag, payload) {
6554
- return this._tryRRWebMethod(newQueuedEvent(() => getRRWebRecord().addCustomEvent(tag, payload)));
6572
+ const rrwebRecord = getRRWebRecord();
6573
+ if (!rrwebRecord || typeof rrwebRecord.addCustomEvent !== 'function') {
6574
+ return false;
6575
+ }
6576
+ return this._tryRRWebMethod(newQueuedEvent(() => rrwebRecord.addCustomEvent(tag, payload)));
6555
6577
  }
6556
6578
  _pageViewFallBack() {
6557
6579
  try {
@@ -6597,7 +6619,11 @@ var leanbase = (function () {
6597
6619
  }
6598
6620
  }
6599
6621
  _tryTakeFullSnapshot() {
6600
- return this._tryRRWebMethod(newQueuedEvent(() => getRRWebRecord().takeFullSnapshot()));
6622
+ const rrwebRecord = getRRWebRecord();
6623
+ if (!rrwebRecord || typeof rrwebRecord.takeFullSnapshot !== 'function') {
6624
+ return false;
6625
+ }
6626
+ return this._tryRRWebMethod(newQueuedEvent(() => rrwebRecord.takeFullSnapshot()));
6601
6627
  }
6602
6628
  get _fullSnapshotIntervalMillis() {
6603
6629
  if (this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING && !['sampled', 'active'].includes(this.status)) {
@@ -6673,6 +6699,7 @@ var leanbase = (function () {
6673
6699
  return parsedConfig;
6674
6700
  }
6675
6701
  async start(startReason) {
6702
+ this._isFullyReady = false;
6676
6703
  const config = this._remoteConfig;
6677
6704
  if (!config) {
6678
6705
  logger.info('remote config must be stored in persistence before recording can start');
@@ -6707,6 +6734,14 @@ var leanbase = (function () {
6707
6734
  });
6708
6735
  this._makeSamplingDecision(this.sessionId);
6709
6736
  await this._startRecorder();
6737
+ // If rrweb failed to load/start, do not proceed further.
6738
+ // This prevents installing listeners that assume rrweb is active.
6739
+ if (!this.isStarted) {
6740
+ return;
6741
+ }
6742
+ // Only start processing rrweb emits once the ingestion endpoint is available.
6743
+ // If it isn't available, we must degrade to a no-op (never crash the host app).
6744
+ this._isFullyReady = this._canCaptureSnapshots();
6710
6745
  // calling addEventListener multiple times is safe and will not add duplicates
6711
6746
  addEventListener(win, 'beforeunload', this._onBeforeUnload);
6712
6747
  addEventListener(win, 'offline', this._onOffline);
@@ -6784,77 +6819,100 @@ var leanbase = (function () {
6784
6819
  this._queuedRRWebEvents = [];
6785
6820
  this._stopRrweb?.();
6786
6821
  this._stopRrweb = undefined;
6822
+ this._isFullyReady = false;
6787
6823
  logger.info('stopped');
6788
6824
  }
6825
+ _snapshotIngestionUrl() {
6826
+ const endpointFor = this._instance?.requestRouter?.endpointFor;
6827
+ if (typeof endpointFor !== 'function') {
6828
+ return null;
6829
+ }
6830
+ try {
6831
+ return endpointFor('api', this._endpoint);
6832
+ } catch {
6833
+ return null;
6834
+ }
6835
+ }
6836
+ _canCaptureSnapshots() {
6837
+ return !!this._snapshotIngestionUrl();
6838
+ }
6789
6839
  onRRwebEmit(rawEvent) {
6790
- this._processQueuedEvents();
6791
- if (!rawEvent || !isObject(rawEvent)) {
6840
+ // Never process rrweb emits until we're fully ready.
6841
+ if (!this._isFullyReady || !this.isStarted) {
6792
6842
  return;
6793
6843
  }
6794
- if (rawEvent.type === EventType$1.Meta) {
6795
- const href = this._maskUrl(rawEvent.data.href);
6796
- this._lastHref = href;
6797
- if (!href) {
6844
+ try {
6845
+ this._processQueuedEvents();
6846
+ if (!rawEvent || !isObject(rawEvent)) {
6798
6847
  return;
6799
6848
  }
6800
- rawEvent.data.href = href;
6801
- } else {
6802
- this._pageViewFallBack();
6803
- }
6804
- // Check if the URL matches any trigger patterns
6805
- this._urlTriggerMatching.checkUrlTriggerConditions(() => this._pauseRecording(), () => this._resumeRecording(), triggerType => this._activateTrigger(triggerType));
6806
- // always have to check if the URL is blocked really early,
6807
- // or you risk getting stuck in a loop
6808
- if (this._urlTriggerMatching.urlBlocked && !isRecordingPausedEvent(rawEvent)) {
6809
- return;
6810
- }
6811
- // we're processing a full snapshot, so we should reset the timer
6812
- if (rawEvent.type === EventType$1.FullSnapshot) {
6813
- this._scheduleFullSnapshot();
6814
- // Full snapshots reset rrweb's node IDs, so clear any logged node tracking
6815
- this._mutationThrottler?.reset();
6816
- }
6817
- // Clear the buffer if waiting for a trigger and only keep data from after the current full snapshot
6818
- // we always start trigger pending so need to wait for flags before we know if we're really pending
6819
- if (rawEvent.type === EventType$1.FullSnapshot && this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
6820
- this._clearBufferBeforeMostRecentMeta();
6821
- }
6822
- const throttledEvent = this._mutationThrottler ? this._mutationThrottler.throttleMutations(rawEvent) : rawEvent;
6823
- if (!throttledEvent) {
6824
- return;
6825
- }
6826
- // TODO: Re-add ensureMaxMessageSize once we are confident in it
6827
- const event = truncateLargeConsoleLogs(throttledEvent);
6828
- this._updateWindowAndSessionIds(event);
6829
- // When in an idle state we keep recording but don't capture the events,
6830
- // we don't want to return early if idle is 'unknown'
6831
- if (this._isIdle === true && !isSessionIdleEvent(event)) {
6832
- return;
6833
- }
6834
- if (isSessionIdleEvent(event)) {
6835
- // session idle events have a timestamp when rrweb sees them
6836
- // which can artificially lengthen a session
6837
- // we know when we detected it based on the payload and can correct the timestamp
6838
- const payload = event.data.payload;
6839
- if (payload) {
6840
- const lastActivity = payload.lastActivityTimestamp;
6841
- const threshold = payload.threshold;
6842
- event.timestamp = lastActivity + threshold;
6843
- }
6844
- }
6845
- const eventToSend = this._instance.config.session_recording?.compress_events ?? true ? compressEvent(event) : event;
6846
- const size = estimateSize(eventToSend);
6847
- const properties = {
6848
- $snapshot_bytes: size,
6849
- $snapshot_data: eventToSend,
6850
- $session_id: this._sessionId,
6851
- $window_id: this._windowId
6852
- };
6853
- if (this.status === DISABLED) {
6854
- this._clearBuffer();
6855
- return;
6849
+ if (rawEvent.type === EventType$1.Meta) {
6850
+ const href = this._maskUrl(rawEvent.data.href);
6851
+ this._lastHref = href;
6852
+ if (!href) {
6853
+ return;
6854
+ }
6855
+ rawEvent.data.href = href;
6856
+ } else {
6857
+ this._pageViewFallBack();
6858
+ }
6859
+ // Check if the URL matches any trigger patterns
6860
+ this._urlTriggerMatching.checkUrlTriggerConditions(() => this._pauseRecording(), () => this._resumeRecording(), triggerType => this._activateTrigger(triggerType));
6861
+ // always have to check if the URL is blocked really early,
6862
+ // or you risk getting stuck in a loop
6863
+ if (this._urlTriggerMatching.urlBlocked && !isRecordingPausedEvent(rawEvent)) {
6864
+ return;
6865
+ }
6866
+ // we're processing a full snapshot, so we should reset the timer
6867
+ if (rawEvent.type === EventType$1.FullSnapshot) {
6868
+ this._scheduleFullSnapshot();
6869
+ // Full snapshots reset rrweb's node IDs, so clear any logged node tracking
6870
+ this._mutationThrottler?.reset();
6871
+ }
6872
+ // Clear the buffer if waiting for a trigger and only keep data from after the current full snapshot
6873
+ // we always start trigger pending so need to wait for flags before we know if we're really pending
6874
+ if (rawEvent.type === EventType$1.FullSnapshot && this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
6875
+ this._clearBufferBeforeMostRecentMeta();
6876
+ }
6877
+ const throttledEvent = this._mutationThrottler ? this._mutationThrottler.throttleMutations(rawEvent) : rawEvent;
6878
+ if (!throttledEvent) {
6879
+ return;
6880
+ }
6881
+ // TODO: Re-add ensureMaxMessageSize once we are confident in it
6882
+ const event = truncateLargeConsoleLogs(throttledEvent);
6883
+ this._updateWindowAndSessionIds(event);
6884
+ // When in an idle state we keep recording but don't capture the events,
6885
+ // we don't want to return early if idle is 'unknown'
6886
+ if (this._isIdle === true && !isSessionIdleEvent(event)) {
6887
+ return;
6888
+ }
6889
+ if (isSessionIdleEvent(event)) {
6890
+ // session idle events have a timestamp when rrweb sees them
6891
+ // which can artificially lengthen a session
6892
+ // we know when we detected it based on the payload and can correct the timestamp
6893
+ const payload = event.data.payload;
6894
+ if (payload) {
6895
+ const lastActivity = payload.lastActivityTimestamp;
6896
+ const threshold = payload.threshold;
6897
+ event.timestamp = lastActivity + threshold;
6898
+ }
6899
+ }
6900
+ const eventToSend = this._instance.config.session_recording?.compress_events ?? true ? compressEvent(event) : event;
6901
+ const size = estimateSize(eventToSend);
6902
+ const properties = {
6903
+ $snapshot_bytes: size,
6904
+ $snapshot_data: eventToSend,
6905
+ $session_id: this._sessionId,
6906
+ $window_id: this._windowId
6907
+ };
6908
+ if (this.status === DISABLED) {
6909
+ this._clearBuffer();
6910
+ return;
6911
+ }
6912
+ this._captureSnapshotBuffered(properties);
6913
+ } catch (e) {
6914
+ logger.error('error processing rrweb event', e);
6856
6915
  }
6857
- this._captureSnapshotBuffered(properties);
6858
6916
  }
6859
6917
  get status() {
6860
6918
  return this._statusMatcher({
@@ -6932,6 +6990,16 @@ var leanbase = (function () {
6932
6990
  }, RECORDING_BUFFER_TIMEOUT);
6933
6991
  return this._buffer;
6934
6992
  }
6993
+ if (!this._canCaptureSnapshots()) {
6994
+ if (!this._loggedMissingEndpointFor) {
6995
+ this._loggedMissingEndpointFor = true;
6996
+ logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
6997
+ }
6998
+ this._flushBufferTimer = setTimeout(() => {
6999
+ this._flushBuffer();
7000
+ }, RECORDING_BUFFER_TIMEOUT);
7001
+ return this._buffer;
7002
+ }
6935
7003
  if (this._buffer.data.length > 0) {
6936
7004
  const snapshotEvents = splitBuffer(this._buffer);
6937
7005
  snapshotEvents.forEach(snapshotBuffer => {
@@ -6964,13 +7032,25 @@ var leanbase = (function () {
6964
7032
  }
6965
7033
  }
6966
7034
  _captureSnapshot(properties) {
6967
- // :TRICKY: Make sure we batch these requests, use a custom endpoint and don't truncate the strings.
6968
- this._instance.capture('$snapshot', properties, {
6969
- _url: this._instance.requestRouter.endpointFor('api', this._endpoint),
6970
- _noTruncate: true,
6971
- _batchKey: SESSION_RECORDING_BATCH_KEY,
6972
- skip_client_rate_limiting: true
6973
- });
7035
+ const url = this._snapshotIngestionUrl();
7036
+ if (!url) {
7037
+ if (!this._loggedMissingEndpointFor) {
7038
+ this._loggedMissingEndpointFor = true;
7039
+ logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
7040
+ }
7041
+ return;
7042
+ }
7043
+ try {
7044
+ // :TRICKY: Make sure we batch these requests, use a custom endpoint and don't truncate the strings.
7045
+ this._instance.capture('$snapshot', properties, {
7046
+ _url: url,
7047
+ _noTruncate: true,
7048
+ _batchKey: SESSION_RECORDING_BATCH_KEY,
7049
+ skip_client_rate_limiting: true
7050
+ });
7051
+ } catch (e) {
7052
+ logger.error('failed to capture snapshot', e);
7053
+ }
6974
7054
  }
6975
7055
  _snapshotUrl() {
6976
7056
  const host = this._instance.config.host || '';
@@ -7240,13 +7320,23 @@ var leanbase = (function () {
7240
7320
  }
7241
7321
  });
7242
7322
  const activePlugins = this._gatherRRWebPlugins();
7243
- this._stopRrweb = rrwebRecord({
7244
- emit: event => {
7245
- this.onRRwebEmit(event);
7246
- },
7247
- plugins: activePlugins,
7248
- ...sessionRecordingOptions
7249
- });
7323
+ try {
7324
+ this._stopRrweb = rrwebRecord({
7325
+ emit: event => {
7326
+ try {
7327
+ this.onRRwebEmit(event);
7328
+ } catch (e) {
7329
+ logger.error('error in rrweb emit handler', e);
7330
+ }
7331
+ },
7332
+ plugins: activePlugins,
7333
+ ...sessionRecordingOptions
7334
+ });
7335
+ } catch (e) {
7336
+ logger.error('failed to start rrweb recorder', e);
7337
+ this._stopRrweb = undefined;
7338
+ return;
7339
+ }
7250
7340
  // We reset the last activity timestamp, resetting the idle timer
7251
7341
  this._lastActivityTimestamp = Date.now();
7252
7342
  // stay unknown if we're not sure if we're idle or not
@@ -7432,18 +7522,25 @@ var leanbase = (function () {
7432
7522
  // If extensions provide an init function, use it. Otherwise, fall back to the local LazyLoadedSessionRecording
7433
7523
  if (assignableWindow.__PosthogExtensions__?.initSessionRecording) {
7434
7524
  if (!this._lazyLoadedSessionRecording) {
7435
- this._lazyLoadedSessionRecording = assignableWindow.__PosthogExtensions__?.initSessionRecording(this._instance);
7436
- this._lazyLoadedSessionRecording._forceAllowLocalhostNetworkCapture = this._forceAllowLocalhostNetworkCapture;
7525
+ const maybeRecording = assignableWindow.__PosthogExtensions__?.initSessionRecording(this._instance);
7526
+ if (maybeRecording && typeof maybeRecording.start === 'function') {
7527
+ this._lazyLoadedSessionRecording = maybeRecording;
7528
+ this._lazyLoadedSessionRecording._forceAllowLocalhostNetworkCapture = this._forceAllowLocalhostNetworkCapture;
7529
+ } else {
7530
+ log.warn('initSessionRecording was present but did not return a recorder instance; falling back to local recorder');
7531
+ }
7437
7532
  }
7438
- try {
7439
- const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
7440
- if (maybePromise && typeof maybePromise.catch === 'function') {
7441
- maybePromise.catch(e => logger$2.error('error starting session recording', e));
7533
+ if (this._lazyLoadedSessionRecording) {
7534
+ try {
7535
+ const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
7536
+ if (maybePromise && typeof maybePromise.catch === 'function') {
7537
+ maybePromise.catch(e => logger$2.error('error starting session recording', e));
7538
+ }
7539
+ } catch (e) {
7540
+ logger$2.error('error starting session recording', e);
7442
7541
  }
7443
- } catch (e) {
7444
- logger$2.error('error starting session recording', e);
7542
+ return;
7445
7543
  }
7446
- return;
7447
7544
  }
7448
7545
  if (!this._lazyLoadedSessionRecording) {
7449
7546
  this._lazyLoadedSessionRecording = new LazyLoadedSessionRecording(this._instance);
@@ -7558,6 +7655,10 @@ var leanbase = (function () {
7558
7655
  token
7559
7656
  });
7560
7657
  super(token, mergedConfig);
7658
+ this._remoteConfigLoadAttempted = false;
7659
+ this._remoteConfigResolved = false;
7660
+ this._featureFlagsResolved = false;
7661
+ this._maybeStartedSessionRecording = false;
7561
7662
  this.personProcessingSetOncePropertiesSent = false;
7562
7663
  this.isLoaded = false;
7563
7664
  this.config = mergedConfig;
@@ -7581,10 +7682,20 @@ var leanbase = (function () {
7581
7682
  this.replayAutocapture.startIfEnabled();
7582
7683
  if (this.sessionManager && this.config.cookieless_mode !== 'always') {
7583
7684
  this.sessionRecording = new SessionRecording(this);
7584
- this.sessionRecording.startIfEnabledOrStop();
7585
7685
  }
7686
+ // Start session recording only once flags + remote config have been resolved.
7687
+ // This matches the PostHog browser SDK where replay activation is driven by remote config and flags.
7586
7688
  if (this.config.preloadFeatureFlags !== false) {
7587
- this.reloadFeatureFlags();
7689
+ this.reloadFeatureFlags({
7690
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
7691
+ cb: _err => {
7692
+ this._featureFlagsResolved = true;
7693
+ this._maybeStartSessionRecording();
7694
+ }
7695
+ });
7696
+ } else {
7697
+ // If feature flags preload is explicitly disabled, treat this requirement as satisfied.
7698
+ this._featureFlagsResolved = true;
7588
7699
  }
7589
7700
  this.config.loaded?.(this);
7590
7701
  if (this.config.capture_pageview) {
@@ -7594,9 +7705,26 @@ var leanbase = (function () {
7594
7705
  }
7595
7706
  }, 1);
7596
7707
  }
7597
- addEventListener(document$1, 'DOMContentLoaded', () => {
7598
- this.loadRemoteConfig();
7599
- });
7708
+ const triggerRemoteConfigLoad = reason => {
7709
+ logger$2.info(`remote config load triggered via ${reason}`);
7710
+ void this.loadRemoteConfig();
7711
+ };
7712
+ if (document$1) {
7713
+ if (document$1.readyState === 'loading') {
7714
+ logger$2.info('remote config load deferred until DOMContentLoaded');
7715
+ const onDomReady = () => {
7716
+ document$1?.removeEventListener('DOMContentLoaded', onDomReady);
7717
+ triggerRemoteConfigLoad('dom');
7718
+ };
7719
+ addEventListener(document$1, 'DOMContentLoaded', onDomReady, {
7720
+ once: true
7721
+ });
7722
+ } else {
7723
+ triggerRemoteConfigLoad('immediate');
7724
+ }
7725
+ } else {
7726
+ triggerRemoteConfigLoad('no-document');
7727
+ }
7600
7728
  addEventListener(window, 'onpagehide' in self ? 'pagehide' : 'unload', this.capturePageLeave.bind(this), {
7601
7729
  passive: false
7602
7730
  });
@@ -7633,11 +7761,19 @@ var leanbase = (function () {
7633
7761
  }
7634
7762
  }
7635
7763
  async loadRemoteConfig() {
7636
- if (!this.isRemoteConfigLoaded) {
7764
+ if (this._remoteConfigLoadAttempted) {
7765
+ return;
7766
+ }
7767
+ this._remoteConfigLoadAttempted = true;
7768
+ try {
7637
7769
  const remoteConfig = await this.reloadRemoteConfigAsync();
7638
7770
  if (remoteConfig) {
7639
7771
  this.onRemoteConfig(remoteConfig);
7640
7772
  }
7773
+ } finally {
7774
+ // Regardless of success/failure, we consider remote config "resolved" so replay isn't blocked forever.
7775
+ this._remoteConfigResolved = true;
7776
+ this._maybeStartSessionRecording();
7641
7777
  }
7642
7778
  }
7643
7779
  onRemoteConfig(config) {
@@ -7650,6 +7786,26 @@ var leanbase = (function () {
7650
7786
  this.isRemoteConfigLoaded = true;
7651
7787
  this.replayAutocapture?.onRemoteConfig(config);
7652
7788
  this.sessionRecording?.onRemoteConfig(config);
7789
+ // Remote config has been applied; allow replay start if flags are also ready.
7790
+ this._remoteConfigResolved = true;
7791
+ this._maybeStartSessionRecording();
7792
+ }
7793
+ _maybeStartSessionRecording() {
7794
+ if (this._maybeStartedSessionRecording) {
7795
+ return;
7796
+ }
7797
+ if (!this.sessionRecording) {
7798
+ return;
7799
+ }
7800
+ if (!this._featureFlagsResolved || !this._remoteConfigResolved) {
7801
+ return;
7802
+ }
7803
+ this._maybeStartedSessionRecording = true;
7804
+ try {
7805
+ this.sessionRecording.startIfEnabledOrStop();
7806
+ } catch (e) {
7807
+ logger$2.error('Failed to start session recording', e);
7808
+ }
7653
7809
  }
7654
7810
  fetch(url, options) {
7655
7811
  const fetchFn = getFetch();