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