@leanbase-giangnd/js 0.2.3 → 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.3";
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');
@@ -6712,6 +6739,9 @@ var leanbase = (function () {
6712
6739
  if (!this.isStarted) {
6713
6740
  return;
6714
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();
6715
6745
  // calling addEventListener multiple times is safe and will not add duplicates
6716
6746
  addEventListener(win, 'beforeunload', this._onBeforeUnload);
6717
6747
  addEventListener(win, 'offline', this._onOffline);
@@ -6789,77 +6819,100 @@ var leanbase = (function () {
6789
6819
  this._queuedRRWebEvents = [];
6790
6820
  this._stopRrweb?.();
6791
6821
  this._stopRrweb = undefined;
6822
+ this._isFullyReady = false;
6792
6823
  logger.info('stopped');
6793
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
+ }
6794
6839
  onRRwebEmit(rawEvent) {
6795
- this._processQueuedEvents();
6796
- if (!rawEvent || !isObject(rawEvent)) {
6840
+ // Never process rrweb emits until we're fully ready.
6841
+ if (!this._isFullyReady || !this.isStarted) {
6797
6842
  return;
6798
6843
  }
6799
- if (rawEvent.type === EventType$1.Meta) {
6800
- const href = this._maskUrl(rawEvent.data.href);
6801
- this._lastHref = href;
6802
- if (!href) {
6844
+ try {
6845
+ this._processQueuedEvents();
6846
+ if (!rawEvent || !isObject(rawEvent)) {
6803
6847
  return;
6804
6848
  }
6805
- rawEvent.data.href = href;
6806
- } else {
6807
- this._pageViewFallBack();
6808
- }
6809
- // Check if the URL matches any trigger patterns
6810
- this._urlTriggerMatching.checkUrlTriggerConditions(() => this._pauseRecording(), () => this._resumeRecording(), triggerType => this._activateTrigger(triggerType));
6811
- // always have to check if the URL is blocked really early,
6812
- // or you risk getting stuck in a loop
6813
- if (this._urlTriggerMatching.urlBlocked && !isRecordingPausedEvent(rawEvent)) {
6814
- return;
6815
- }
6816
- // we're processing a full snapshot, so we should reset the timer
6817
- if (rawEvent.type === EventType$1.FullSnapshot) {
6818
- this._scheduleFullSnapshot();
6819
- // Full snapshots reset rrweb's node IDs, so clear any logged node tracking
6820
- this._mutationThrottler?.reset();
6821
- }
6822
- // Clear the buffer if waiting for a trigger and only keep data from after the current full snapshot
6823
- // we always start trigger pending so need to wait for flags before we know if we're really pending
6824
- if (rawEvent.type === EventType$1.FullSnapshot && this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
6825
- this._clearBufferBeforeMostRecentMeta();
6826
- }
6827
- const throttledEvent = this._mutationThrottler ? this._mutationThrottler.throttleMutations(rawEvent) : rawEvent;
6828
- if (!throttledEvent) {
6829
- return;
6830
- }
6831
- // TODO: Re-add ensureMaxMessageSize once we are confident in it
6832
- const event = truncateLargeConsoleLogs(throttledEvent);
6833
- this._updateWindowAndSessionIds(event);
6834
- // When in an idle state we keep recording but don't capture the events,
6835
- // we don't want to return early if idle is 'unknown'
6836
- if (this._isIdle === true && !isSessionIdleEvent(event)) {
6837
- return;
6838
- }
6839
- if (isSessionIdleEvent(event)) {
6840
- // session idle events have a timestamp when rrweb sees them
6841
- // which can artificially lengthen a session
6842
- // we know when we detected it based on the payload and can correct the timestamp
6843
- const payload = event.data.payload;
6844
- if (payload) {
6845
- const lastActivity = payload.lastActivityTimestamp;
6846
- const threshold = payload.threshold;
6847
- event.timestamp = lastActivity + threshold;
6848
- }
6849
- }
6850
- const eventToSend = this._instance.config.session_recording?.compress_events ?? true ? compressEvent(event) : event;
6851
- const size = estimateSize(eventToSend);
6852
- const properties = {
6853
- $snapshot_bytes: size,
6854
- $snapshot_data: eventToSend,
6855
- $session_id: this._sessionId,
6856
- $window_id: this._windowId
6857
- };
6858
- if (this.status === DISABLED) {
6859
- this._clearBuffer();
6860
- 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);
6861
6915
  }
6862
- this._captureSnapshotBuffered(properties);
6863
6916
  }
6864
6917
  get status() {
6865
6918
  return this._statusMatcher({
@@ -6937,6 +6990,16 @@ var leanbase = (function () {
6937
6990
  }, RECORDING_BUFFER_TIMEOUT);
6938
6991
  return this._buffer;
6939
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
+ }
6940
7003
  if (this._buffer.data.length > 0) {
6941
7004
  const snapshotEvents = splitBuffer(this._buffer);
6942
7005
  snapshotEvents.forEach(snapshotBuffer => {
@@ -6969,13 +7032,25 @@ var leanbase = (function () {
6969
7032
  }
6970
7033
  }
6971
7034
  _captureSnapshot(properties) {
6972
- // :TRICKY: Make sure we batch these requests, use a custom endpoint and don't truncate the strings.
6973
- this._instance.capture('$snapshot', properties, {
6974
- _url: this._instance.requestRouter.endpointFor('api', this._endpoint),
6975
- _noTruncate: true,
6976
- _batchKey: SESSION_RECORDING_BATCH_KEY,
6977
- skip_client_rate_limiting: true
6978
- });
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
+ }
6979
7054
  }
6980
7055
  _snapshotUrl() {
6981
7056
  const host = this._instance.config.host || '';
@@ -7248,7 +7323,11 @@ var leanbase = (function () {
7248
7323
  try {
7249
7324
  this._stopRrweb = rrwebRecord({
7250
7325
  emit: event => {
7251
- this.onRRwebEmit(event);
7326
+ try {
7327
+ this.onRRwebEmit(event);
7328
+ } catch (e) {
7329
+ logger.error('error in rrweb emit handler', e);
7330
+ }
7252
7331
  },
7253
7332
  plugins: activePlugins,
7254
7333
  ...sessionRecordingOptions