@sentry/replay 7.55.2 → 7.56.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
@@ -1,5 +1,5 @@
1
1
  import { getCurrentHub, addGlobalEventProcessor, prepareEvent, setContext, captureException } from '@sentry/core';
2
- import { GLOBAL_OBJ, normalize, htmlTreeAsString, logger, uuid4, SENTRY_XHR_DATA_KEY, dropUndefinedKeys, stringMatchesSomePattern, addInstrumentationHandler, browserPerformanceTimeOrigin, createEnvelope, createEventEnvelopeHeaders, getSdkMetadataForEnvelopeHeader, isNodeEnv } from '@sentry/utils';
2
+ import { GLOBAL_OBJ, normalize, fill, htmlTreeAsString, logger, uuid4, SENTRY_XHR_DATA_KEY, dropUndefinedKeys, stringMatchesSomePattern, addInstrumentationHandler, browserPerformanceTimeOrigin, createEnvelope, createEventEnvelopeHeaders, getSdkMetadataForEnvelopeHeader, isNodeEnv } from '@sentry/utils';
3
3
 
4
4
  // exporting a separate copy of `WINDOW` rather than exporting the one from `@sentry/browser`
5
5
  // prevents the browser package from being bundled in the CDN bundle, and avoids a
@@ -42,6 +42,11 @@ const CONSOLE_ARG_MAX_SIZE = 5000;
42
42
  const SLOW_CLICK_THRESHOLD = 3000;
43
43
  /* For scroll actions after a click, we only look for a very short time period to detect programmatic scrolling. */
44
44
  const SLOW_CLICK_SCROLL_TIMEOUT = 300;
45
+ /* Clicks in this time period are considered e.g. double/triple clicks. */
46
+ const MULTI_CLICK_TIMEOUT = 1000;
47
+
48
+ /** When encountering a total segment size exceeding this size, stop the replay (as we cannot properly ingest it). */
49
+ const REPLAY_MAX_EVENT_BUFFER_SIZE = 20000000; // ~20MB
45
50
 
46
51
  var NodeType$1;
47
52
  (function (NodeType) {
@@ -3267,29 +3272,6 @@ record.takeFullSnapshot = (isCheckout) => {
3267
3272
  };
3268
3273
  record.mirror = mirror;
3269
3274
 
3270
- /**
3271
- * Create a breadcrumb for a replay.
3272
- */
3273
- function createBreadcrumb(
3274
- breadcrumb,
3275
- ) {
3276
- return {
3277
- timestamp: Date.now() / 1000,
3278
- type: 'default',
3279
- ...breadcrumb,
3280
- };
3281
- }
3282
-
3283
- var NodeType;
3284
- (function (NodeType) {
3285
- NodeType[NodeType["Document"] = 0] = "Document";
3286
- NodeType[NodeType["DocumentType"] = 1] = "DocumentType";
3287
- NodeType[NodeType["Element"] = 2] = "Element";
3288
- NodeType[NodeType["Text"] = 3] = "Text";
3289
- NodeType[NodeType["CDATA"] = 4] = "CDATA";
3290
- NodeType[NodeType["Comment"] = 5] = "Comment";
3291
- })(NodeType || (NodeType = {}));
3292
-
3293
3275
  /**
3294
3276
  * Add a breadcrumb event to replay.
3295
3277
  */
@@ -3322,116 +3304,324 @@ function addBreadcrumbEvent(replay, breadcrumb) {
3322
3304
  });
3323
3305
  }
3324
3306
 
3307
+ const INTERACTIVE_SELECTOR = 'button,a';
3308
+
3325
3309
  /**
3326
- * Detect a slow click on a button/a tag,
3327
- * and potentially create a corresponding breadcrumb.
3310
+ * For clicks, we check if the target is inside of a button or link
3311
+ * If so, we use this as the target instead
3312
+ * This is useful because if you click on the image in <button><img></button>,
3313
+ * The target will be the image, not the button, which we don't want here
3328
3314
  */
3329
- function detectSlowClick(
3330
- replay,
3331
- config,
3332
- clickBreadcrumb,
3333
- node,
3334
- ) {
3335
- if (ignoreElement(node, config)) {
3336
- return;
3315
+ function getClickTargetNode(event) {
3316
+ const target = getTargetNode(event);
3317
+
3318
+ if (!target || !(target instanceof Element)) {
3319
+ return target;
3337
3320
  }
3338
3321
 
3339
- /*
3340
- We consider a slow click a click on a button/a, which does not trigger one of:
3341
- - DOM mutation
3342
- - Scroll (within 100ms)
3343
- Within the given threshold time.
3344
- After time timeout time, we stop listening and mark it as a slow click anyhow.
3345
- */
3322
+ const closestInteractive = target.closest(INTERACTIVE_SELECTOR);
3323
+ return closestInteractive || target;
3324
+ }
3346
3325
 
3347
- let cleanup = () => {
3348
- // replaced further down
3349
- };
3326
+ /** Get the event target node. */
3327
+ function getTargetNode(event) {
3328
+ if (isEventWithTarget(event)) {
3329
+ return event.target ;
3330
+ }
3331
+
3332
+ return event;
3333
+ }
3350
3334
 
3351
- // After timeout time, def. consider this a slow click, and stop watching for mutations
3352
- const timeout = setTimeout(() => {
3353
- handleSlowClick(replay, clickBreadcrumb, config.timeout, 'timeout');
3354
- cleanup();
3355
- }, config.timeout);
3335
+ function isEventWithTarget(event) {
3336
+ return typeof event === 'object' && !!event && 'target' in event;
3337
+ }
3356
3338
 
3357
- const mutationHandler = () => {
3358
- maybeHandleSlowClick(replay, clickBreadcrumb, config.threshold, config.timeout, 'mutation');
3359
- cleanup();
3360
- };
3339
+ let handlers;
3361
3340
 
3362
- const scrollHandler = () => {
3363
- maybeHandleSlowClick(replay, clickBreadcrumb, config.scrollTimeout, config.timeout, 'scroll');
3364
- cleanup();
3341
+ /**
3342
+ * Register a handler to be called when `window.open()` is called.
3343
+ * Returns a cleanup function.
3344
+ */
3345
+ function onWindowOpen(cb) {
3346
+ // Ensure to only register this once
3347
+ if (!handlers) {
3348
+ handlers = [];
3349
+ monkeyPatchWindowOpen();
3350
+ }
3351
+
3352
+ handlers.push(cb);
3353
+
3354
+ return () => {
3355
+ const pos = handlers ? handlers.indexOf(cb) : -1;
3356
+ if (pos > -1) {
3357
+ (handlers ).splice(pos, 1);
3358
+ }
3365
3359
  };
3360
+ }
3366
3361
 
3367
- const obs = new MutationObserver(mutationHandler);
3362
+ function monkeyPatchWindowOpen() {
3363
+ fill(WINDOW, 'open', function (originalWindowOpen) {
3364
+ return function (...args) {
3365
+ if (handlers) {
3366
+ try {
3367
+ handlers.forEach(handler => handler());
3368
+ } catch (e) {
3369
+ // ignore errors in here
3370
+ }
3371
+ }
3368
3372
 
3369
- obs.observe(WINDOW.document.documentElement, {
3370
- attributes: true,
3371
- characterData: true,
3372
- childList: true,
3373
- subtree: true,
3373
+ return originalWindowOpen.apply(WINDOW, args);
3374
+ };
3374
3375
  });
3376
+ }
3375
3377
 
3376
- WINDOW.addEventListener('scroll', scrollHandler);
3378
+ /** Handle a click. */
3379
+ function handleClick(clickDetector, clickBreadcrumb, node) {
3380
+ clickDetector.handleClick(clickBreadcrumb, node);
3381
+ }
3377
3382
 
3378
- // Stop listening to scroll timeouts early
3379
- const scrollTimeout = setTimeout(() => {
3380
- WINDOW.removeEventListener('scroll', scrollHandler);
3381
- }, config.scrollTimeout);
3383
+ /** A click detector class that can be used to detect slow or rage clicks on elements. */
3384
+ class ClickDetector {
3385
+ // protected for testing
3386
+ __init() {this._lastMutation = 0;}
3387
+ __init2() {this._lastScroll = 0;}
3382
3388
 
3383
- cleanup = () => {
3384
- clearTimeout(timeout);
3385
- clearTimeout(scrollTimeout);
3386
- obs.disconnect();
3387
- WINDOW.removeEventListener('scroll', scrollHandler);
3388
- };
3389
- }
3389
+ __init3() {this._clicks = [];}
3390
3390
 
3391
- function maybeHandleSlowClick(
3392
- replay,
3393
- clickBreadcrumb,
3394
- threshold,
3395
- timeout,
3396
- endReason,
3397
- ) {
3398
- const now = Date.now();
3399
- const timeAfterClickMs = now - clickBreadcrumb.timestamp * 1000;
3391
+ constructor(
3392
+ replay,
3393
+ slowClickConfig,
3394
+ // Just for easier testing
3395
+ _addBreadcrumbEvent = addBreadcrumbEvent,
3396
+ ) {ClickDetector.prototype.__init.call(this);ClickDetector.prototype.__init2.call(this);ClickDetector.prototype.__init3.call(this);
3397
+ // We want everything in s, but options are in ms
3398
+ this._timeout = slowClickConfig.timeout / 1000;
3399
+ this._multiClickTimeout = slowClickConfig.multiClickTimeout / 1000;
3400
+ this._threshold = slowClickConfig.threshold / 1000;
3401
+ this._scollTimeout = slowClickConfig.scrollTimeout / 1000;
3402
+ this._replay = replay;
3403
+ this._ignoreSelector = slowClickConfig.ignoreSelector;
3404
+ this._addBreadcrumbEvent = _addBreadcrumbEvent;
3405
+ }
3400
3406
 
3401
- if (timeAfterClickMs > threshold) {
3402
- handleSlowClick(replay, clickBreadcrumb, Math.min(timeAfterClickMs, timeout), endReason);
3403
- return true;
3407
+ /** Register click detection handlers on mutation or scroll. */
3408
+ addListeners() {
3409
+ const mutationHandler = () => {
3410
+ this._lastMutation = nowInSeconds();
3411
+ };
3412
+
3413
+ const scrollHandler = () => {
3414
+ this._lastScroll = nowInSeconds();
3415
+ };
3416
+
3417
+ const cleanupWindowOpen = onWindowOpen(() => {
3418
+ // Treat window.open as mutation
3419
+ this._lastMutation = nowInSeconds();
3420
+ });
3421
+
3422
+ const clickHandler = (event) => {
3423
+ if (!event.target) {
3424
+ return;
3425
+ }
3426
+
3427
+ const node = getClickTargetNode(event);
3428
+ if (node) {
3429
+ this._handleMultiClick(node );
3430
+ }
3431
+ };
3432
+
3433
+ const obs = new MutationObserver(mutationHandler);
3434
+
3435
+ obs.observe(WINDOW.document.documentElement, {
3436
+ attributes: true,
3437
+ characterData: true,
3438
+ childList: true,
3439
+ subtree: true,
3440
+ });
3441
+
3442
+ WINDOW.addEventListener('scroll', scrollHandler, { passive: true });
3443
+ WINDOW.addEventListener('click', clickHandler, { passive: true });
3444
+
3445
+ this._teardown = () => {
3446
+ WINDOW.removeEventListener('scroll', scrollHandler);
3447
+ WINDOW.removeEventListener('click', clickHandler);
3448
+ cleanupWindowOpen();
3449
+
3450
+ obs.disconnect();
3451
+ this._clicks = [];
3452
+ this._lastMutation = 0;
3453
+ this._lastScroll = 0;
3454
+ };
3404
3455
  }
3405
3456
 
3406
- return false;
3407
- }
3457
+ /** Clean up listeners. */
3458
+ removeListeners() {
3459
+ if (this._teardown) {
3460
+ this._teardown();
3461
+ }
3408
3462
 
3409
- function handleSlowClick(
3410
- replay,
3411
- clickBreadcrumb,
3412
- timeAfterClickMs,
3413
- endReason,
3414
- ) {
3415
- const breadcrumb = {
3416
- message: clickBreadcrumb.message,
3417
- timestamp: clickBreadcrumb.timestamp,
3418
- category: 'ui.slowClickDetected',
3419
- data: {
3420
- ...clickBreadcrumb.data,
3421
- url: WINDOW.location.href,
3422
- // TODO FN: add parametrized route, when possible
3423
- timeAfterClickMs,
3424
- endReason,
3425
- },
3426
- };
3463
+ if (this._checkClickTimeout) {
3464
+ clearTimeout(this._checkClickTimeout);
3465
+ }
3466
+ }
3427
3467
 
3428
- addBreadcrumbEvent(replay, breadcrumb);
3468
+ /** Handle a click */
3469
+ handleClick(breadcrumb, node) {
3470
+ if (ignoreElement(node, this._ignoreSelector) || !isClickBreadcrumb(breadcrumb)) {
3471
+ return;
3472
+ }
3473
+
3474
+ const click = this._getClick(node);
3475
+
3476
+ if (click) {
3477
+ // this means a click on the same element was captured in the last 1s, so we consider this a multi click
3478
+ return;
3479
+ }
3480
+
3481
+ const newClick = {
3482
+ timestamp: breadcrumb.timestamp,
3483
+ clickBreadcrumb: breadcrumb,
3484
+ // Set this to 0 so we know it originates from the click breadcrumb
3485
+ clickCount: 0,
3486
+ node,
3487
+ };
3488
+ this._clicks.push(newClick);
3489
+
3490
+ // If this is the first new click, set a timeout to check for multi clicks
3491
+ if (this._clicks.length === 1) {
3492
+ this._scheduleCheckClicks();
3493
+ }
3494
+ }
3495
+
3496
+ /** Count multiple clicks on elements. */
3497
+ _handleMultiClick(node) {
3498
+ const click = this._getClick(node);
3499
+
3500
+ if (!click) {
3501
+ return;
3502
+ }
3503
+
3504
+ click.clickCount++;
3505
+ }
3506
+
3507
+ /** Try to get an existing click on the given element. */
3508
+ _getClick(node) {
3509
+ const now = nowInSeconds();
3510
+
3511
+ // Find any click on the same element in the last second
3512
+ // If one exists, we consider this click as a double/triple/etc click
3513
+ return this._clicks.find(click => click.node === node && now - click.timestamp < this._multiClickTimeout);
3514
+ }
3515
+
3516
+ /** Check the clicks that happened. */
3517
+ _checkClicks() {
3518
+ const timedOutClicks = [];
3519
+
3520
+ const now = nowInSeconds();
3521
+
3522
+ this._clicks.forEach(click => {
3523
+ if (!click.mutationAfter && this._lastMutation) {
3524
+ click.mutationAfter = click.timestamp <= this._lastMutation ? this._lastMutation - click.timestamp : undefined;
3525
+ }
3526
+ if (!click.scrollAfter && this._lastScroll) {
3527
+ click.scrollAfter = click.timestamp <= this._lastScroll ? this._lastScroll - click.timestamp : undefined;
3528
+ }
3529
+
3530
+ // If an action happens after the multi click threshold, we can skip waiting and handle the click right away
3531
+ const actionTime = click.scrollAfter || click.mutationAfter || 0;
3532
+ if (actionTime && actionTime >= this._multiClickTimeout) {
3533
+ timedOutClicks.push(click);
3534
+ return;
3535
+ }
3536
+
3537
+ if (click.timestamp + this._timeout <= now) {
3538
+ timedOutClicks.push(click);
3539
+ }
3540
+ });
3541
+
3542
+ // Remove "old" clicks
3543
+ for (const click of timedOutClicks) {
3544
+ this._generateBreadcrumbs(click);
3545
+
3546
+ const pos = this._clicks.indexOf(click);
3547
+ if (pos !== -1) {
3548
+ this._clicks.splice(pos, 1);
3549
+ }
3550
+ }
3551
+
3552
+ // Trigger new check, unless no clicks left
3553
+ if (this._clicks.length) {
3554
+ this._scheduleCheckClicks();
3555
+ }
3556
+ }
3557
+
3558
+ /** Generate matching breadcrumb(s) for the click. */
3559
+ _generateBreadcrumbs(click) {
3560
+ const replay = this._replay;
3561
+ const hadScroll = click.scrollAfter && click.scrollAfter <= this._scollTimeout;
3562
+ const hadMutation = click.mutationAfter && click.mutationAfter <= this._threshold;
3563
+
3564
+ const isSlowClick = !hadScroll && !hadMutation;
3565
+ const { clickCount, clickBreadcrumb } = click;
3566
+
3567
+ // Slow click
3568
+ if (isSlowClick) {
3569
+ // If `mutationAfter` is set, it means a mutation happened after the threshold, but before the timeout
3570
+ // If not, it means we just timed out without scroll & mutation
3571
+ const timeAfterClickMs = Math.min(click.mutationAfter || this._timeout, this._timeout) * 1000;
3572
+ const endReason = timeAfterClickMs < this._timeout * 1000 ? 'mutation' : 'timeout';
3573
+
3574
+ const breadcrumb = {
3575
+ type: 'default',
3576
+ message: clickBreadcrumb.message,
3577
+ timestamp: clickBreadcrumb.timestamp,
3578
+ category: 'ui.slowClickDetected',
3579
+ data: {
3580
+ ...clickBreadcrumb.data,
3581
+ url: WINDOW.location.href,
3582
+ route: replay.getCurrentRoute(),
3583
+ timeAfterClickMs,
3584
+ endReason,
3585
+ // If clickCount === 0, it means multiClick was not correctly captured here
3586
+ // - we still want to send 1 in this case
3587
+ clickCount: clickCount || 1,
3588
+ },
3589
+ };
3590
+
3591
+ this._addBreadcrumbEvent(replay, breadcrumb);
3592
+ return;
3593
+ }
3594
+
3595
+ // Multi click
3596
+ if (clickCount > 1) {
3597
+ const breadcrumb = {
3598
+ type: 'default',
3599
+ message: clickBreadcrumb.message,
3600
+ timestamp: clickBreadcrumb.timestamp,
3601
+ category: 'ui.multiClick',
3602
+ data: {
3603
+ ...clickBreadcrumb.data,
3604
+ url: WINDOW.location.href,
3605
+ route: replay.getCurrentRoute(),
3606
+ clickCount,
3607
+ metric: true,
3608
+ },
3609
+ };
3610
+
3611
+ this._addBreadcrumbEvent(replay, breadcrumb);
3612
+ }
3613
+ }
3614
+
3615
+ /** Schedule to check current clicks. */
3616
+ _scheduleCheckClicks() {
3617
+ this._checkClickTimeout = setTimeout(() => this._checkClicks(), 1000);
3618
+ }
3429
3619
  }
3430
3620
 
3431
3621
  const SLOW_CLICK_TAGS = ['A', 'BUTTON', 'INPUT'];
3432
3622
 
3433
3623
  /** exported for tests only */
3434
- function ignoreElement(node, config) {
3624
+ function ignoreElement(node, ignoreSelector) {
3435
3625
  if (!SLOW_CLICK_TAGS.includes(node.tagName)) {
3436
3626
  return true;
3437
3627
  }
@@ -3451,13 +3641,45 @@ function ignoreElement(node, config) {
3451
3641
  return true;
3452
3642
  }
3453
3643
 
3454
- if (config.ignoreSelector && node.matches(config.ignoreSelector)) {
3644
+ if (ignoreSelector && node.matches(ignoreSelector)) {
3455
3645
  return true;
3456
3646
  }
3457
3647
 
3458
3648
  return false;
3459
3649
  }
3460
3650
 
3651
+ function isClickBreadcrumb(breadcrumb) {
3652
+ return !!(breadcrumb.data && typeof breadcrumb.data.nodeId === 'number' && breadcrumb.timestamp);
3653
+ }
3654
+
3655
+ // This is good enough for us, and is easier to test/mock than `timestampInSeconds`
3656
+ function nowInSeconds() {
3657
+ return Date.now() / 1000;
3658
+ }
3659
+
3660
+ /**
3661
+ * Create a breadcrumb for a replay.
3662
+ */
3663
+ function createBreadcrumb(
3664
+ breadcrumb,
3665
+ ) {
3666
+ return {
3667
+ timestamp: Date.now() / 1000,
3668
+ type: 'default',
3669
+ ...breadcrumb,
3670
+ };
3671
+ }
3672
+
3673
+ var NodeType;
3674
+ (function (NodeType) {
3675
+ NodeType[NodeType["Document"] = 0] = "Document";
3676
+ NodeType[NodeType["DocumentType"] = 1] = "DocumentType";
3677
+ NodeType[NodeType["Element"] = 2] = "Element";
3678
+ NodeType[NodeType["Text"] = 3] = "Text";
3679
+ NodeType[NodeType["CDATA"] = 4] = "CDATA";
3680
+ NodeType[NodeType["Comment"] = 5] = "Comment";
3681
+ })(NodeType || (NodeType = {}));
3682
+
3461
3683
  // Note that these are the serialized attributes and not attributes directly on
3462
3684
  // the DOM Node. Attributes we are interested in:
3463
3685
  const ATTRIBUTES_TO_RECORD = new Set([
@@ -3497,17 +3719,6 @@ function getAttributesToRecord(attributes) {
3497
3719
  const handleDomListener = (
3498
3720
  replay,
3499
3721
  ) => {
3500
- const { slowClickTimeout, slowClickIgnoreSelectors } = replay.getOptions();
3501
-
3502
- const slowClickConfig = slowClickTimeout
3503
- ? {
3504
- threshold: Math.min(SLOW_CLICK_THRESHOLD, slowClickTimeout),
3505
- timeout: slowClickTimeout,
3506
- scrollTimeout: SLOW_CLICK_SCROLL_TIMEOUT,
3507
- ignoreSelector: slowClickIgnoreSelectors ? slowClickIgnoreSelectors.join(',') : '',
3508
- }
3509
- : undefined;
3510
-
3511
3722
  return (handlerData) => {
3512
3723
  if (!replay.isEnabled()) {
3513
3724
  return;
@@ -3522,10 +3733,9 @@ const handleDomListener = (
3522
3733
  const isClick = handlerData.name === 'click';
3523
3734
  const event = isClick && (handlerData.event );
3524
3735
  // Ignore clicks if ctrl/alt/meta keys are held down as they alter behavior of clicks (e.g. open in new tab)
3525
- if (isClick && slowClickConfig && event && !event.altKey && !event.metaKey && !event.ctrlKey) {
3526
- detectSlowClick(
3527
- replay,
3528
- slowClickConfig,
3736
+ if (isClick && replay.clickDetector && event && !event.altKey && !event.metaKey && !event.ctrlKey) {
3737
+ handleClick(
3738
+ replay.clickDetector,
3529
3739
  result ,
3530
3740
  getClickTargetNode(handlerData.event) ,
3531
3741
  );
@@ -3598,35 +3808,6 @@ function isRrwebNode(node) {
3598
3808
  return '__sn' in node;
3599
3809
  }
3600
3810
 
3601
- function getTargetNode(event) {
3602
- if (isEventWithTarget(event)) {
3603
- return event.target ;
3604
- }
3605
-
3606
- return event;
3607
- }
3608
-
3609
- const INTERACTIVE_SELECTOR = 'button,a';
3610
-
3611
- // For clicks, we check if the target is inside of a button or link
3612
- // If so, we use this as the target instead
3613
- // This is useful because if you click on the image in <button><img></button>,
3614
- // The target will be the image, not the button, which we don't want here
3615
- function getClickTargetNode(event) {
3616
- const target = getTargetNode(event);
3617
-
3618
- if (!target || !(target instanceof Element)) {
3619
- return target;
3620
- }
3621
-
3622
- const closestInteractive = target.closest(INTERACTIVE_SELECTOR);
3623
- return closestInteractive || target;
3624
- }
3625
-
3626
- function isEventWithTarget(event) {
3627
- return typeof event === 'object' && !!event && 'target' in event;
3628
- }
3629
-
3630
3811
  /** Handle keyboard events & create breadcrumbs. */
3631
3812
  function handleKeyboardEvent(replay, event) {
3632
3813
  if (!replay.isEnabled()) {
@@ -3839,7 +4020,9 @@ function timestampToMs(timestamp) {
3839
4020
  class EventBufferArray {
3840
4021
  /** All the events that are buffered to be sent. */
3841
4022
 
3842
- constructor() {
4023
+ __init() {this._totalSize = 0;}
4024
+
4025
+ constructor() {EventBufferArray.prototype.__init.call(this);
3843
4026
  this.events = [];
3844
4027
  }
3845
4028
 
@@ -3860,6 +4043,12 @@ class EventBufferArray {
3860
4043
 
3861
4044
  /** @inheritdoc */
3862
4045
  async addEvent(event) {
4046
+ const eventSize = JSON.stringify(event).length;
4047
+ this._totalSize += eventSize;
4048
+ if (this._totalSize > REPLAY_MAX_EVENT_BUFFER_SIZE) {
4049
+ throw new EventBufferSizeExceededError();
4050
+ }
4051
+
3863
4052
  this.events.push(event);
3864
4053
  }
3865
4054
 
@@ -3870,7 +4059,7 @@ class EventBufferArray {
3870
4059
  // events member so that we do not lose new events while uploading
3871
4060
  // attachment.
3872
4061
  const eventsRet = this.events;
3873
- this.events = [];
4062
+ this.clear();
3874
4063
  resolve(JSON.stringify(eventsRet));
3875
4064
  });
3876
4065
  }
@@ -3878,6 +4067,7 @@ class EventBufferArray {
3878
4067
  /** @inheritdoc */
3879
4068
  clear() {
3880
4069
  this.events = [];
4070
+ this._totalSize = 0;
3881
4071
  }
3882
4072
 
3883
4073
  /** @inheritdoc */
@@ -3998,7 +4188,9 @@ class WorkerHandler {
3998
4188
  */
3999
4189
  class EventBufferCompressionWorker {
4000
4190
 
4001
- constructor(worker) {
4191
+ __init() {this._totalSize = 0;}
4192
+
4193
+ constructor(worker) {EventBufferCompressionWorker.prototype.__init.call(this);
4002
4194
  this._worker = new WorkerHandler(worker);
4003
4195
  this._earliestTimestamp = null;
4004
4196
  }
@@ -4039,7 +4231,14 @@ class EventBufferCompressionWorker {
4039
4231
  this._earliestTimestamp = timestamp;
4040
4232
  }
4041
4233
 
4042
- return this._sendEventToWorker(event);
4234
+ const data = JSON.stringify(event);
4235
+ this._totalSize += data.length;
4236
+
4237
+ if (this._totalSize > REPLAY_MAX_EVENT_BUFFER_SIZE) {
4238
+ return Promise.reject(new EventBufferSizeExceededError());
4239
+ }
4240
+
4241
+ return this._sendEventToWorker(data);
4043
4242
  }
4044
4243
 
4045
4244
  /**
@@ -4052,6 +4251,7 @@ class EventBufferCompressionWorker {
4052
4251
  /** @inheritdoc */
4053
4252
  clear() {
4054
4253
  this._earliestTimestamp = null;
4254
+ this._totalSize = 0;
4055
4255
  // We do not wait on this, as we assume the order of messages is consistent for the worker
4056
4256
  void this._worker.postMessage('clear');
4057
4257
  }
@@ -4064,8 +4264,8 @@ class EventBufferCompressionWorker {
4064
4264
  /**
4065
4265
  * Send the event to the worker.
4066
4266
  */
4067
- _sendEventToWorker(event) {
4068
- return this._worker.postMessage('addEvent', JSON.stringify(event));
4267
+ _sendEventToWorker(data) {
4268
+ return this._worker.postMessage('addEvent', data);
4069
4269
  }
4070
4270
 
4071
4271
  /**
@@ -4075,6 +4275,7 @@ class EventBufferCompressionWorker {
4075
4275
  const response = await this._worker.postMessage('finish');
4076
4276
 
4077
4277
  this._earliestTimestamp = null;
4278
+ this._totalSize = 0;
4078
4279
 
4079
4280
  return response;
4080
4281
  }
@@ -4202,6 +4403,13 @@ function createEventBuffer({ useCompression }) {
4202
4403
  return new EventBufferArray();
4203
4404
  }
4204
4405
 
4406
+ /** This error indicates that the event buffer size exceeded the limit.. */
4407
+ class EventBufferSizeExceededError extends Error {
4408
+ constructor() {
4409
+ super(`Event buffer exceeded maximum size of ${REPLAY_MAX_EVENT_BUFFER_SIZE}.`);
4410
+ }
4411
+ }
4412
+
4205
4413
  /**
4206
4414
  * Removes the session from Session Storage and unsets session in replay instance
4207
4415
  */
@@ -4462,8 +4670,10 @@ async function addEvent(
4462
4670
 
4463
4671
  return await replay.eventBuffer.addEvent(eventAfterPossibleCallback);
4464
4672
  } catch (error) {
4673
+ const reason = error && error instanceof EventBufferSizeExceededError ? 'addEventSizeExceeded' : 'addEvent';
4674
+
4465
4675
  (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error(error);
4466
- await replay.stop('addEvent');
4676
+ await replay.stop(reason);
4467
4677
 
4468
4678
  const client = getCurrentHub().getClient();
4469
4679
 
@@ -6932,6 +7142,22 @@ class ReplayContainer {
6932
7142
  // ... per 5s
6933
7143
  5,
6934
7144
  );
7145
+
7146
+ const { slowClickTimeout, slowClickIgnoreSelectors } = this.getOptions();
7147
+
7148
+ const slowClickConfig = slowClickTimeout
7149
+ ? {
7150
+ threshold: Math.min(SLOW_CLICK_THRESHOLD, slowClickTimeout),
7151
+ timeout: slowClickTimeout,
7152
+ scrollTimeout: SLOW_CLICK_SCROLL_TIMEOUT,
7153
+ ignoreSelector: slowClickIgnoreSelectors ? slowClickIgnoreSelectors.join(',') : '',
7154
+ multiClickTimeout: MULTI_CLICK_TIMEOUT,
7155
+ }
7156
+ : undefined;
7157
+
7158
+ if (slowClickConfig) {
7159
+ this.clickDetector = new ClickDetector(this, slowClickConfig);
7160
+ }
6935
7161
  }
6936
7162
 
6937
7163
  /** Get the event context. */
@@ -7510,6 +7736,10 @@ class ReplayContainer {
7510
7736
  WINDOW.addEventListener('focus', this._handleWindowFocus);
7511
7737
  WINDOW.addEventListener('keydown', this._handleKeyboardEvent);
7512
7738
 
7739
+ if (this.clickDetector) {
7740
+ this.clickDetector.addListeners();
7741
+ }
7742
+
7513
7743
  // There is no way to remove these listeners, so ensure they are only added once
7514
7744
  if (!this._hasInitializedCoreListeners) {
7515
7745
  addGlobalListeners(this);
@@ -7539,6 +7769,10 @@ class ReplayContainer {
7539
7769
  WINDOW.removeEventListener('focus', this._handleWindowFocus);
7540
7770
  WINDOW.removeEventListener('keydown', this._handleKeyboardEvent);
7541
7771
 
7772
+ if (this.clickDetector) {
7773
+ this.clickDetector.removeListeners();
7774
+ }
7775
+
7542
7776
  if (this._performanceObserver) {
7543
7777
  this._performanceObserver.disconnect();
7544
7778
  this._performanceObserver = null;