@sentry/replay 7.65.0-alpha.0 → 7.66.0-alpha.0

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/cjs/index.js CHANGED
@@ -5699,41 +5699,6 @@ function deleteSession() {
5699
5699
  }
5700
5700
  }
5701
5701
 
5702
- /**
5703
- * Given an initial timestamp and an expiry duration, checks to see if current
5704
- * time should be considered as expired.
5705
- */
5706
- function isExpired(
5707
- initialTime,
5708
- expiry,
5709
- targetTime = +new Date(),
5710
- ) {
5711
- // Always expired if < 0
5712
- if (initialTime === null || expiry === undefined || expiry < 0) {
5713
- return true;
5714
- }
5715
-
5716
- // Never expires if == 0
5717
- if (expiry === 0) {
5718
- return false;
5719
- }
5720
-
5721
- return initialTime + expiry <= targetTime;
5722
- }
5723
-
5724
- /**
5725
- * Checks to see if session is expired
5726
- */
5727
- function isSessionExpired(session, timeouts, targetTime = +new Date()) {
5728
- return (
5729
- // First, check that maximum session length has not been exceeded
5730
- isExpired(session.started, timeouts.maxSessionLife, targetTime) ||
5731
- // check that the idle timeout has not been exceeded (i.e. user has
5732
- // performed an action within the last `sessionIdleExpire` ms)
5733
- isExpired(session.lastActivity, timeouts.sessionIdleExpire, targetTime)
5734
- );
5735
- }
5736
-
5737
5702
  /**
5738
5703
  * Given a sample rate, returns true if replay should be sampled.
5739
5704
  *
@@ -5775,7 +5740,7 @@ function makeSession(session) {
5775
5740
  const lastActivity = session.lastActivity || now;
5776
5741
  const segmentId = session.segmentId || 0;
5777
5742
  const sampled = session.sampled;
5778
- const shouldRefresh = typeof session.shouldRefresh === 'boolean' ? session.shouldRefresh : true;
5743
+ const previousSessionId = session.previousSessionId;
5779
5744
 
5780
5745
  return {
5781
5746
  id,
@@ -5783,7 +5748,7 @@ function makeSession(session) {
5783
5748
  lastActivity,
5784
5749
  segmentId,
5785
5750
  sampled,
5786
- shouldRefresh,
5751
+ previousSessionId,
5787
5752
  };
5788
5753
  }
5789
5754
 
@@ -5799,10 +5764,14 @@ function getSessionSampleType(sessionSampleRate, allowBuffering) {
5799
5764
  * that all replays will be saved to as attachments. Currently, we only expect
5800
5765
  * one of these Sentry events per "replay session".
5801
5766
  */
5802
- function createSession({ sessionSampleRate, allowBuffering, stickySession = false }) {
5767
+ function createSession(
5768
+ { sessionSampleRate, allowBuffering, stickySession = false },
5769
+ { previousSessionId } = {},
5770
+ ) {
5803
5771
  const sampled = getSessionSampleType(sessionSampleRate, allowBuffering);
5804
5772
  const session = makeSession({
5805
5773
  sampled,
5774
+ previousSessionId,
5806
5775
  });
5807
5776
 
5808
5777
  if (stickySession) {
@@ -5839,49 +5808,83 @@ function fetchSession(traceInternals) {
5839
5808
  }
5840
5809
 
5841
5810
  /**
5842
- * Get or create a session
5811
+ * Given an initial timestamp and an expiry duration, checks to see if current
5812
+ * time should be considered as expired.
5843
5813
  */
5844
- function getSession({
5845
- timeouts,
5846
- currentSession,
5847
- stickySession,
5848
- sessionSampleRate,
5849
- allowBuffering,
5850
- traceInternals,
5851
- }) {
5852
- // If session exists and is passed, use it instead of always hitting session storage
5853
- const session = currentSession || (stickySession && fetchSession(traceInternals));
5854
-
5855
- if (session) {
5856
- // If there is a session, check if it is valid (e.g. "last activity" time
5857
- // should be within the "session idle time", and "session started" time is
5858
- // within "max session time").
5859
- const isExpired = isSessionExpired(session, timeouts);
5860
-
5861
- if (!isExpired || (allowBuffering && session.shouldRefresh)) {
5862
- return { type: 'saved', session };
5863
- } else if (!session.shouldRefresh) {
5864
- // This is the case if we have an error session that is completed
5865
- // (=triggered an error). Session will continue as session-based replay,
5866
- // and when this session is expired, it will not be renewed until user
5867
- // reloads.
5868
- const discardedSession = makeSession({ sampled: false });
5869
- logInfoNextTick('[Replay] Session should not be refreshed', traceInternals);
5870
- return { type: 'new', session: discardedSession };
5871
- } else {
5872
- logInfoNextTick('[Replay] Session has expired', traceInternals);
5873
- }
5874
- // Otherwise continue to create a new session
5814
+ function isExpired(
5815
+ initialTime,
5816
+ expiry,
5817
+ targetTime = +new Date(),
5818
+ ) {
5819
+ // Always expired if < 0
5820
+ if (initialTime === null || expiry === undefined || expiry < 0) {
5821
+ return true;
5875
5822
  }
5876
5823
 
5877
- const newSession = createSession({
5878
- stickySession,
5879
- sessionSampleRate,
5880
- allowBuffering,
5881
- });
5882
- logInfoNextTick('[Replay] Created new session', traceInternals);
5824
+ // Never expires if == 0
5825
+ if (expiry === 0) {
5826
+ return false;
5827
+ }
5828
+
5829
+ return initialTime + expiry <= targetTime;
5830
+ }
5831
+
5832
+ /**
5833
+ * Checks to see if session is expired
5834
+ */
5835
+ function isSessionExpired(session, timeouts, targetTime = +new Date()) {
5836
+ return (
5837
+ // First, check that maximum session length has not been exceeded
5838
+ isExpired(session.started, timeouts.maxSessionLife, targetTime) ||
5839
+ // check that the idle timeout has not been exceeded (i.e. user has
5840
+ // performed an action within the last `sessionIdleExpire` ms)
5841
+ isExpired(session.lastActivity, timeouts.sessionIdleExpire, targetTime)
5842
+ );
5843
+ }
5844
+
5845
+ /** If the session should be refreshed or not. */
5846
+ function shouldRefreshSession(session, timeouts) {
5847
+ // If not expired, all good, just keep the session
5848
+ if (!isSessionExpired(session, timeouts)) {
5849
+ return false;
5850
+ }
5851
+
5852
+ // If we are buffering & haven't ever flushed yet, always continue
5853
+ if (session.sampled === 'buffer' && session.segmentId === 0) {
5854
+ return false;
5855
+ }
5856
+
5857
+ return true;
5858
+ }
5859
+
5860
+ /**
5861
+ * Get or create a session, when initializing the replay.
5862
+ * Returns a session that may be unsampled.
5863
+ */
5864
+ function loadOrCreateSession(
5865
+ {
5866
+ timeouts,
5867
+ traceInternals,
5868
+ previousSessionId,
5869
+ }
5870
+
5871
+ ,
5872
+ sessionOptions,
5873
+ ) {
5874
+ const existingSession = sessionOptions.stickySession && fetchSession(traceInternals);
5875
+
5876
+ // No session exists yet, just create a new one
5877
+ if (!existingSession) {
5878
+ logInfoNextTick('[Replay] Creating new session', traceInternals);
5879
+ return createSession(sessionOptions, { previousSessionId });
5880
+ }
5881
+
5882
+ if (!shouldRefreshSession(existingSession, timeouts)) {
5883
+ return existingSession;
5884
+ }
5883
5885
 
5884
- return { type: 'new', session: newSession };
5886
+ logInfoNextTick('[Replay] Session in sessionStorage is expired, creating new one...');
5887
+ return createSession(sessionOptions, { previousSessionId: existingSession.id });
5885
5888
  }
5886
5889
 
5887
5890
  function isCustomEvent(event) {
@@ -8473,7 +8476,7 @@ class ReplayContainer {
8473
8476
  * Initializes the plugin based on sampling configuration. Should not be
8474
8477
  * called outside of constructor.
8475
8478
  */
8476
- initializeSampling() {
8479
+ initializeSampling(previousSessionId) {
8477
8480
  const { errorSampleRate, sessionSampleRate } = this._options;
8478
8481
 
8479
8482
  // If neither sample rate is > 0, then do nothing - user will need to call one of
@@ -8484,13 +8487,7 @@ class ReplayContainer {
8484
8487
 
8485
8488
  // Otherwise if there is _any_ sample rate set, try to load an existing
8486
8489
  // session, or create a new one.
8487
- const isSessionSampled = this._loadAndCheckSession();
8488
-
8489
- if (!isSessionSampled) {
8490
- // This should only occur if `errorSampleRate` is 0 and was unsampled for
8491
- // session-based replay. In this case there is nothing to do.
8492
- return;
8493
- }
8490
+ this._initializeSessionForSampling(previousSessionId);
8494
8491
 
8495
8492
  if (!this.session) {
8496
8493
  // This should not happen, something wrong has occurred
@@ -8498,14 +8495,16 @@ class ReplayContainer {
8498
8495
  return;
8499
8496
  }
8500
8497
 
8501
- if (this.session.sampled && this.session.sampled !== 'session') {
8502
- // If not sampled as session-based, then recording mode will be `buffer`
8503
- // Note that we don't explicitly check if `sampled === 'buffer'` because we
8504
- // could have sessions from Session storage that are still `error` from
8505
- // prior SDK version.
8506
- this.recordingMode = 'buffer';
8498
+ if (this.session.sampled === false) {
8499
+ // This should only occur if `errorSampleRate` is 0 and was unsampled for
8500
+ // session-based replay. In this case there is nothing to do.
8501
+ return;
8507
8502
  }
8508
8503
 
8504
+ // If segmentId > 0, it means we've previously already captured this session
8505
+ // In this case, we still want to continue in `session` recording mode
8506
+ this.recordingMode = this.session.sampled === 'buffer' && this.session.segmentId === 0 ? 'buffer' : 'session';
8507
+
8509
8508
  logInfoNextTick(
8510
8509
  `[Replay] Starting replay in ${this.recordingMode} mode`,
8511
8510
  this._options._experiments.traceInternals,
@@ -8532,19 +8531,19 @@ class ReplayContainer {
8532
8531
 
8533
8532
  logInfoNextTick('[Replay] Starting replay in session mode', this._options._experiments.traceInternals);
8534
8533
 
8535
- const previousSessionId = this.session && this.session.id;
8536
-
8537
- const { session } = getSession({
8538
- timeouts: this.timeouts,
8539
- stickySession: Boolean(this._options.stickySession),
8540
- currentSession: this.session,
8541
- // This is intentional: create a new session-based replay when calling `start()`
8542
- sessionSampleRate: 1,
8543
- allowBuffering: false,
8544
- traceInternals: this._options._experiments.traceInternals,
8545
- });
8534
+ const session = loadOrCreateSession(
8535
+ {
8536
+ timeouts: this.timeouts,
8537
+ traceInternals: this._options._experiments.traceInternals,
8538
+ },
8539
+ {
8540
+ stickySession: this._options.stickySession,
8541
+ // This is intentional: create a new session-based replay when calling `start()`
8542
+ sessionSampleRate: 1,
8543
+ allowBuffering: false,
8544
+ },
8545
+ );
8546
8546
 
8547
- session.previousSessionId = previousSessionId;
8548
8547
  this.session = session;
8549
8548
 
8550
8549
  this._initializeRecording();
@@ -8561,18 +8560,18 @@ class ReplayContainer {
8561
8560
 
8562
8561
  logInfoNextTick('[Replay] Starting replay in buffer mode', this._options._experiments.traceInternals);
8563
8562
 
8564
- const previousSessionId = this.session && this.session.id;
8565
-
8566
- const { session } = getSession({
8567
- timeouts: this.timeouts,
8568
- stickySession: Boolean(this._options.stickySession),
8569
- currentSession: this.session,
8570
- sessionSampleRate: 0,
8571
- allowBuffering: true,
8572
- traceInternals: this._options._experiments.traceInternals,
8573
- });
8563
+ const session = loadOrCreateSession(
8564
+ {
8565
+ timeouts: this.timeouts,
8566
+ traceInternals: this._options._experiments.traceInternals,
8567
+ },
8568
+ {
8569
+ stickySession: this._options.stickySession,
8570
+ sessionSampleRate: 0,
8571
+ allowBuffering: true,
8572
+ },
8573
+ );
8574
8574
 
8575
- session.previousSessionId = previousSessionId;
8576
8575
  this.session = session;
8577
8576
 
8578
8577
  this.recordingMode = 'buffer';
@@ -8629,15 +8628,18 @@ class ReplayContainer {
8629
8628
  return;
8630
8629
  }
8631
8630
 
8631
+ // We can't move `_isEnabled` after awaiting a flush, otherwise we can
8632
+ // enter into an infinite loop when `stop()` is called while flushing.
8633
+ this._isEnabled = false;
8634
+
8632
8635
  try {
8633
8636
  logInfo(
8634
- `[Replay] Stopping Replay${reason ? ` triggered by ${reason}` : ''}`,
8637
+ `[Replay] Stopping Replay${reason ? ` triggered by ${reason}` : ''} ${new Date().toISOString()} ${
8638
+ this._isEnabled
8639
+ }`,
8635
8640
  this._options._experiments.traceInternals,
8636
8641
  );
8637
8642
 
8638
- // We can't move `_isEnabled` after awaiting a flush, otherwise we can
8639
- // enter into an infinite loop when `stop()` is called while flushing.
8640
- this._isEnabled = false;
8641
8643
  this._removeListeners();
8642
8644
  this.stopRecording();
8643
8645
 
@@ -8683,7 +8685,7 @@ class ReplayContainer {
8683
8685
  * new DOM checkout.`
8684
8686
  */
8685
8687
  resume() {
8686
- if (!this._isPaused || !this._loadAndCheckSession()) {
8688
+ if (!this._isPaused || !this._checkSession()) {
8687
8689
  return;
8688
8690
  }
8689
8691
 
@@ -8731,16 +8733,6 @@ class ReplayContainer {
8731
8733
 
8732
8734
  // Once this session ends, we do not want to refresh it
8733
8735
  if (this.session) {
8734
- this.session.shouldRefresh = false;
8735
-
8736
- // It's possible that the session lifespan is > max session lifespan
8737
- // because we have been buffering beyond max session lifespan (we ignore
8738
- // expiration given that `shouldRefresh` is true). Since we flip
8739
- // `shouldRefresh`, the session could be considered expired due to
8740
- // lifespan, which is not what we want. Update session start date to be
8741
- // the current timestamp, so that session is not considered to be
8742
- // expired. This means that max replay duration can be MAX_SESSION_LIFE +
8743
- // (length of buffer), which we are ok with.
8744
8736
  this._updateUserActivity(activityTime);
8745
8737
  this._updateSessionActivity(activityTime);
8746
8738
  this._maybeSaveSession();
@@ -8791,7 +8783,7 @@ class ReplayContainer {
8791
8783
  if (!this._stopRecording) {
8792
8784
  // Create a new session, otherwise when the user action is flushed, it
8793
8785
  // will get rejected due to an expired session.
8794
- if (!this._loadAndCheckSession()) {
8786
+ if (!this._checkSession()) {
8795
8787
  return;
8796
8788
  }
8797
8789
 
@@ -8890,7 +8882,7 @@ class ReplayContainer {
8890
8882
 
8891
8883
  // --- There is recent user activity --- //
8892
8884
  // This will create a new session if expired, based on expiry length
8893
- if (!this._loadAndCheckSession()) {
8885
+ if (!this._checkSession()) {
8894
8886
  return;
8895
8887
  }
8896
8888
 
@@ -8992,6 +8984,7 @@ class ReplayContainer {
8992
8984
 
8993
8985
  // Need to set as enabled before we start recording, as `record()` can trigger a flush with a new checkout
8994
8986
  this._isEnabled = true;
8987
+ this._isPaused = false;
8995
8988
 
8996
8989
  this.startRecording();
8997
8990
  }
@@ -9007,39 +9000,62 @@ class ReplayContainer {
9007
9000
 
9008
9001
  /**
9009
9002
  * Loads (or refreshes) the current session.
9010
- * Returns false if session is not recorded.
9011
9003
  */
9012
- _loadAndCheckSession() {
9013
- const { type, session } = getSession({
9014
- timeouts: this.timeouts,
9015
- stickySession: Boolean(this._options.stickySession),
9016
- currentSession: this.session,
9017
- sessionSampleRate: this._options.sessionSampleRate,
9018
- allowBuffering: this._options.errorSampleRate > 0 || this.recordingMode === 'buffer',
9019
- traceInternals: this._options._experiments.traceInternals,
9020
- });
9004
+ _initializeSessionForSampling(previousSessionId) {
9005
+ // Whenever there is _any_ error sample rate, we always allow buffering
9006
+ // Because we decide on sampling when an error occurs, we need to buffer at all times if sampling for errors
9007
+ const allowBuffering = this._options.errorSampleRate > 0;
9008
+
9009
+ const session = loadOrCreateSession(
9010
+ {
9011
+ timeouts: this.timeouts,
9012
+ traceInternals: this._options._experiments.traceInternals,
9013
+ previousSessionId,
9014
+ },
9015
+ {
9016
+ stickySession: this._options.stickySession,
9017
+ sessionSampleRate: this._options.sessionSampleRate,
9018
+ allowBuffering,
9019
+ },
9020
+ );
9021
9021
 
9022
- // If session was newly created (i.e. was not loaded from storage), then
9023
- // enable flag to create the root replay
9024
- if (type === 'new') {
9025
- this.setInitialState();
9026
- }
9022
+ this.session = session;
9023
+ }
9027
9024
 
9028
- const currentSessionId = this.getSessionId();
9029
- if (session.id !== currentSessionId) {
9030
- session.previousSessionId = currentSessionId;
9025
+ /**
9026
+ * Checks and potentially refreshes the current session.
9027
+ * Returns false if session is not recorded.
9028
+ */
9029
+ _checkSession() {
9030
+ // If there is no session yet, we do not want to refresh anything
9031
+ // This should generally not happen, but to be safe....
9032
+ if (!this.session) {
9033
+ return false;
9031
9034
  }
9032
9035
 
9033
- this.session = session;
9036
+ const currentSession = this.session;
9034
9037
 
9035
- if (!this.session.sampled) {
9036
- void this.stop({ reason: 'session not refreshed' });
9038
+ if (shouldRefreshSession(currentSession, this.timeouts)) {
9039
+ void this._refreshSession(currentSession);
9037
9040
  return false;
9038
9041
  }
9039
9042
 
9040
9043
  return true;
9041
9044
  }
9042
9045
 
9046
+ /**
9047
+ * Refresh a session with a new one.
9048
+ * This stops the current session (without forcing a flush, as that would never work since we are expired),
9049
+ * and then does a new sampling based on the refreshed session.
9050
+ */
9051
+ async _refreshSession(session) {
9052
+ if (!this._isEnabled) {
9053
+ return;
9054
+ }
9055
+ await this.stop({ reason: 'refresh session' });
9056
+ this.initializeSampling(session.id);
9057
+ }
9058
+
9043
9059
  /**
9044
9060
  * Adds listeners to record events for the replay
9045
9061
  */
@@ -9296,7 +9312,9 @@ class ReplayContainer {
9296
9312
  * Should never be called directly, only by `flush`
9297
9313
  */
9298
9314
  async _runFlush() {
9299
- if (!this.session || !this.eventBuffer) {
9315
+ const replayId = this.getSessionId();
9316
+
9317
+ if (!this.session || !this.eventBuffer || !replayId) {
9300
9318
  (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.error('[Replay] No session or eventBuffer found to flush.');
9301
9319
  return;
9302
9320
  }
@@ -9316,13 +9334,15 @@ class ReplayContainer {
9316
9334
  return;
9317
9335
  }
9318
9336
 
9337
+ // if this changed in the meanwhile, e.g. because the session was refreshed or similar, we abort here
9338
+ if (replayId !== this.getSessionId()) {
9339
+ return;
9340
+ }
9341
+
9319
9342
  try {
9320
9343
  // This uses the data from the eventBuffer, so we need to call this before `finish()
9321
9344
  this._updateInitialTimestampFromEventBuffer();
9322
9345
 
9323
- // Note this empties the event buffer regardless of outcome of sending replay
9324
- const recordingData = await this.eventBuffer.finish();
9325
-
9326
9346
  const timestamp = Date.now();
9327
9347
 
9328
9348
  // Check total duration again, to avoid sending outdated stuff
@@ -9332,14 +9352,14 @@ class ReplayContainer {
9332
9352
  throw new Error('Session is too long, not sending replay');
9333
9353
  }
9334
9354
 
9335
- // NOTE: Copy values from instance members, as it's possible they could
9336
- // change before the flush finishes.
9337
- const replayId = this.session.id;
9338
9355
  const eventContext = this._popEventContext();
9339
9356
  // Always increment segmentId regardless of outcome of sending replay
9340
9357
  const segmentId = this.session.segmentId++;
9341
9358
  this._maybeSaveSession();
9342
9359
 
9360
+ // Note this empties the event buffer regardless of outcome of sending replay
9361
+ const recordingData = await this.eventBuffer.finish();
9362
+
9343
9363
  await sendReplay({
9344
9364
  replayId,
9345
9365
  recordingData,