@sentry/replay 7.58.0 → 7.59.0-beta.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
@@ -14,9 +14,6 @@ const UNABLE_TO_SEND_REPLAY = 'Unable to send Replay';
14
14
  // The idle limit for a session after which recording is paused.
15
15
  const SESSION_IDLE_PAUSE_DURATION = 300000; // 5 minutes in ms
16
16
 
17
- // The idle limit for a session after which the session expires.
18
- const SESSION_IDLE_EXPIRE_DURATION = 900000; // 15 minutes in ms
19
-
20
17
  // The maximum length of a session
21
18
  const MAX_SESSION_LIFE = 3600000; // 60 minutes in ms
22
19
 
@@ -3279,26 +3276,26 @@ function addBreadcrumbEvent(replay, breadcrumb) {
3279
3276
  }
3280
3277
 
3281
3278
  if (['ui.click', 'ui.input'].includes(breadcrumb.category )) {
3282
- replay.triggerUserActivity();
3283
- } else {
3284
- replay.checkAndHandleExpiredSession();
3279
+ replay.updateUserActivity();
3285
3280
  }
3286
3281
 
3287
- replay.addUpdate(() => {
3288
- void replay.throttledAddEvent({
3289
- type: EventType.Custom,
3290
- // TODO: We were converting from ms to seconds for breadcrumbs, spans,
3291
- // but maybe we should just keep them as milliseconds
3292
- timestamp: (breadcrumb.timestamp || 0) * 1000,
3293
- data: {
3294
- tag: 'breadcrumb',
3295
- // normalize to max. 10 depth and 1_000 properties per object
3296
- payload: normalize(breadcrumb, 10, 1000),
3297
- },
3298
- });
3282
+ replay.checkSessionState(() => {
3283
+ replay.addUpdate(() => {
3284
+ void replay.throttledAddEvent({
3285
+ type: EventType.Custom,
3286
+ // TODO: We were converting from ms to seconds for breadcrumbs, spans,
3287
+ // but maybe we should just keep them as milliseconds
3288
+ timestamp: (breadcrumb.timestamp || 0) * 1000,
3289
+ data: {
3290
+ tag: 'breadcrumb',
3291
+ // normalize to max. 10 depth and 1_000 properties per object
3292
+ payload: normalize(breadcrumb, 10, 1000),
3293
+ },
3294
+ });
3299
3295
 
3300
- // Do not flush after console log messages
3301
- return breadcrumb.category === 'console';
3296
+ // Do not flush after console log messages
3297
+ return breadcrumb.category === 'console';
3298
+ });
3302
3299
  });
3303
3300
  }
3304
3301
 
@@ -4389,6 +4386,66 @@ function createEventBuffer({ useCompression }) {
4389
4386
  return new EventBufferArray();
4390
4387
  }
4391
4388
 
4389
+ /**
4390
+ * Given an initial timestamp and an expiry duration, checks to see if current
4391
+ * time should be considered as expired.
4392
+ */
4393
+ function isExpired(
4394
+ initialTime,
4395
+ expiry,
4396
+ targetTime = +new Date(),
4397
+ ) {
4398
+ // Always expired if < 0
4399
+ if (initialTime === null || expiry === undefined || expiry < 0) {
4400
+ return true;
4401
+ }
4402
+
4403
+ // Never expires if == 0
4404
+ if (expiry === 0) {
4405
+ return false;
4406
+ }
4407
+
4408
+ return initialTime + expiry <= targetTime;
4409
+ }
4410
+
4411
+ /** Check the state of the session. */
4412
+ function checkSessionState(
4413
+ session,
4414
+ recordingMode,
4415
+ timeouts,
4416
+ callbacks
4417
+
4418
+ ,
4419
+ ) {
4420
+ const _isIdle = () => {
4421
+ return isExpired(session.lastActivity, timeouts.sessionIdlePause);
4422
+ };
4423
+
4424
+ const _exceedsMaxLength = () => {
4425
+ return isExpired(session.started, timeouts.maxSessionLife);
4426
+ };
4427
+
4428
+ // When buffering, we never want to expire/end/pause/restart the recording
4429
+ if (recordingMode === 'buffer') {
4430
+ callbacks.onContinue();
4431
+ return;
4432
+ }
4433
+
4434
+ if (_exceedsMaxLength()) {
4435
+ callbacks.onEnd();
4436
+ return;
4437
+ }
4438
+
4439
+ if (_isIdle()) {
4440
+ callbacks.onPause();
4441
+ return;
4442
+ }
4443
+
4444
+ callbacks.ensureResumed();
4445
+
4446
+ callbacks.onContinue();
4447
+ }
4448
+
4392
4449
  /** If sessionStorage is available. */
4393
4450
  function hasSessionStorage() {
4394
4451
  return 'sessionStorage' in WINDOW && !!WINDOW.sessionStorage;
@@ -4417,41 +4474,6 @@ function deleteSession() {
4417
4474
  }
4418
4475
  }
4419
4476
 
4420
- /**
4421
- * Given an initial timestamp and an expiry duration, checks to see if current
4422
- * time should be considered as expired.
4423
- */
4424
- function isExpired(
4425
- initialTime,
4426
- expiry,
4427
- targetTime = +new Date(),
4428
- ) {
4429
- // Always expired if < 0
4430
- if (initialTime === null || expiry === undefined || expiry < 0) {
4431
- return true;
4432
- }
4433
-
4434
- // Never expires if == 0
4435
- if (expiry === 0) {
4436
- return false;
4437
- }
4438
-
4439
- return initialTime + expiry <= targetTime;
4440
- }
4441
-
4442
- /**
4443
- * Checks to see if session is expired
4444
- */
4445
- function isSessionExpired(session, timeouts, targetTime = +new Date()) {
4446
- return (
4447
- // First, check that maximum session length has not been exceeded
4448
- isExpired(session.started, timeouts.maxSessionLife, targetTime) ||
4449
- // check that the idle timeout has not been exceeded (i.e. user has
4450
- // performed an action within the last `sessionIdleExpire` ms)
4451
- isExpired(session.lastActivity, timeouts.sessionIdleExpire, targetTime)
4452
- );
4453
- }
4454
-
4455
4477
  /**
4456
4478
  * Given a sample rate, returns true if replay should be sampled.
4457
4479
  *
@@ -4500,24 +4522,20 @@ function makeSession(session) {
4500
4522
  lastActivity,
4501
4523
  segmentId,
4502
4524
  sampled,
4503
- shouldRefresh: true,
4504
4525
  };
4505
4526
  }
4506
4527
 
4507
- /**
4508
- * Get the sampled status for a session based on sample rates & current sampled status.
4509
- */
4510
- function getSessionSampleType(sessionSampleRate, allowBuffering) {
4511
- return isSampled(sessionSampleRate) ? 'session' : allowBuffering ? 'buffer' : false;
4512
- }
4513
-
4514
4528
  /**
4515
4529
  * Create a new session, which in its current implementation is a Sentry event
4516
4530
  * that all replays will be saved to as attachments. Currently, we only expect
4517
4531
  * one of these Sentry events per "replay session".
4518
4532
  */
4519
- function createSession({ sessionSampleRate, allowBuffering, stickySession = false }) {
4520
- const sampled = getSessionSampleType(sessionSampleRate, allowBuffering);
4533
+ function createSession({
4534
+ sampled,
4535
+ stickySession = false,
4536
+ }
4537
+
4538
+ ) {
4521
4539
  const session = makeSession({
4522
4540
  sampled,
4523
4541
  });
@@ -4555,47 +4573,31 @@ function fetchSession() {
4555
4573
  }
4556
4574
  }
4557
4575
 
4558
- /**
4559
- * Get or create a session
4560
- */
4561
- function getSession({
4562
- timeouts,
4563
- currentSession,
4576
+ /** Either restore a session from sessionStorage, or create a new one. */
4577
+ function restoreOrCreateSession({
4564
4578
  stickySession,
4565
- sessionSampleRate,
4566
- allowBuffering,
4567
- }) {
4568
- // If session exists and is passed, use it instead of always hitting session storage
4569
- const session = currentSession || (stickySession && fetchSession());
4570
-
4571
- if (session) {
4572
- // If there is a session, check if it is valid (e.g. "last activity" time
4573
- // should be within the "session idle time", and "session started" time is
4574
- // within "max session time").
4575
- const isExpired = isSessionExpired(session, timeouts);
4576
-
4577
- if (!isExpired || (allowBuffering && session.shouldRefresh)) {
4578
- return { type: 'saved', session };
4579
- } else if (!session.shouldRefresh) {
4580
- // This is the case if we have an error session that is completed
4581
- // (=triggered an error). Session will continue as session-based replay,
4582
- // and when this session is expired, it will not be renewed until user
4583
- // reloads.
4584
- const discardedSession = makeSession({ sampled: false });
4585
- return { type: 'new', session: discardedSession };
4586
- } else {
4587
- (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Session has expired');
4588
- }
4589
- // Otherwise continue to create a new session
4579
+ sampled,
4580
+ forceSampled,
4581
+ }
4582
+
4583
+ ) {
4584
+ const currentSession = stickySession && fetchSession();
4585
+
4586
+ if (currentSession && currentSession.sampled !== sampled && forceSampled) {
4587
+ // If for whatever reason the session from sessionStorage has a different sampling, we force to the new sampling
4588
+ (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) &&
4589
+ logger.log(`[Replay] Tried to restore session with different sampling: ${currentSession.sampled} !== ${sampled}`);
4590
+ } else if (currentSession) {
4591
+ (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log(`[Replay] Loaded session from sessionStorage: id=${currentSession.id}`);
4592
+ return currentSession;
4590
4593
  }
4591
4594
 
4592
4595
  const newSession = createSession({
4593
4596
  stickySession,
4594
- sessionSampleRate,
4595
- allowBuffering,
4597
+ sampled,
4596
4598
  });
4597
4599
 
4598
- return { type: 'new', session: newSession };
4600
+ return newSession;
4599
4601
  }
4600
4602
 
4601
4603
  function isCustomEvent(event) {
@@ -4908,12 +4910,14 @@ function handleHistorySpanListener(replay) {
4908
4910
 
4909
4911
  // Need to collect visited URLs
4910
4912
  replay.getContext().urls.push(result.name);
4911
- replay.triggerUserActivity();
4913
+ replay.updateUserActivity();
4912
4914
 
4913
- replay.addUpdate(() => {
4914
- createPerformanceSpans(replay, [result]);
4915
- // Returning false to flush
4916
- return false;
4915
+ replay.checkSessionState(() => {
4916
+ replay.addUpdate(() => {
4917
+ createPerformanceSpans(replay, [result]);
4918
+ // Returning false to flush
4919
+ return false;
4920
+ });
4917
4921
  });
4918
4922
  };
4919
4923
  }
@@ -6529,79 +6533,64 @@ function getHandleRecordingEmit(replay) {
6529
6533
  let hadFirstEvent = false;
6530
6534
 
6531
6535
  return (event, _isCheckout) => {
6532
- // If this is false, it means session is expired, create and a new session and wait for checkout
6533
- if (!replay.checkAndHandleExpiredSession()) {
6534
- (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.warn('[Replay] Received replay event after session expired.');
6535
-
6536
- return;
6537
- }
6538
-
6539
- // `_isCheckout` is only set when the checkout is due to `checkoutEveryNms`
6540
- // We also want to treat the first event as a checkout, so we handle this specifically here
6541
- const isCheckout = _isCheckout || !hadFirstEvent;
6542
- hadFirstEvent = true;
6543
-
6544
- // The handler returns `true` if we do not want to trigger debounced flush, `false` if we want to debounce flush.
6545
- replay.addUpdate(() => {
6546
- // The session is always started immediately on pageload/init, but for
6547
- // error-only replays, it should reflect the most recent checkout
6548
- // when an error occurs. Clear any state that happens before this current
6549
- // checkout. This needs to happen before `addEvent()` which updates state
6550
- // dependent on this reset.
6551
- if (replay.recordingMode === 'buffer' && isCheckout) {
6552
- replay.setInitialState();
6553
- }
6554
-
6555
- // We need to clear existing events on a checkout, otherwise they are
6556
- // incremental event updates and should be appended
6557
- void addEvent(replay, event, isCheckout);
6558
-
6559
- // Different behavior for full snapshots (type=2), ignore other event types
6560
- // See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16
6561
- if (!isCheckout) {
6562
- return false;
6563
- }
6536
+ replay.checkSessionState(() => {
6537
+ // `_isCheckout` is only set when the checkout is due to `checkoutEveryNms`
6538
+ // We also want to treat the first event as a checkout, so we handle this specifically here
6539
+ const isCheckout = _isCheckout || !hadFirstEvent;
6540
+ hadFirstEvent = true;
6541
+
6542
+ // The handler returns `true` if we do not want to trigger debounced flush, `false` if we want to debounce flush.
6543
+ replay.addUpdate(() => {
6544
+ // The session is always started immediately on pageload/init, but for
6545
+ // error-only replays, it should reflect the most recent checkout
6546
+ // when an error occurs. Clear any state that happens before this current
6547
+ // checkout. This needs to happen before `addEvent()` which updates state
6548
+ // dependent on this reset.
6549
+ if (replay.recordingMode === 'buffer' && isCheckout) {
6550
+ replay.setInitialState();
6551
+ }
6564
6552
 
6565
- // Additionally, create a meta event that will capture certain SDK settings.
6566
- // In order to handle buffer mode, this needs to either be done when we
6567
- // receive checkout events or at flush time.
6568
- //
6569
- // `isCheckout` is always true, but want to be explicit that it should
6570
- // only be added for checkouts
6571
- void addSettingsEvent(replay, isCheckout);
6572
-
6573
- // If there is a previousSessionId after a full snapshot occurs, then
6574
- // the replay session was started due to session expiration. The new session
6575
- // is started before triggering a new checkout and contains the id
6576
- // of the previous session. Do not immediately flush in this case
6577
- // to avoid capturing only the checkout and instead the replay will
6578
- // be captured if they perform any follow-up actions.
6579
- if (replay.session && replay.session.previousSessionId) {
6580
- return true;
6581
- }
6553
+ // We need to clear existing events on a checkout, otherwise they are
6554
+ // incremental event updates and should be appended
6555
+ void addEvent(replay, event, isCheckout);
6582
6556
 
6583
- // When in buffer mode, make sure we adjust the session started date to the current earliest event of the buffer
6584
- // this should usually be the timestamp of the checkout event, but to be safe...
6585
- if (replay.recordingMode === 'buffer' && replay.session && replay.eventBuffer) {
6586
- const earliestEvent = replay.eventBuffer.getEarliestTimestamp();
6587
- if (earliestEvent) {
6588
- replay.session.started = earliestEvent;
6557
+ // Different behavior for full snapshots (type=2), ignore other event types
6558
+ // See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16
6559
+ if (!isCheckout) {
6560
+ return false;
6561
+ }
6589
6562
 
6590
- if (replay.getOptions().stickySession) {
6591
- saveSession(replay.session);
6563
+ // Additionally, create a meta event that will capture certain SDK settings.
6564
+ // In order to handle buffer mode, this needs to either be done when we
6565
+ // receive checkout events or at flush time.
6566
+ //
6567
+ // `isCheckout` is always true, but want to be explicit that it should
6568
+ // only be added for checkouts
6569
+ void addSettingsEvent(replay, isCheckout);
6570
+
6571
+ // When in buffer mode, make sure we adjust the session started date to the current earliest event of the buffer
6572
+ // this should usually be the timestamp of the checkout event, but to be safe...
6573
+ if (replay.recordingMode === 'buffer' && replay.session && replay.eventBuffer) {
6574
+ const earliestEvent = replay.eventBuffer.getEarliestTimestamp();
6575
+ if (earliestEvent) {
6576
+ replay.session.started = earliestEvent;
6577
+
6578
+ if (replay.getOptions().stickySession) {
6579
+ saveSession(replay.session);
6580
+ }
6592
6581
  }
6593
6582
  }
6594
- }
6595
6583
 
6596
- if (replay.recordingMode === 'session') {
6597
- // If the full snapshot is due to an initial load, we will not have
6598
- // a previous session ID. In this case, we want to buffer events
6599
- // for a set amount of time before flushing. This can help avoid
6600
- // capturing replays of users that immediately close the window.
6601
- void replay.flush();
6602
- }
6584
+ if (replay.recordingMode === 'session') {
6585
+ // If the full snapshot is due to an initial load, we will not have
6586
+ // a previous session ID. In this case, we want to buffer events
6587
+ // for a set amount of time before flushing. This can help avoid
6588
+ // capturing replays of users that immediately close the window.
6589
+ void replay.flush();
6590
+ }
6603
6591
 
6604
- return true;
6592
+ return true;
6593
+ });
6605
6594
  });
6606
6595
  };
6607
6596
  }
@@ -6646,6 +6635,26 @@ function addSettingsEvent(replay, isCheckout) {
6646
6635
  return addEvent(replay, createOptionsEvent(replay), false);
6647
6636
  }
6648
6637
 
6638
+ /**
6639
+ * Sample a session based on the provided sample rates.
6640
+ */
6641
+ function sampleSession({
6642
+ errorSampleRate,
6643
+ sessionSampleRate,
6644
+ }
6645
+
6646
+ ) {
6647
+ if (errorSampleRate <= 0 && sessionSampleRate <= 0) {
6648
+ return false;
6649
+ }
6650
+
6651
+ if (isSampled(sessionSampleRate)) {
6652
+ return 'session';
6653
+ }
6654
+
6655
+ return errorSampleRate > 0 ? 'buffer' : false;
6656
+ }
6657
+
6649
6658
  /**
6650
6659
  * Create a replay envelope ready to be sent.
6651
6660
  * This includes both the replay event, as well as the recording data.
@@ -7037,7 +7046,6 @@ class ReplayContainer {
7037
7046
  */
7038
7047
  __init4() {this.timeouts = {
7039
7048
  sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
7040
- sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
7041
7049
  maxSessionLife: MAX_SESSION_LIFE,
7042
7050
  }; }
7043
7051
 
@@ -7049,35 +7057,30 @@ class ReplayContainer {
7049
7057
 
7050
7058
  __init6() {this._flushLock = null;}
7051
7059
 
7052
- /**
7053
- * Timestamp of the last user activity. This lives across sessions.
7054
- */
7055
- __init7() {this._lastActivity = Date.now();}
7056
-
7057
7060
  /**
7058
7061
  * Is the integration currently active?
7059
7062
  */
7060
- __init8() {this._isEnabled = false;}
7063
+ __init7() {this._isEnabled = false;}
7061
7064
 
7062
7065
  /**
7063
7066
  * Paused is a state where:
7064
7067
  * - DOM Recording is not listening at all
7065
7068
  * - Nothing will be added to event buffer (e.g. core SDK events)
7066
7069
  */
7067
- __init9() {this._isPaused = false;}
7070
+ __init8() {this._isPaused = false;}
7068
7071
 
7069
7072
  /**
7070
7073
  * Have we attached listeners to the core SDK?
7071
7074
  * Note we have to track this as there is no way to remove instrumentation handlers.
7072
7075
  */
7073
- __init10() {this._hasInitializedCoreListeners = false;}
7076
+ __init9() {this._hasInitializedCoreListeners = false;}
7074
7077
 
7075
7078
  /**
7076
7079
  * Function to stop recording
7077
7080
  */
7078
- __init11() {this._stopRecording = null;}
7081
+ __init10() {this._stopRecording = null;}
7079
7082
 
7080
- __init12() {this._context = {
7083
+ __init11() {this._context = {
7081
7084
  errorIds: new Set(),
7082
7085
  traceIds: new Set(),
7083
7086
  urls: [],
@@ -7090,7 +7093,7 @@ class ReplayContainer {
7090
7093
  recordingOptions,
7091
7094
  }
7092
7095
 
7093
- ) {ReplayContainer.prototype.__init.call(this);ReplayContainer.prototype.__init2.call(this);ReplayContainer.prototype.__init3.call(this);ReplayContainer.prototype.__init4.call(this);ReplayContainer.prototype.__init5.call(this);ReplayContainer.prototype.__init6.call(this);ReplayContainer.prototype.__init7.call(this);ReplayContainer.prototype.__init8.call(this);ReplayContainer.prototype.__init9.call(this);ReplayContainer.prototype.__init10.call(this);ReplayContainer.prototype.__init11.call(this);ReplayContainer.prototype.__init12.call(this);ReplayContainer.prototype.__init13.call(this);ReplayContainer.prototype.__init14.call(this);ReplayContainer.prototype.__init15.call(this);ReplayContainer.prototype.__init16.call(this);ReplayContainer.prototype.__init17.call(this);ReplayContainer.prototype.__init18.call(this);
7096
+ ) {ReplayContainer.prototype.__init.call(this);ReplayContainer.prototype.__init2.call(this);ReplayContainer.prototype.__init3.call(this);ReplayContainer.prototype.__init4.call(this);ReplayContainer.prototype.__init5.call(this);ReplayContainer.prototype.__init6.call(this);ReplayContainer.prototype.__init7.call(this);ReplayContainer.prototype.__init8.call(this);ReplayContainer.prototype.__init9.call(this);ReplayContainer.prototype.__init10.call(this);ReplayContainer.prototype.__init11.call(this);ReplayContainer.prototype.__init12.call(this);ReplayContainer.prototype.__init13.call(this);ReplayContainer.prototype.__init14.call(this);ReplayContainer.prototype.__init15.call(this);ReplayContainer.prototype.__init16.call(this);ReplayContainer.prototype.__init17.call(this);
7094
7097
  this._recordingOptions = recordingOptions;
7095
7098
  this._options = options;
7096
7099
 
@@ -7148,37 +7151,13 @@ class ReplayContainer {
7148
7151
  */
7149
7152
  initializeSampling() {
7150
7153
  const { errorSampleRate, sessionSampleRate } = this._options;
7154
+ const sampled = sampleSession({ errorSampleRate, sessionSampleRate });
7151
7155
 
7152
- // If neither sample rate is > 0, then do nothing - user will need to call one of
7153
- // `start()` or `startBuffering` themselves.
7154
- if (errorSampleRate <= 0 && sessionSampleRate <= 0) {
7155
- return;
7156
- }
7157
-
7158
- // Otherwise if there is _any_ sample rate set, try to load an existing
7159
- // session, or create a new one.
7160
- const isSessionSampled = this._loadAndCheckSession();
7161
-
7162
- if (!isSessionSampled) {
7163
- // This should only occur if `errorSampleRate` is 0 and was unsampled for
7164
- // session-based replay. In this case there is nothing to do.
7156
+ if (!sampled) {
7165
7157
  return;
7166
7158
  }
7167
7159
 
7168
- if (!this.session) {
7169
- // This should not happen, something wrong has occurred
7170
- this._handleException(new Error('Unable to initialize and create session'));
7171
- return;
7172
- }
7173
-
7174
- if (this.session.sampled && this.session.sampled !== 'session') {
7175
- // If not sampled as session-based, then recording mode will be `buffer`
7176
- // Note that we don't explicitly check if `sampled === 'buffer'` because we
7177
- // could have sessions from Session storage that are still `error` from
7178
- // prior SDK version.
7179
- this.recordingMode = 'buffer';
7180
- }
7181
-
7160
+ this._initializeSession(sampled);
7182
7161
  this._initializeRecording();
7183
7162
  }
7184
7163
 
@@ -7198,20 +7177,7 @@ class ReplayContainer {
7198
7177
  throw new Error('Replay buffering is in progress, call `flush()` to save the replay');
7199
7178
  }
7200
7179
 
7201
- const previousSessionId = this.session && this.session.id;
7202
-
7203
- const { session } = getSession({
7204
- timeouts: this.timeouts,
7205
- stickySession: Boolean(this._options.stickySession),
7206
- currentSession: this.session,
7207
- // This is intentional: create a new session-based replay when calling `start()`
7208
- sessionSampleRate: 1,
7209
- allowBuffering: false,
7210
- });
7211
-
7212
- session.previousSessionId = previousSessionId;
7213
- this.session = session;
7214
-
7180
+ this._initializeSession('session', { forceSampled: true });
7215
7181
  this._initializeRecording();
7216
7182
  }
7217
7183
 
@@ -7224,20 +7190,7 @@ class ReplayContainer {
7224
7190
  throw new Error('Replay recording is already in progress');
7225
7191
  }
7226
7192
 
7227
- const previousSessionId = this.session && this.session.id;
7228
-
7229
- const { session } = getSession({
7230
- timeouts: this.timeouts,
7231
- stickySession: Boolean(this._options.stickySession),
7232
- currentSession: this.session,
7233
- sessionSampleRate: 0,
7234
- allowBuffering: true,
7235
- });
7236
-
7237
- session.previousSessionId = previousSessionId;
7238
- this.session = session;
7239
-
7240
- this.recordingMode = 'buffer';
7193
+ this._initializeSession('buffer', { forceSampled: true });
7241
7194
  this._initializeRecording();
7242
7195
  }
7243
7196
 
@@ -7319,8 +7272,7 @@ class ReplayContainer {
7319
7272
  this.eventBuffer && this.eventBuffer.destroy();
7320
7273
  this.eventBuffer = null;
7321
7274
 
7322
- // Clear session from session storage, note this means if a new session
7323
- // is started after, it will not have `previousSessionId`
7275
+ // Clear session from session storage
7324
7276
  clearSession(this);
7325
7277
  } catch (err) {
7326
7278
  this._handleException(err);
@@ -7333,6 +7285,10 @@ class ReplayContainer {
7333
7285
  * not as thorough of a shutdown as `stop()`.
7334
7286
  */
7335
7287
  pause() {
7288
+ if (this.isPaused()) {
7289
+ return;
7290
+ }
7291
+
7336
7292
  this._isPaused = true;
7337
7293
  this.stopRecording();
7338
7294
  }
@@ -7344,12 +7300,14 @@ class ReplayContainer {
7344
7300
  * new DOM checkout.`
7345
7301
  */
7346
7302
  resume() {
7347
- if (!this._loadAndCheckSession()) {
7303
+ if (!this.isPaused()) {
7348
7304
  return;
7349
7305
  }
7350
7306
 
7351
7307
  this._isPaused = false;
7352
- this.startRecording();
7308
+ this.checkSessionState(() => {
7309
+ this.startRecording();
7310
+ });
7353
7311
  }
7354
7312
 
7355
7313
  /**
@@ -7384,20 +7342,8 @@ class ReplayContainer {
7384
7342
  // starting a new recording
7385
7343
  this.recordingMode = 'session';
7386
7344
 
7387
- // Once this session ends, we do not want to refresh it
7388
7345
  if (this.session) {
7389
- this.session.shouldRefresh = false;
7390
-
7391
- // It's possible that the session lifespan is > max session lifespan
7392
- // because we have been buffering beyond max session lifespan (we ignore
7393
- // expiration given that `shouldRefresh` is true). Since we flip
7394
- // `shouldRefresh`, the session could be considered expired due to
7395
- // lifespan, which is not what we want. Update session start date to be
7396
- // the current timestamp, so that session is not considered to be
7397
- // expired. This means that max replay duration can be MAX_SESSION_LIFE +
7398
- // (length of buffer), which we are ok with.
7399
- this._updateUserActivity(activityTime);
7400
- this._updateSessionActivity(activityTime);
7346
+ this.session.lastActivity = activityTime;
7401
7347
  this.session.started = activityTime;
7402
7348
  this._maybeSaveSession();
7403
7349
  }
@@ -7435,42 +7381,9 @@ class ReplayContainer {
7435
7381
  }
7436
7382
 
7437
7383
  /**
7438
- * Updates the user activity timestamp and resumes recording. This should be
7439
- * called in an event handler for a user action that we consider as the user
7440
- * being "active" (e.g. a mouse click).
7441
- */
7442
- triggerUserActivity() {
7443
- this._updateUserActivity();
7444
-
7445
- // This case means that recording was once stopped due to inactivity.
7446
- // Ensure that recording is resumed.
7447
- if (!this._stopRecording) {
7448
- // Create a new session, otherwise when the user action is flushed, it
7449
- // will get rejected due to an expired session.
7450
- if (!this._loadAndCheckSession()) {
7451
- return;
7452
- }
7453
-
7454
- // Note: This will cause a new DOM checkout
7455
- this.resume();
7456
- return;
7457
- }
7458
-
7459
- // Otherwise... recording was never suspended, continue as normalish
7460
- this.checkAndHandleExpiredSession();
7461
-
7462
- this._updateSessionActivity();
7463
- }
7464
-
7465
- /**
7466
- * Updates the user activity timestamp *without* resuming
7467
- * recording. Some user events (e.g. keydown) can be create
7468
- * low-value replays that only contain the keypress as a
7469
- * breadcrumb. Instead this would require other events to
7470
- * create a new replay after a session has expired.
7384
+ * Updates the user activity timestamp.
7471
7385
  */
7472
7386
  updateUserActivity() {
7473
- this._updateUserActivity();
7474
7387
  this._updateSessionActivity();
7475
7388
  }
7476
7389
 
@@ -7515,54 +7428,6 @@ class ReplayContainer {
7515
7428
  return this.session && this.session.id;
7516
7429
  }
7517
7430
 
7518
- /**
7519
- * Checks if recording should be stopped due to user inactivity. Otherwise
7520
- * check if session is expired and create a new session if so. Triggers a new
7521
- * full snapshot on new session.
7522
- *
7523
- * Returns true if session is not expired, false otherwise.
7524
- * @hidden
7525
- */
7526
- checkAndHandleExpiredSession() {
7527
- const oldSessionId = this.getSessionId();
7528
-
7529
- // Prevent starting a new session if the last user activity is older than
7530
- // SESSION_IDLE_PAUSE_DURATION. Otherwise non-user activity can trigger a new
7531
- // session+recording. This creates noisy replays that do not have much
7532
- // content in them.
7533
- if (
7534
- this._lastActivity &&
7535
- isExpired(this._lastActivity, this.timeouts.sessionIdlePause) &&
7536
- this.session &&
7537
- this.session.sampled === 'session'
7538
- ) {
7539
- // Pause recording only for session-based replays. Otherwise, resuming
7540
- // will create a new replay and will conflict with users who only choose
7541
- // to record error-based replays only. (e.g. the resumed replay will not
7542
- // contain a reference to an error)
7543
- this.pause();
7544
- return;
7545
- }
7546
-
7547
- // --- There is recent user activity --- //
7548
- // This will create a new session if expired, based on expiry length
7549
- if (!this._loadAndCheckSession()) {
7550
- return;
7551
- }
7552
-
7553
- // Session was expired if session ids do not match
7554
- const expired = oldSessionId !== this.getSessionId();
7555
-
7556
- if (!expired) {
7557
- return true;
7558
- }
7559
-
7560
- // Session is expired, trigger a full snapshot (which will create a new session)
7561
- this._triggerFullSnapshot();
7562
-
7563
- return false;
7564
- }
7565
-
7566
7431
  /**
7567
7432
  * Capture some initial state that can change throughout the lifespan of the
7568
7433
  * replay. This is required because otherwise they would be captured at the
@@ -7615,6 +7480,23 @@ class ReplayContainer {
7615
7480
  return res;
7616
7481
  }
7617
7482
 
7483
+ /**
7484
+ * Check the state/expiration of the session.
7485
+ * The callback is called when the session is neither paused nor expired.
7486
+ */
7487
+ checkSessionState(onContinue) {
7488
+ if (!this.session || !this.session.sampled) {
7489
+ return;
7490
+ }
7491
+
7492
+ checkSessionState(this.session, this.recordingMode, this.timeouts, {
7493
+ onPause: () => this.pause(),
7494
+ ensureResumed: () => this.resume(),
7495
+ onEnd: () => this._refreshSession(),
7496
+ onContinue,
7497
+ });
7498
+ }
7499
+
7618
7500
  /**
7619
7501
  * This will get the parametrized route name of the current page.
7620
7502
  * This is only available if performance is enabled, and if an instrumented router is used.
@@ -7628,6 +7510,31 @@ class ReplayContainer {
7628
7510
  return lastTransaction.name;
7629
7511
  }
7630
7512
 
7513
+ /**
7514
+ * Initialize a new session.
7515
+ */
7516
+ _initializeSession(sampled, { forceSampled = false } = {}) {
7517
+ const { stickySession } = this._options;
7518
+
7519
+ // If neither sample rate is > 0, then do nothing - user will need to call one of
7520
+ // `start()` or `startBuffering` themselves.
7521
+ if (!sampled) {
7522
+ return;
7523
+ }
7524
+
7525
+ const session = restoreOrCreateSession({ stickySession, sampled, forceSampled });
7526
+
7527
+ // This shouldn't be possible really, but just in case...
7528
+ if (!session.sampled) {
7529
+ return;
7530
+ }
7531
+
7532
+ this.session = session;
7533
+ this.recordingMode = this.session.sampled === 'buffer' ? 'buffer' : 'session';
7534
+
7535
+ return;
7536
+ }
7537
+
7631
7538
  /**
7632
7539
  * Initialize and start all listeners to varying events (DOM,
7633
7540
  * Performance Observer, Recording, Sentry SDK, etc)
@@ -7637,7 +7544,7 @@ class ReplayContainer {
7637
7544
 
7638
7545
  // this method is generally called on page load or manually - in both cases
7639
7546
  // we should treat it as an activity
7640
- this._updateSessionActivity();
7547
+ this.updateUserActivity();
7641
7548
 
7642
7549
  this.eventBuffer = createEventBuffer({
7643
7550
  useCompression: this._options.useCompression,
@@ -7648,6 +7555,7 @@ class ReplayContainer {
7648
7555
 
7649
7556
  // Need to set as enabled before we start recording, as `record()` can trigger a flush with a new checkout
7650
7557
  this._isEnabled = true;
7558
+ this._isPaused = false;
7651
7559
 
7652
7560
  this.startRecording();
7653
7561
  }
@@ -7662,37 +7570,31 @@ class ReplayContainer {
7662
7570
  }
7663
7571
 
7664
7572
  /**
7665
- * Loads (or refreshes) the current session.
7666
- * Returns false if session is not recorded.
7573
+ * Refresh a session that has ended, either when it exceeded the max. age or when it was inactive for too long.
7574
+ * This means there was a sampled & sent session before - this will never be called while the session is buffering.
7667
7575
  */
7668
- _loadAndCheckSession() {
7669
- const { type, session } = getSession({
7670
- timeouts: this.timeouts,
7671
- stickySession: Boolean(this._options.stickySession),
7672
- currentSession: this.session,
7673
- sessionSampleRate: this._options.sessionSampleRate,
7674
- allowBuffering: this._options.errorSampleRate > 0 || this.recordingMode === 'buffer',
7675
- });
7676
-
7677
- // If session was newly created (i.e. was not loaded from storage), then
7678
- // enable flag to create the root replay
7679
- if (type === 'new') {
7680
- this.setInitialState();
7681
- }
7682
-
7683
- const currentSessionId = this.getSessionId();
7684
- if (session.id !== currentSessionId) {
7685
- session.previousSessionId = currentSessionId;
7576
+ _refreshSession() {
7577
+ // To avoid firing this multiple times, we check if we are even recording
7578
+ if (!this.isEnabled()) {
7579
+ return;
7686
7580
  }
7687
7581
 
7688
- this.session = session;
7582
+ const { errorSampleRate, sessionSampleRate } = this._options;
7583
+ const sampled = sampleSession({ errorSampleRate, sessionSampleRate });
7689
7584
 
7690
- if (!this.session.sampled) {
7691
- void this.stop('session unsampled');
7692
- return false;
7585
+ if (!sampled || !this.session) {
7586
+ void this.stop('session expired without new session');
7587
+ return;
7693
7588
  }
7694
7589
 
7695
- return true;
7590
+ void this.stop('session expired with refreshing').then(() => {
7591
+ if (sampled === 'session') {
7592
+ return this.start();
7593
+ }
7594
+ if (sampled === 'buffer') {
7595
+ return this.startBuffering();
7596
+ }
7597
+ });
7696
7598
  }
7697
7599
 
7698
7600
  /**
@@ -7757,7 +7659,7 @@ class ReplayContainer {
7757
7659
  * be hidden. Likewise, moving a different window to cover the contents of the
7758
7660
  * page will also trigger a change to a hidden state.
7759
7661
  */
7760
- __init13() {this._handleVisibilityChange = () => {
7662
+ __init12() {this._handleVisibilityChange = () => {
7761
7663
  if (WINDOW.document.visibilityState === 'visible') {
7762
7664
  this._doChangeToForegroundTasks();
7763
7665
  } else {
@@ -7768,7 +7670,7 @@ class ReplayContainer {
7768
7670
  /**
7769
7671
  * Handle when page is blurred
7770
7672
  */
7771
- __init14() {this._handleWindowBlur = () => {
7673
+ __init13() {this._handleWindowBlur = () => {
7772
7674
  const breadcrumb = createBreadcrumb({
7773
7675
  category: 'ui.blur',
7774
7676
  });
@@ -7781,7 +7683,7 @@ class ReplayContainer {
7781
7683
  /**
7782
7684
  * Handle when page is focused
7783
7685
  */
7784
- __init15() {this._handleWindowFocus = () => {
7686
+ __init14() {this._handleWindowFocus = () => {
7785
7687
  const breadcrumb = createBreadcrumb({
7786
7688
  category: 'ui.focus',
7787
7689
  });
@@ -7792,7 +7694,7 @@ class ReplayContainer {
7792
7694
  };}
7793
7695
 
7794
7696
  /** Ensure page remains active when a key is pressed. */
7795
- __init16() {this._handleKeyboardEvent = (event) => {
7697
+ __init15() {this._handleKeyboardEvent = (event) => {
7796
7698
  handleKeyboardEvent(this, event);
7797
7699
  };}
7798
7700
 
@@ -7800,71 +7702,35 @@ class ReplayContainer {
7800
7702
  * Tasks to run when we consider a page to be hidden (via blurring and/or visibility)
7801
7703
  */
7802
7704
  _doChangeToBackgroundTasks(breadcrumb) {
7803
- if (!this.session) {
7804
- return;
7805
- }
7806
-
7807
- const expired = isSessionExpired(this.session, this.timeouts);
7808
-
7809
- if (breadcrumb && !expired) {
7810
- this._createCustomBreadcrumb(breadcrumb);
7811
- }
7705
+ this.checkSessionState(() => {
7706
+ if (breadcrumb) {
7707
+ this._createCustomBreadcrumb(breadcrumb);
7708
+ }
7812
7709
 
7813
- // Send replay when the page/tab becomes hidden. There is no reason to send
7814
- // replay if it becomes visible, since no actions we care about were done
7815
- // while it was hidden
7816
- void this.conditionalFlush();
7710
+ // Send replay when the page/tab becomes hidden. There is no reason to send
7711
+ // replay if it becomes visible, since no actions we care about were done
7712
+ // while it was hidden
7713
+ void this.conditionalFlush();
7714
+ });
7817
7715
  }
7818
7716
 
7819
7717
  /**
7820
7718
  * Tasks to run when we consider a page to be visible (via focus and/or visibility)
7821
7719
  */
7822
7720
  _doChangeToForegroundTasks(breadcrumb) {
7823
- if (!this.session) {
7824
- return;
7825
- }
7826
-
7827
- const isSessionActive = this.checkAndHandleExpiredSession();
7828
-
7829
- if (!isSessionActive) {
7830
- // If the user has come back to the page within SESSION_IDLE_PAUSE_DURATION
7831
- // ms, we will re-use the existing session, otherwise create a new
7832
- // session
7833
- (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Document has become active, but session has expired');
7834
- return;
7835
- }
7836
-
7837
- if (breadcrumb) {
7838
- this._createCustomBreadcrumb(breadcrumb);
7839
- }
7840
- }
7841
-
7842
- /**
7843
- * Trigger rrweb to take a full snapshot which will cause this plugin to
7844
- * create a new Replay event.
7845
- */
7846
- _triggerFullSnapshot(checkout = true) {
7847
- try {
7848
- (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Replay] Taking full rrweb snapshot');
7849
- record.takeFullSnapshot(checkout);
7850
- } catch (err) {
7851
- this._handleException(err);
7852
- }
7853
- }
7854
-
7855
- /**
7856
- * Update user activity (across session lifespans)
7857
- */
7858
- _updateUserActivity(_lastActivity = Date.now()) {
7859
- this._lastActivity = _lastActivity;
7721
+ this.checkSessionState(() => {
7722
+ if (breadcrumb) {
7723
+ this._createCustomBreadcrumb(breadcrumb);
7724
+ }
7725
+ });
7860
7726
  }
7861
7727
 
7862
7728
  /**
7863
7729
  * Updates the session's last activity timestamp
7864
7730
  */
7865
- _updateSessionActivity(_lastActivity = Date.now()) {
7731
+ _updateSessionActivity(lastActivity = Date.now()) {
7866
7732
  if (this.session) {
7867
- this.session.lastActivity = _lastActivity;
7733
+ this.session.lastActivity = lastActivity;
7868
7734
  this._maybeSaveSession();
7869
7735
  }
7870
7736
  }
@@ -8015,7 +7881,7 @@ class ReplayContainer {
8015
7881
  * Flush recording data to Sentry. Creates a lock so that only a single flush
8016
7882
  * can be active at a time. Do not call this directly.
8017
7883
  */
8018
- __init17() {this._flush = async ({
7884
+ __init16() {this._flush = async ({
8019
7885
  force = false,
8020
7886
  }
8021
7887
 
@@ -8025,42 +7891,58 @@ class ReplayContainer {
8025
7891
  return;
8026
7892
  }
8027
7893
 
8028
- if (!this.checkAndHandleExpiredSession()) {
8029
- (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay] Attempting to finish replay event after session expired.');
8030
- return;
8031
- }
7894
+ return new Promise(resolve => {
7895
+ if (!this.session) {
7896
+ resolve();
7897
+ return;
7898
+ }
7899
+ checkSessionState(this.session, this.recordingMode, this.timeouts, {
7900
+ onPause: () => {
7901
+ this.pause();
7902
+ resolve();
7903
+ },
7904
+ ensureResumed: () => this.resume(),
7905
+ onEnd: () => {
7906
+ (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay] Attempting to finish replay event after session expired.');
7907
+ this._refreshSession();
7908
+ resolve();
7909
+ },
7910
+ onContinue: () => {
7911
+ void this._flushLocked().then(resolve);
7912
+ },
7913
+ });
7914
+ });
7915
+ };}
8032
7916
 
8033
- if (!this.session) {
8034
- (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error('[Replay] No session found to flush.');
8035
- return;
8036
- }
7917
+ /**
7918
+ * Flush, while locking so any future flush attemps while this is ongoing are queued.
7919
+ */
7920
+ async _flushLocked() {
7921
+ try {
7922
+ // A flush is about to happen, cancel any queued flushes
7923
+ this._debouncedFlush.cancel();
8037
7924
 
8038
- // A flush is about to happen, cancel any queued flushes
8039
- this._debouncedFlush.cancel();
7925
+ // this._flushLock acts as a lock so that future calls to `_flush()`
7926
+ // will be blocked until this promise resolves
7927
+ if (!this._flushLock) {
7928
+ this._flushLock = this._runFlush();
7929
+ await this._flushLock;
7930
+ this._flushLock = null;
7931
+ return;
7932
+ }
8040
7933
 
8041
- // this._flushLock acts as a lock so that future calls to `_flush()`
8042
- // will be blocked until this promise resolves
8043
- if (!this._flushLock) {
8044
- this._flushLock = this._runFlush();
7934
+ // Wait for previous flush to finish, then call the debounced `_flush()`.
7935
+ // It's possible there are other flush requests queued and waiting for it
7936
+ // to resolve. We want to reduce all outstanding requests (as well as any
7937
+ // new flush requests that occur within a second of the locked flush
7938
+ // completing) into a single flush.
8045
7939
  await this._flushLock;
8046
- this._flushLock = null;
8047
- return;
7940
+ } catch (error) {
7941
+ (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error(error);
8048
7942
  }
8049
7943
 
8050
- // Wait for previous flush to finish, then call the debounced `_flush()`.
8051
- // It's possible there are other flush requests queued and waiting for it
8052
- // to resolve. We want to reduce all outstanding requests (as well as any
8053
- // new flush requests that occur within a second of the locked flush
8054
- // completing) into a single flush.
8055
-
8056
- try {
8057
- await this._flushLock;
8058
- } catch (err) {
8059
- (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error(err);
8060
- } finally {
8061
- this._debouncedFlush();
8062
- }
8063
- };}
7944
+ this._debouncedFlush();
7945
+ }
8064
7946
 
8065
7947
  /** Save the session, if it is sticky */
8066
7948
  _maybeSaveSession() {
@@ -8070,7 +7952,7 @@ class ReplayContainer {
8070
7952
  }
8071
7953
 
8072
7954
  /** Handler for rrweb.record.onMutation */
8073
- __init18() {this._onMutationHandler = (mutations) => {
7955
+ __init17() {this._onMutationHandler = (mutations) => {
8074
7956
  const count = mutations.length;
8075
7957
 
8076
7958
  const mutationLimit = this._options.mutationLimit;