@sentry/replay 7.61.1 → 7.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cjs/index.js CHANGED
@@ -3498,6 +3498,14 @@ class ClickDetector {
3498
3498
  clickCount: 0,
3499
3499
  node,
3500
3500
  };
3501
+
3502
+ // If there was a click in the last 1s on the same element, ignore it - only keep a single reference per second
3503
+ if (
3504
+ this._clicks.some(click => click.node === newClick.node && Math.abs(click.timestamp - newClick.timestamp) < 1)
3505
+ ) {
3506
+ return;
3507
+ }
3508
+
3501
3509
  this._clicks.push(newClick);
3502
3510
 
3503
3511
  // If this is the first new click, set a timeout to check for multi clicks
@@ -4027,21 +4035,45 @@ function logInfo(message, shouldAddBreadcrumb) {
4027
4035
  utils.logger.info(message);
4028
4036
 
4029
4037
  if (shouldAddBreadcrumb) {
4030
- const hub = core.getCurrentHub();
4031
- hub.addBreadcrumb(
4032
- {
4033
- category: 'console',
4034
- data: {
4035
- logger: 'replay',
4036
- },
4037
- level: 'info',
4038
- message,
4039
- },
4040
- { level: 'info' },
4041
- );
4038
+ addBreadcrumb(message);
4039
+ }
4040
+ }
4041
+
4042
+ /**
4043
+ * Log a message, and add a breadcrumb in the next tick.
4044
+ * This is necessary when the breadcrumb may be added before the replay is initialized.
4045
+ */
4046
+ function logInfoNextTick(message, shouldAddBreadcrumb) {
4047
+ if (!(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) {
4048
+ return;
4049
+ }
4050
+
4051
+ utils.logger.info(message);
4052
+
4053
+ if (shouldAddBreadcrumb) {
4054
+ // Wait a tick here to avoid race conditions for some initial logs
4055
+ // which may be added before replay is initialized
4056
+ setTimeout(() => {
4057
+ addBreadcrumb(message);
4058
+ }, 0);
4042
4059
  }
4043
4060
  }
4044
4061
 
4062
+ function addBreadcrumb(message) {
4063
+ const hub = core.getCurrentHub();
4064
+ hub.addBreadcrumb(
4065
+ {
4066
+ category: 'console',
4067
+ data: {
4068
+ logger: 'replay',
4069
+ },
4070
+ level: 'info',
4071
+ message,
4072
+ },
4073
+ { level: 'info' },
4074
+ );
4075
+ }
4076
+
4045
4077
  /** This error indicates that the event buffer size exceeded the limit.. */
4046
4078
  class EventBufferSizeExceededError extends Error {
4047
4079
  constructor() {
@@ -4056,9 +4088,12 @@ class EventBufferSizeExceededError extends Error {
4056
4088
  class EventBufferArray {
4057
4089
  /** All the events that are buffered to be sent. */
4058
4090
 
4091
+ /** @inheritdoc */
4092
+
4059
4093
  constructor() {
4060
4094
  this.events = [];
4061
4095
  this._totalSize = 0;
4096
+ this.hasCheckout = false;
4062
4097
  }
4063
4098
 
4064
4099
  /** @inheritdoc */
@@ -4103,6 +4138,7 @@ class EventBufferArray {
4103
4138
  clear() {
4104
4139
  this.events = [];
4105
4140
  this._totalSize = 0;
4141
+ this.hasCheckout = false;
4106
4142
  }
4107
4143
 
4108
4144
  /** @inheritdoc */
@@ -4222,11 +4258,13 @@ class WorkerHandler {
4222
4258
  * Exported only for testing.
4223
4259
  */
4224
4260
  class EventBufferCompressionWorker {
4261
+ /** @inheritdoc */
4225
4262
 
4226
4263
  constructor(worker) {
4227
4264
  this._worker = new WorkerHandler(worker);
4228
4265
  this._earliestTimestamp = null;
4229
4266
  this._totalSize = 0;
4267
+ this.hasCheckout = false;
4230
4268
  }
4231
4269
 
4232
4270
  /** @inheritdoc */
@@ -4286,6 +4324,8 @@ class EventBufferCompressionWorker {
4286
4324
  clear() {
4287
4325
  this._earliestTimestamp = null;
4288
4326
  this._totalSize = 0;
4327
+ this.hasCheckout = false;
4328
+
4289
4329
  // We do not wait on this, as we assume the order of messages is consistent for the worker
4290
4330
  void this._worker.postMessage('clear');
4291
4331
  }
@@ -4340,6 +4380,15 @@ class EventBufferProxy {
4340
4380
  return this._used.hasEvents;
4341
4381
  }
4342
4382
 
4383
+ /** @inheritdoc */
4384
+ get hasCheckout() {
4385
+ return this._used.hasCheckout;
4386
+ }
4387
+ /** @inheritdoc */
4388
+ set hasCheckout(value) {
4389
+ this._used.hasCheckout = value;
4390
+ }
4391
+
4343
4392
  /** @inheritDoc */
4344
4393
  destroy() {
4345
4394
  this._fallback.destroy();
@@ -4395,13 +4444,15 @@ class EventBufferProxy {
4395
4444
 
4396
4445
  /** Switch the used buffer to the compression worker. */
4397
4446
  async _switchToCompressionWorker() {
4398
- const { events } = this._fallback;
4447
+ const { events, hasCheckout } = this._fallback;
4399
4448
 
4400
4449
  const addEventPromises = [];
4401
4450
  for (const event of events) {
4402
4451
  addEventPromises.push(this._compression.addEvent(event));
4403
4452
  }
4404
4453
 
4454
+ this._compression.hasCheckout = hasCheckout;
4455
+
4405
4456
  // We switch over to the new buffer immediately - any further events will be added
4406
4457
  // after the previously buffered ones
4407
4458
  this._used = this._compression;
@@ -4601,7 +4652,7 @@ function fetchSession(traceInternals) {
4601
4652
 
4602
4653
  const sessionObj = JSON.parse(sessionStringFromStorage) ;
4603
4654
 
4604
- logInfo('[Replay] Loading existing session', traceInternals);
4655
+ logInfoNextTick('[Replay] Loading existing session', traceInternals);
4605
4656
 
4606
4657
  return makeSession(sessionObj);
4607
4658
  } catch (e) {
@@ -4637,10 +4688,10 @@ function getSession({
4637
4688
  // and when this session is expired, it will not be renewed until user
4638
4689
  // reloads.
4639
4690
  const discardedSession = makeSession({ sampled: false });
4640
- logInfo('[Replay] Session should not be refreshed', traceInternals);
4691
+ logInfoNextTick('[Replay] Session should not be refreshed', traceInternals);
4641
4692
  return { type: 'new', session: discardedSession };
4642
4693
  } else {
4643
- logInfo('[Replay] Session has expired', traceInternals);
4694
+ logInfoNextTick('[Replay] Session has expired', traceInternals);
4644
4695
  }
4645
4696
  // Otherwise continue to create a new session
4646
4697
  }
@@ -4650,7 +4701,7 @@ function getSession({
4650
4701
  sessionSampleRate,
4651
4702
  allowBuffering,
4652
4703
  });
4653
- logInfo('[Replay] Created new session', traceInternals);
4704
+ logInfoNextTick('[Replay] Created new session', traceInternals);
4654
4705
 
4655
4706
  return { type: 'new', session: newSession };
4656
4707
  }
@@ -4688,11 +4739,24 @@ async function addEvent(
4688
4739
  return null;
4689
4740
  }
4690
4741
 
4742
+ // Throw out events that are +60min from the initial timestamp
4743
+ if (timestampInMs > replay.getContext().initialTimestamp + replay.timeouts.maxSessionLife) {
4744
+ logInfo(
4745
+ `[Replay] Skipping event with timestamp ${timestampInMs} because it is after maxSessionLife`,
4746
+ replay.getOptions()._experiments.traceInternals,
4747
+ );
4748
+ return null;
4749
+ }
4750
+
4691
4751
  try {
4692
- if (isCheckout) {
4752
+ if (isCheckout && replay.recordingMode === 'buffer') {
4693
4753
  replay.eventBuffer.clear();
4694
4754
  }
4695
4755
 
4756
+ if (isCheckout) {
4757
+ replay.eventBuffer.hasCheckout = true;
4758
+ }
4759
+
4696
4760
  const replayOptions = replay.getOptions();
4697
4761
 
4698
4762
  const eventAfterPossibleCallback = maybeApplyCallback(event, replayOptions.beforeAddRecordingEvent);
@@ -4706,7 +4770,7 @@ async function addEvent(
4706
4770
  const reason = error && error instanceof EventBufferSizeExceededError ? 'addEventSizeExceeded' : 'addEvent';
4707
4771
 
4708
4772
  (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.error(error);
4709
- await replay.stop(reason);
4773
+ await replay.stop({ reason });
4710
4774
 
4711
4775
  const client = core.getCurrentHub().getClient();
4712
4776
 
@@ -7266,7 +7330,10 @@ class ReplayContainer {
7266
7330
  this.recordingMode = 'buffer';
7267
7331
  }
7268
7332
 
7269
- logInfo(`[Replay] Starting replay in ${this.recordingMode} mode`, this._options._experiments.traceInternals);
7333
+ logInfoNextTick(
7334
+ `[Replay] Starting replay in ${this.recordingMode} mode`,
7335
+ this._options._experiments.traceInternals,
7336
+ );
7270
7337
 
7271
7338
  this._initializeRecording();
7272
7339
  }
@@ -7287,7 +7354,7 @@ class ReplayContainer {
7287
7354
  throw new Error('Replay buffering is in progress, call `flush()` to save the replay');
7288
7355
  }
7289
7356
 
7290
- logInfo('[Replay] Starting replay in session mode', this._options._experiments.traceInternals);
7357
+ logInfoNextTick('[Replay] Starting replay in session mode', this._options._experiments.traceInternals);
7291
7358
 
7292
7359
  const previousSessionId = this.session && this.session.id;
7293
7360
 
@@ -7316,7 +7383,7 @@ class ReplayContainer {
7316
7383
  throw new Error('Replay recording is already in progress');
7317
7384
  }
7318
7385
 
7319
- logInfo('[Replay] Starting replay in buffer mode', this._options._experiments.traceInternals);
7386
+ logInfoNextTick('[Replay] Starting replay in buffer mode', this._options._experiments.traceInternals);
7320
7387
 
7321
7388
  const previousSessionId = this.session && this.session.id;
7322
7389
 
@@ -7381,7 +7448,7 @@ class ReplayContainer {
7381
7448
  * Currently, this needs to be manually called (e.g. for tests). Sentry SDK
7382
7449
  * does not support a teardown
7383
7450
  */
7384
- async stop(reason) {
7451
+ async stop({ forceFlush = false, reason } = {}) {
7385
7452
  if (!this._isEnabled) {
7386
7453
  return;
7387
7454
  }
@@ -7401,7 +7468,7 @@ class ReplayContainer {
7401
7468
  this._debouncedFlush.cancel();
7402
7469
  // See comment above re: `_isEnabled`, we "force" a flush, ignoring the
7403
7470
  // `_isEnabled` state of the plugin since it was disabled above.
7404
- if (this.recordingMode === 'session') {
7471
+ if (forceFlush) {
7405
7472
  await this._flush({ force: true });
7406
7473
  }
7407
7474
 
@@ -7790,7 +7857,7 @@ class ReplayContainer {
7790
7857
  this.session = session;
7791
7858
 
7792
7859
  if (!this.session.sampled) {
7793
- void this.stop('session not refreshed');
7860
+ void this.stop({ reason: 'session not refreshed' });
7794
7861
  return false;
7795
7862
  }
7796
7863
 
@@ -8080,6 +8147,15 @@ class ReplayContainer {
8080
8147
  // Note this empties the event buffer regardless of outcome of sending replay
8081
8148
  const recordingData = await this.eventBuffer.finish();
8082
8149
 
8150
+ const timestamp = Date.now();
8151
+
8152
+ // Check total duration again, to avoid sending outdated stuff
8153
+ // We leave 30s wiggle room to accomodate late flushing etc.
8154
+ // This _could_ happen when the browser is suspended during flushing, in which case we just want to stop
8155
+ if (timestamp - this._context.initialTimestamp > this.timeouts.maxSessionLife + 30000) {
8156
+ throw new Error('Session is too long, not sending replay');
8157
+ }
8158
+
8083
8159
  // NOTE: Copy values from instance members, as it's possible they could
8084
8160
  // change before the flush finishes.
8085
8161
  const replayId = this.session.id;
@@ -8095,7 +8171,7 @@ class ReplayContainer {
8095
8171
  eventContext,
8096
8172
  session: this.session,
8097
8173
  options: this.getOptions(),
8098
- timestamp: Date.now(),
8174
+ timestamp,
8099
8175
  });
8100
8176
  } catch (err) {
8101
8177
  this._handleException(err);
@@ -8103,7 +8179,7 @@ class ReplayContainer {
8103
8179
  // This means we retried 3 times and all of them failed,
8104
8180
  // or we ran into a problem we don't want to retry, like rate limiting.
8105
8181
  // In this case, we want to completely stop the replay - otherwise, we may get inconsistent segments
8106
- void this.stop('sendReplay');
8182
+ void this.stop({ reason: 'sendReplay' });
8107
8183
 
8108
8184
  const client = core.getCurrentHub().getClient();
8109
8185
 
@@ -8141,6 +8217,9 @@ class ReplayContainer {
8141
8217
  const now = Date.now();
8142
8218
  const duration = now - start;
8143
8219
 
8220
+ // A flush is about to happen, cancel any queued flushes
8221
+ this._debouncedFlush.cancel();
8222
+
8144
8223
  // If session is too short, or too long (allow some wiggle room over maxSessionLife), do not send it
8145
8224
  // This _should_ not happen, but it may happen if flush is triggered due to a page activity change or similar
8146
8225
  const tooShort = duration < this._options.minReplayDuration;
@@ -8150,6 +8229,7 @@ class ReplayContainer {
8150
8229
  `[Replay] Session duration (${Math.floor(duration / 1000)}s) is too ${
8151
8230
  tooShort ? 'short' : 'long'
8152
8231
  }, not sending replay.`,
8232
+ this._options._experiments.traceInternals,
8153
8233
  );
8154
8234
 
8155
8235
  if (tooShort) {
@@ -8158,8 +8238,11 @@ class ReplayContainer {
8158
8238
  return;
8159
8239
  }
8160
8240
 
8161
- // A flush is about to happen, cancel any queued flushes
8162
- this._debouncedFlush.cancel();
8241
+ const eventBuffer = this.eventBuffer;
8242
+ if (eventBuffer && this.session.segmentId === 0 && !eventBuffer.hasCheckout) {
8243
+ logInfo('[Replay] Flushing initial segment without checkout.', this._options._experiments.traceInternals);
8244
+ // TODO FN: Evaluate if we want to stop here, or remove this again?
8245
+ }
8163
8246
 
8164
8247
  // this._flushLock acts as a lock so that future calls to `_flush()`
8165
8248
  // will be blocked until this promise resolves
@@ -8215,7 +8298,7 @@ class ReplayContainer {
8215
8298
 
8216
8299
  // Stop replay if over the mutation limit
8217
8300
  if (overMutationLimit) {
8218
- void this.stop('mutationLimit');
8301
+ void this.stop({ reason: 'mutationLimit', forceFlush: this.recordingMode === 'session' });
8219
8302
  return false;
8220
8303
  }
8221
8304
 
@@ -8566,7 +8649,7 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
8566
8649
  return Promise.resolve();
8567
8650
  }
8568
8651
 
8569
- return this._replay.stop();
8652
+ return this._replay.stop({ forceFlush: this._replay.recordingMode === 'session' });
8570
8653
  }
8571
8654
 
8572
8655
  /**