@sentry/replay 7.58.1 → 7.59.0-beta.1

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