@sentry/replay 7.55.2 → 7.57.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.
Files changed (50) hide show
  1. package/cjs/index.js +403 -191
  2. package/cjs/index.js.map +1 -1
  3. package/esm/index.js +404 -192
  4. package/esm/index.js.map +1 -1
  5. package/package.json +13 -6
  6. package/types/constants.d.ts +3 -0
  7. package/types/constants.d.ts.map +1 -1
  8. package/types/coreHandlers/handleAfterSendEvent.d.ts +1 -1
  9. package/types/coreHandlers/handleAfterSendEvent.d.ts.map +1 -1
  10. package/types/coreHandlers/handleClick.d.ts +40 -0
  11. package/types/coreHandlers/handleClick.d.ts.map +1 -0
  12. package/types/coreHandlers/handleDom.d.ts +1 -6
  13. package/types/coreHandlers/handleDom.d.ts.map +1 -1
  14. package/types/coreHandlers/handleScope.d.ts +1 -1
  15. package/types/coreHandlers/handleScope.d.ts.map +1 -1
  16. package/types/coreHandlers/util/domUtils.d.ts +19 -0
  17. package/types/coreHandlers/util/domUtils.d.ts.map +1 -0
  18. package/types/coreHandlers/util/onWindowOpen.d.ts +8 -0
  19. package/types/coreHandlers/util/onWindowOpen.d.ts.map +1 -0
  20. package/types/eventBuffer/EventBufferArray.d.ts +1 -0
  21. package/types/eventBuffer/EventBufferArray.d.ts.map +1 -1
  22. package/types/eventBuffer/EventBufferCompressionWorker.d.ts +1 -0
  23. package/types/eventBuffer/EventBufferCompressionWorker.d.ts.map +1 -1
  24. package/types/eventBuffer/index.d.ts +4 -0
  25. package/types/eventBuffer/index.d.ts.map +1 -1
  26. package/types/replay.d.ts +6 -0
  27. package/types/replay.d.ts.map +1 -1
  28. package/types/types/performance.d.ts +9 -9
  29. package/types/types/performance.d.ts.map +1 -1
  30. package/types/types/replay.d.ts +20 -13
  31. package/types/types/replay.d.ts.map +1 -1
  32. package/types/types/replayFrame.d.ts +19 -7
  33. package/types/types/replayFrame.d.ts.map +1 -1
  34. package/types/types/request.d.ts +4 -4
  35. package/types/types/request.d.ts.map +1 -1
  36. package/types/types/rrweb.d.ts +4 -4
  37. package/types/types/rrweb.d.ts.map +1 -1
  38. package/types/util/addEvent.d.ts.map +1 -1
  39. package/types/util/createPerformanceEntries.d.ts.map +1 -1
  40. package/types/util/debounce.d.ts +3 -3
  41. package/types/util/debounce.d.ts.map +1 -1
  42. package/types/util/dedupePerformanceEntries.d.ts.map +1 -1
  43. package/types/util/getPrivacyOptions.d.ts +1 -1
  44. package/types/util/getPrivacyOptions.d.ts.map +1 -1
  45. package/types/util/handleRecordingEmit.d.ts +1 -1
  46. package/types/util/handleRecordingEmit.d.ts.map +1 -1
  47. package/types/util/truncateJson/constants.d.ts +1 -1
  48. package/types/util/truncateJson/constants.d.ts.map +1 -1
  49. package/types/coreHandlers/handleSlowClick.d.ts +0 -14
  50. package/types/coreHandlers/handleSlowClick.d.ts.map +0 -1
package/cjs/index.js CHANGED
@@ -44,6 +44,11 @@ const CONSOLE_ARG_MAX_SIZE = 5000;
44
44
  const SLOW_CLICK_THRESHOLD = 3000;
45
45
  /* For scroll actions after a click, we only look for a very short time period to detect programmatic scrolling. */
46
46
  const SLOW_CLICK_SCROLL_TIMEOUT = 300;
47
+ /* Clicks in this time period are considered e.g. double/triple clicks. */
48
+ const MULTI_CLICK_TIMEOUT = 1000;
49
+
50
+ /** When encountering a total segment size exceeding this size, stop the replay (as we cannot properly ingest it). */
51
+ const REPLAY_MAX_EVENT_BUFFER_SIZE = 20000000; // ~20MB
47
52
 
48
53
  var NodeType$1;
49
54
  (function (NodeType) {
@@ -3269,29 +3274,6 @@ record.takeFullSnapshot = (isCheckout) => {
3269
3274
  };
3270
3275
  record.mirror = mirror;
3271
3276
 
3272
- /**
3273
- * Create a breadcrumb for a replay.
3274
- */
3275
- function createBreadcrumb(
3276
- breadcrumb,
3277
- ) {
3278
- return {
3279
- timestamp: Date.now() / 1000,
3280
- type: 'default',
3281
- ...breadcrumb,
3282
- };
3283
- }
3284
-
3285
- var NodeType;
3286
- (function (NodeType) {
3287
- NodeType[NodeType["Document"] = 0] = "Document";
3288
- NodeType[NodeType["DocumentType"] = 1] = "DocumentType";
3289
- NodeType[NodeType["Element"] = 2] = "Element";
3290
- NodeType[NodeType["Text"] = 3] = "Text";
3291
- NodeType[NodeType["CDATA"] = 4] = "CDATA";
3292
- NodeType[NodeType["Comment"] = 5] = "Comment";
3293
- })(NodeType || (NodeType = {}));
3294
-
3295
3277
  /**
3296
3278
  * Add a breadcrumb event to replay.
3297
3279
  */
@@ -3324,116 +3306,324 @@ function addBreadcrumbEvent(replay, breadcrumb) {
3324
3306
  });
3325
3307
  }
3326
3308
 
3309
+ const INTERACTIVE_SELECTOR = 'button,a';
3310
+
3327
3311
  /**
3328
- * Detect a slow click on a button/a tag,
3329
- * and potentially create a corresponding breadcrumb.
3312
+ * For clicks, we check if the target is inside of a button or link
3313
+ * If so, we use this as the target instead
3314
+ * This is useful because if you click on the image in <button><img></button>,
3315
+ * The target will be the image, not the button, which we don't want here
3330
3316
  */
3331
- function detectSlowClick(
3332
- replay,
3333
- config,
3334
- clickBreadcrumb,
3335
- node,
3336
- ) {
3337
- if (ignoreElement(node, config)) {
3338
- return;
3317
+ function getClickTargetNode(event) {
3318
+ const target = getTargetNode(event);
3319
+
3320
+ if (!target || !(target instanceof Element)) {
3321
+ return target;
3339
3322
  }
3340
3323
 
3341
- /*
3342
- We consider a slow click a click on a button/a, which does not trigger one of:
3343
- - DOM mutation
3344
- - Scroll (within 100ms)
3345
- Within the given threshold time.
3346
- After time timeout time, we stop listening and mark it as a slow click anyhow.
3347
- */
3324
+ const closestInteractive = target.closest(INTERACTIVE_SELECTOR);
3325
+ return closestInteractive || target;
3326
+ }
3348
3327
 
3349
- let cleanup = () => {
3350
- // replaced further down
3351
- };
3328
+ /** Get the event target node. */
3329
+ function getTargetNode(event) {
3330
+ if (isEventWithTarget(event)) {
3331
+ return event.target ;
3332
+ }
3352
3333
 
3353
- // After timeout time, def. consider this a slow click, and stop watching for mutations
3354
- const timeout = setTimeout(() => {
3355
- handleSlowClick(replay, clickBreadcrumb, config.timeout, 'timeout');
3356
- cleanup();
3357
- }, config.timeout);
3334
+ return event;
3335
+ }
3358
3336
 
3359
- const mutationHandler = () => {
3360
- maybeHandleSlowClick(replay, clickBreadcrumb, config.threshold, config.timeout, 'mutation');
3361
- cleanup();
3362
- };
3337
+ function isEventWithTarget(event) {
3338
+ return typeof event === 'object' && !!event && 'target' in event;
3339
+ }
3340
+
3341
+ let handlers;
3342
+
3343
+ /**
3344
+ * Register a handler to be called when `window.open()` is called.
3345
+ * Returns a cleanup function.
3346
+ */
3347
+ function onWindowOpen(cb) {
3348
+ // Ensure to only register this once
3349
+ if (!handlers) {
3350
+ handlers = [];
3351
+ monkeyPatchWindowOpen();
3352
+ }
3363
3353
 
3364
- const scrollHandler = () => {
3365
- maybeHandleSlowClick(replay, clickBreadcrumb, config.scrollTimeout, config.timeout, 'scroll');
3366
- cleanup();
3354
+ handlers.push(cb);
3355
+
3356
+ return () => {
3357
+ const pos = handlers ? handlers.indexOf(cb) : -1;
3358
+ if (pos > -1) {
3359
+ (handlers ).splice(pos, 1);
3360
+ }
3367
3361
  };
3362
+ }
3368
3363
 
3369
- const obs = new MutationObserver(mutationHandler);
3364
+ function monkeyPatchWindowOpen() {
3365
+ utils.fill(WINDOW, 'open', function (originalWindowOpen) {
3366
+ return function (...args) {
3367
+ if (handlers) {
3368
+ try {
3369
+ handlers.forEach(handler => handler());
3370
+ } catch (e) {
3371
+ // ignore errors in here
3372
+ }
3373
+ }
3370
3374
 
3371
- obs.observe(WINDOW.document.documentElement, {
3372
- attributes: true,
3373
- characterData: true,
3374
- childList: true,
3375
- subtree: true,
3375
+ return originalWindowOpen.apply(WINDOW, args);
3376
+ };
3376
3377
  });
3378
+ }
3377
3379
 
3378
- WINDOW.addEventListener('scroll', scrollHandler);
3380
+ /** Handle a click. */
3381
+ function handleClick(clickDetector, clickBreadcrumb, node) {
3382
+ clickDetector.handleClick(clickBreadcrumb, node);
3383
+ }
3379
3384
 
3380
- // Stop listening to scroll timeouts early
3381
- const scrollTimeout = setTimeout(() => {
3382
- WINDOW.removeEventListener('scroll', scrollHandler);
3383
- }, config.scrollTimeout);
3385
+ /** A click detector class that can be used to detect slow or rage clicks on elements. */
3386
+ class ClickDetector {
3387
+ // protected for testing
3388
+ __init() {this._lastMutation = 0;}
3389
+ __init2() {this._lastScroll = 0;}
3384
3390
 
3385
- cleanup = () => {
3386
- clearTimeout(timeout);
3387
- clearTimeout(scrollTimeout);
3388
- obs.disconnect();
3389
- WINDOW.removeEventListener('scroll', scrollHandler);
3390
- };
3391
- }
3391
+ __init3() {this._clicks = [];}
3392
3392
 
3393
- function maybeHandleSlowClick(
3394
- replay,
3395
- clickBreadcrumb,
3396
- threshold,
3397
- timeout,
3398
- endReason,
3399
- ) {
3400
- const now = Date.now();
3401
- const timeAfterClickMs = now - clickBreadcrumb.timestamp * 1000;
3393
+ constructor(
3394
+ replay,
3395
+ slowClickConfig,
3396
+ // Just for easier testing
3397
+ _addBreadcrumbEvent = addBreadcrumbEvent,
3398
+ ) {ClickDetector.prototype.__init.call(this);ClickDetector.prototype.__init2.call(this);ClickDetector.prototype.__init3.call(this);
3399
+ // We want everything in s, but options are in ms
3400
+ this._timeout = slowClickConfig.timeout / 1000;
3401
+ this._multiClickTimeout = slowClickConfig.multiClickTimeout / 1000;
3402
+ this._threshold = slowClickConfig.threshold / 1000;
3403
+ this._scollTimeout = slowClickConfig.scrollTimeout / 1000;
3404
+ this._replay = replay;
3405
+ this._ignoreSelector = slowClickConfig.ignoreSelector;
3406
+ this._addBreadcrumbEvent = _addBreadcrumbEvent;
3407
+ }
3402
3408
 
3403
- if (timeAfterClickMs > threshold) {
3404
- handleSlowClick(replay, clickBreadcrumb, Math.min(timeAfterClickMs, timeout), endReason);
3405
- return true;
3409
+ /** Register click detection handlers on mutation or scroll. */
3410
+ addListeners() {
3411
+ const mutationHandler = () => {
3412
+ this._lastMutation = nowInSeconds();
3413
+ };
3414
+
3415
+ const scrollHandler = () => {
3416
+ this._lastScroll = nowInSeconds();
3417
+ };
3418
+
3419
+ const cleanupWindowOpen = onWindowOpen(() => {
3420
+ // Treat window.open as mutation
3421
+ this._lastMutation = nowInSeconds();
3422
+ });
3423
+
3424
+ const clickHandler = (event) => {
3425
+ if (!event.target) {
3426
+ return;
3427
+ }
3428
+
3429
+ const node = getClickTargetNode(event);
3430
+ if (node) {
3431
+ this._handleMultiClick(node );
3432
+ }
3433
+ };
3434
+
3435
+ const obs = new MutationObserver(mutationHandler);
3436
+
3437
+ obs.observe(WINDOW.document.documentElement, {
3438
+ attributes: true,
3439
+ characterData: true,
3440
+ childList: true,
3441
+ subtree: true,
3442
+ });
3443
+
3444
+ WINDOW.addEventListener('scroll', scrollHandler, { passive: true });
3445
+ WINDOW.addEventListener('click', clickHandler, { passive: true });
3446
+
3447
+ this._teardown = () => {
3448
+ WINDOW.removeEventListener('scroll', scrollHandler);
3449
+ WINDOW.removeEventListener('click', clickHandler);
3450
+ cleanupWindowOpen();
3451
+
3452
+ obs.disconnect();
3453
+ this._clicks = [];
3454
+ this._lastMutation = 0;
3455
+ this._lastScroll = 0;
3456
+ };
3406
3457
  }
3407
3458
 
3408
- return false;
3409
- }
3459
+ /** Clean up listeners. */
3460
+ removeListeners() {
3461
+ if (this._teardown) {
3462
+ this._teardown();
3463
+ }
3410
3464
 
3411
- function handleSlowClick(
3412
- replay,
3413
- clickBreadcrumb,
3414
- timeAfterClickMs,
3415
- endReason,
3416
- ) {
3417
- const breadcrumb = {
3418
- message: clickBreadcrumb.message,
3419
- timestamp: clickBreadcrumb.timestamp,
3420
- category: 'ui.slowClickDetected',
3421
- data: {
3422
- ...clickBreadcrumb.data,
3423
- url: WINDOW.location.href,
3424
- // TODO FN: add parametrized route, when possible
3425
- timeAfterClickMs,
3426
- endReason,
3427
- },
3428
- };
3465
+ if (this._checkClickTimeout) {
3466
+ clearTimeout(this._checkClickTimeout);
3467
+ }
3468
+ }
3429
3469
 
3430
- addBreadcrumbEvent(replay, breadcrumb);
3470
+ /** Handle a click */
3471
+ handleClick(breadcrumb, node) {
3472
+ if (ignoreElement(node, this._ignoreSelector) || !isClickBreadcrumb(breadcrumb)) {
3473
+ return;
3474
+ }
3475
+
3476
+ const click = this._getClick(node);
3477
+
3478
+ if (click) {
3479
+ // this means a click on the same element was captured in the last 1s, so we consider this a multi click
3480
+ return;
3481
+ }
3482
+
3483
+ const newClick = {
3484
+ timestamp: breadcrumb.timestamp,
3485
+ clickBreadcrumb: breadcrumb,
3486
+ // Set this to 0 so we know it originates from the click breadcrumb
3487
+ clickCount: 0,
3488
+ node,
3489
+ };
3490
+ this._clicks.push(newClick);
3491
+
3492
+ // If this is the first new click, set a timeout to check for multi clicks
3493
+ if (this._clicks.length === 1) {
3494
+ this._scheduleCheckClicks();
3495
+ }
3496
+ }
3497
+
3498
+ /** Count multiple clicks on elements. */
3499
+ _handleMultiClick(node) {
3500
+ const click = this._getClick(node);
3501
+
3502
+ if (!click) {
3503
+ return;
3504
+ }
3505
+
3506
+ click.clickCount++;
3507
+ }
3508
+
3509
+ /** Try to get an existing click on the given element. */
3510
+ _getClick(node) {
3511
+ const now = nowInSeconds();
3512
+
3513
+ // Find any click on the same element in the last second
3514
+ // If one exists, we consider this click as a double/triple/etc click
3515
+ return this._clicks.find(click => click.node === node && now - click.timestamp < this._multiClickTimeout);
3516
+ }
3517
+
3518
+ /** Check the clicks that happened. */
3519
+ _checkClicks() {
3520
+ const timedOutClicks = [];
3521
+
3522
+ const now = nowInSeconds();
3523
+
3524
+ this._clicks.forEach(click => {
3525
+ if (!click.mutationAfter && this._lastMutation) {
3526
+ click.mutationAfter = click.timestamp <= this._lastMutation ? this._lastMutation - click.timestamp : undefined;
3527
+ }
3528
+ if (!click.scrollAfter && this._lastScroll) {
3529
+ click.scrollAfter = click.timestamp <= this._lastScroll ? this._lastScroll - click.timestamp : undefined;
3530
+ }
3531
+
3532
+ // If an action happens after the multi click threshold, we can skip waiting and handle the click right away
3533
+ const actionTime = click.scrollAfter || click.mutationAfter || 0;
3534
+ if (actionTime && actionTime >= this._multiClickTimeout) {
3535
+ timedOutClicks.push(click);
3536
+ return;
3537
+ }
3538
+
3539
+ if (click.timestamp + this._timeout <= now) {
3540
+ timedOutClicks.push(click);
3541
+ }
3542
+ });
3543
+
3544
+ // Remove "old" clicks
3545
+ for (const click of timedOutClicks) {
3546
+ this._generateBreadcrumbs(click);
3547
+
3548
+ const pos = this._clicks.indexOf(click);
3549
+ if (pos !== -1) {
3550
+ this._clicks.splice(pos, 1);
3551
+ }
3552
+ }
3553
+
3554
+ // Trigger new check, unless no clicks left
3555
+ if (this._clicks.length) {
3556
+ this._scheduleCheckClicks();
3557
+ }
3558
+ }
3559
+
3560
+ /** Generate matching breadcrumb(s) for the click. */
3561
+ _generateBreadcrumbs(click) {
3562
+ const replay = this._replay;
3563
+ const hadScroll = click.scrollAfter && click.scrollAfter <= this._scollTimeout;
3564
+ const hadMutation = click.mutationAfter && click.mutationAfter <= this._threshold;
3565
+
3566
+ const isSlowClick = !hadScroll && !hadMutation;
3567
+ const { clickCount, clickBreadcrumb } = click;
3568
+
3569
+ // Slow click
3570
+ if (isSlowClick) {
3571
+ // If `mutationAfter` is set, it means a mutation happened after the threshold, but before the timeout
3572
+ // If not, it means we just timed out without scroll & mutation
3573
+ const timeAfterClickMs = Math.min(click.mutationAfter || this._timeout, this._timeout) * 1000;
3574
+ const endReason = timeAfterClickMs < this._timeout * 1000 ? 'mutation' : 'timeout';
3575
+
3576
+ const breadcrumb = {
3577
+ type: 'default',
3578
+ message: clickBreadcrumb.message,
3579
+ timestamp: clickBreadcrumb.timestamp,
3580
+ category: 'ui.slowClickDetected',
3581
+ data: {
3582
+ ...clickBreadcrumb.data,
3583
+ url: WINDOW.location.href,
3584
+ route: replay.getCurrentRoute(),
3585
+ timeAfterClickMs,
3586
+ endReason,
3587
+ // If clickCount === 0, it means multiClick was not correctly captured here
3588
+ // - we still want to send 1 in this case
3589
+ clickCount: clickCount || 1,
3590
+ },
3591
+ };
3592
+
3593
+ this._addBreadcrumbEvent(replay, breadcrumb);
3594
+ return;
3595
+ }
3596
+
3597
+ // Multi click
3598
+ if (clickCount > 1) {
3599
+ const breadcrumb = {
3600
+ type: 'default',
3601
+ message: clickBreadcrumb.message,
3602
+ timestamp: clickBreadcrumb.timestamp,
3603
+ category: 'ui.multiClick',
3604
+ data: {
3605
+ ...clickBreadcrumb.data,
3606
+ url: WINDOW.location.href,
3607
+ route: replay.getCurrentRoute(),
3608
+ clickCount,
3609
+ metric: true,
3610
+ },
3611
+ };
3612
+
3613
+ this._addBreadcrumbEvent(replay, breadcrumb);
3614
+ }
3615
+ }
3616
+
3617
+ /** Schedule to check current clicks. */
3618
+ _scheduleCheckClicks() {
3619
+ this._checkClickTimeout = setTimeout(() => this._checkClicks(), 1000);
3620
+ }
3431
3621
  }
3432
3622
 
3433
3623
  const SLOW_CLICK_TAGS = ['A', 'BUTTON', 'INPUT'];
3434
3624
 
3435
3625
  /** exported for tests only */
3436
- function ignoreElement(node, config) {
3626
+ function ignoreElement(node, ignoreSelector) {
3437
3627
  if (!SLOW_CLICK_TAGS.includes(node.tagName)) {
3438
3628
  return true;
3439
3629
  }
@@ -3453,13 +3643,45 @@ function ignoreElement(node, config) {
3453
3643
  return true;
3454
3644
  }
3455
3645
 
3456
- if (config.ignoreSelector && node.matches(config.ignoreSelector)) {
3646
+ if (ignoreSelector && node.matches(ignoreSelector)) {
3457
3647
  return true;
3458
3648
  }
3459
3649
 
3460
3650
  return false;
3461
3651
  }
3462
3652
 
3653
+ function isClickBreadcrumb(breadcrumb) {
3654
+ return !!(breadcrumb.data && typeof breadcrumb.data.nodeId === 'number' && breadcrumb.timestamp);
3655
+ }
3656
+
3657
+ // This is good enough for us, and is easier to test/mock than `timestampInSeconds`
3658
+ function nowInSeconds() {
3659
+ return Date.now() / 1000;
3660
+ }
3661
+
3662
+ /**
3663
+ * Create a breadcrumb for a replay.
3664
+ */
3665
+ function createBreadcrumb(
3666
+ breadcrumb,
3667
+ ) {
3668
+ return {
3669
+ timestamp: Date.now() / 1000,
3670
+ type: 'default',
3671
+ ...breadcrumb,
3672
+ };
3673
+ }
3674
+
3675
+ var NodeType;
3676
+ (function (NodeType) {
3677
+ NodeType[NodeType["Document"] = 0] = "Document";
3678
+ NodeType[NodeType["DocumentType"] = 1] = "DocumentType";
3679
+ NodeType[NodeType["Element"] = 2] = "Element";
3680
+ NodeType[NodeType["Text"] = 3] = "Text";
3681
+ NodeType[NodeType["CDATA"] = 4] = "CDATA";
3682
+ NodeType[NodeType["Comment"] = 5] = "Comment";
3683
+ })(NodeType || (NodeType = {}));
3684
+
3463
3685
  // Note that these are the serialized attributes and not attributes directly on
3464
3686
  // the DOM Node. Attributes we are interested in:
3465
3687
  const ATTRIBUTES_TO_RECORD = new Set([
@@ -3499,17 +3721,6 @@ function getAttributesToRecord(attributes) {
3499
3721
  const handleDomListener = (
3500
3722
  replay,
3501
3723
  ) => {
3502
- const { slowClickTimeout, slowClickIgnoreSelectors } = replay.getOptions();
3503
-
3504
- const slowClickConfig = slowClickTimeout
3505
- ? {
3506
- threshold: Math.min(SLOW_CLICK_THRESHOLD, slowClickTimeout),
3507
- timeout: slowClickTimeout,
3508
- scrollTimeout: SLOW_CLICK_SCROLL_TIMEOUT,
3509
- ignoreSelector: slowClickIgnoreSelectors ? slowClickIgnoreSelectors.join(',') : '',
3510
- }
3511
- : undefined;
3512
-
3513
3724
  return (handlerData) => {
3514
3725
  if (!replay.isEnabled()) {
3515
3726
  return;
@@ -3524,10 +3735,9 @@ const handleDomListener = (
3524
3735
  const isClick = handlerData.name === 'click';
3525
3736
  const event = isClick && (handlerData.event );
3526
3737
  // Ignore clicks if ctrl/alt/meta keys are held down as they alter behavior of clicks (e.g. open in new tab)
3527
- if (isClick && slowClickConfig && event && !event.altKey && !event.metaKey && !event.ctrlKey) {
3528
- detectSlowClick(
3529
- replay,
3530
- slowClickConfig,
3738
+ if (isClick && replay.clickDetector && event && !event.altKey && !event.metaKey && !event.ctrlKey) {
3739
+ handleClick(
3740
+ replay.clickDetector,
3531
3741
  result ,
3532
3742
  getClickTargetNode(handlerData.event) ,
3533
3743
  );
@@ -3600,35 +3810,6 @@ function isRrwebNode(node) {
3600
3810
  return '__sn' in node;
3601
3811
  }
3602
3812
 
3603
- function getTargetNode(event) {
3604
- if (isEventWithTarget(event)) {
3605
- return event.target ;
3606
- }
3607
-
3608
- return event;
3609
- }
3610
-
3611
- const INTERACTIVE_SELECTOR = 'button,a';
3612
-
3613
- // For clicks, we check if the target is inside of a button or link
3614
- // If so, we use this as the target instead
3615
- // This is useful because if you click on the image in <button><img></button>,
3616
- // The target will be the image, not the button, which we don't want here
3617
- function getClickTargetNode(event) {
3618
- const target = getTargetNode(event);
3619
-
3620
- if (!target || !(target instanceof Element)) {
3621
- return target;
3622
- }
3623
-
3624
- const closestInteractive = target.closest(INTERACTIVE_SELECTOR);
3625
- return closestInteractive || target;
3626
- }
3627
-
3628
- function isEventWithTarget(event) {
3629
- return typeof event === 'object' && !!event && 'target' in event;
3630
- }
3631
-
3632
3813
  /** Handle keyboard events & create breadcrumbs. */
3633
3814
  function handleKeyboardEvent(replay, event) {
3634
3815
  if (!replay.isEnabled()) {
@@ -3841,7 +4022,9 @@ function timestampToMs(timestamp) {
3841
4022
  class EventBufferArray {
3842
4023
  /** All the events that are buffered to be sent. */
3843
4024
 
3844
- constructor() {
4025
+ __init() {this._totalSize = 0;}
4026
+
4027
+ constructor() {EventBufferArray.prototype.__init.call(this);
3845
4028
  this.events = [];
3846
4029
  }
3847
4030
 
@@ -3862,6 +4045,12 @@ class EventBufferArray {
3862
4045
 
3863
4046
  /** @inheritdoc */
3864
4047
  async addEvent(event) {
4048
+ const eventSize = JSON.stringify(event).length;
4049
+ this._totalSize += eventSize;
4050
+ if (this._totalSize > REPLAY_MAX_EVENT_BUFFER_SIZE) {
4051
+ throw new EventBufferSizeExceededError();
4052
+ }
4053
+
3865
4054
  this.events.push(event);
3866
4055
  }
3867
4056
 
@@ -3872,7 +4061,7 @@ class EventBufferArray {
3872
4061
  // events member so that we do not lose new events while uploading
3873
4062
  // attachment.
3874
4063
  const eventsRet = this.events;
3875
- this.events = [];
4064
+ this.clear();
3876
4065
  resolve(JSON.stringify(eventsRet));
3877
4066
  });
3878
4067
  }
@@ -3880,6 +4069,7 @@ class EventBufferArray {
3880
4069
  /** @inheritdoc */
3881
4070
  clear() {
3882
4071
  this.events = [];
4072
+ this._totalSize = 0;
3883
4073
  }
3884
4074
 
3885
4075
  /** @inheritdoc */
@@ -4000,7 +4190,9 @@ class WorkerHandler {
4000
4190
  */
4001
4191
  class EventBufferCompressionWorker {
4002
4192
 
4003
- constructor(worker) {
4193
+ __init() {this._totalSize = 0;}
4194
+
4195
+ constructor(worker) {EventBufferCompressionWorker.prototype.__init.call(this);
4004
4196
  this._worker = new WorkerHandler(worker);
4005
4197
  this._earliestTimestamp = null;
4006
4198
  }
@@ -4041,7 +4233,14 @@ class EventBufferCompressionWorker {
4041
4233
  this._earliestTimestamp = timestamp;
4042
4234
  }
4043
4235
 
4044
- return this._sendEventToWorker(event);
4236
+ const data = JSON.stringify(event);
4237
+ this._totalSize += data.length;
4238
+
4239
+ if (this._totalSize > REPLAY_MAX_EVENT_BUFFER_SIZE) {
4240
+ return Promise.reject(new EventBufferSizeExceededError());
4241
+ }
4242
+
4243
+ return this._sendEventToWorker(data);
4045
4244
  }
4046
4245
 
4047
4246
  /**
@@ -4054,6 +4253,7 @@ class EventBufferCompressionWorker {
4054
4253
  /** @inheritdoc */
4055
4254
  clear() {
4056
4255
  this._earliestTimestamp = null;
4256
+ this._totalSize = 0;
4057
4257
  // We do not wait on this, as we assume the order of messages is consistent for the worker
4058
4258
  void this._worker.postMessage('clear');
4059
4259
  }
@@ -4066,8 +4266,8 @@ class EventBufferCompressionWorker {
4066
4266
  /**
4067
4267
  * Send the event to the worker.
4068
4268
  */
4069
- _sendEventToWorker(event) {
4070
- return this._worker.postMessage('addEvent', JSON.stringify(event));
4269
+ _sendEventToWorker(data) {
4270
+ return this._worker.postMessage('addEvent', data);
4071
4271
  }
4072
4272
 
4073
4273
  /**
@@ -4077,6 +4277,7 @@ class EventBufferCompressionWorker {
4077
4277
  const response = await this._worker.postMessage('finish');
4078
4278
 
4079
4279
  this._earliestTimestamp = null;
4280
+ this._totalSize = 0;
4080
4281
 
4081
4282
  return response;
4082
4283
  }
@@ -4204,6 +4405,13 @@ function createEventBuffer({ useCompression }) {
4204
4405
  return new EventBufferArray();
4205
4406
  }
4206
4407
 
4408
+ /** This error indicates that the event buffer size exceeded the limit.. */
4409
+ class EventBufferSizeExceededError extends Error {
4410
+ constructor() {
4411
+ super(`Event buffer exceeded maximum size of ${REPLAY_MAX_EVENT_BUFFER_SIZE}.`);
4412
+ }
4413
+ }
4414
+
4207
4415
  /**
4208
4416
  * Removes the session from Session Storage and unsets session in replay instance
4209
4417
  */
@@ -4464,8 +4672,10 @@ async function addEvent(
4464
4672
 
4465
4673
  return await replay.eventBuffer.addEvent(eventAfterPossibleCallback);
4466
4674
  } catch (error) {
4675
+ const reason = error && error instanceof EventBufferSizeExceededError ? 'addEventSizeExceeded' : 'addEvent';
4676
+
4467
4677
  (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.error(error);
4468
- await replay.stop('addEvent');
4678
+ await replay.stop(reason);
4469
4679
 
4470
4680
  const client = core.getCurrentHub().getClient();
4471
4681
 
@@ -4797,7 +5007,7 @@ function handleFetch(handlerData) {
4797
5007
  name: url,
4798
5008
  data: {
4799
5009
  method,
4800
- statusCode: response && (response ).status,
5010
+ statusCode: response ? (response ).status : undefined,
4801
5011
  },
4802
5012
  };
4803
5013
  }
@@ -6399,41 +6609,12 @@ function getHandleRecordingEmit(replay) {
6399
6609
  }
6400
6610
  }
6401
6611
 
6402
- const options = replay.getOptions();
6403
-
6404
- // TODO: We want this as an experiment so that we can test
6405
- // internally and create metrics before making this the default
6406
- if (options._experiments.delayFlushOnCheckout) {
6612
+ if (replay.recordingMode === 'session') {
6407
6613
  // If the full snapshot is due to an initial load, we will not have
6408
6614
  // a previous session ID. In this case, we want to buffer events
6409
6615
  // for a set amount of time before flushing. This can help avoid
6410
6616
  // capturing replays of users that immediately close the window.
6411
- // TODO: We should check `recordingMode` here and do nothing if it's
6412
- // buffer, instead of checking inside of timeout, this will make our
6413
- // tests a bit cleaner as we will need to wait on the delay in order to
6414
- // do nothing.
6415
- setTimeout(() => replay.conditionalFlush(), options._experiments.delayFlushOnCheckout);
6416
-
6417
- // Cancel any previously debounced flushes to ensure there are no [near]
6418
- // simultaneous flushes happening. The latter request should be
6419
- // insignificant in this case, so wait for additional user interaction to
6420
- // trigger a new flush.
6421
- //
6422
- // This can happen because there's no guarantee that a recording event
6423
- // happens first. e.g. a mouse click can happen and trigger a debounced
6424
- // flush before the checkout.
6425
- replay.cancelFlush();
6426
-
6427
- return true;
6428
- }
6429
-
6430
- // Flush immediately so that we do not miss the first segment, otherwise
6431
- // it can prevent loading on the UI. This will cause an increase in short
6432
- // replays (e.g. opening and closing a tab quickly), but these can be
6433
- // filtered on the UI.
6434
- if (replay.recordingMode === 'session') {
6435
- // We want to ensure the worker is ready, as otherwise we'd always send the first event uncompressed
6436
- void replay.flushImmediate();
6617
+ void replay.flush();
6437
6618
  }
6438
6619
 
6439
6620
  return true;
@@ -6934,6 +7115,22 @@ class ReplayContainer {
6934
7115
  // ... per 5s
6935
7116
  5,
6936
7117
  );
7118
+
7119
+ const { slowClickTimeout, slowClickIgnoreSelectors } = this.getOptions();
7120
+
7121
+ const slowClickConfig = slowClickTimeout
7122
+ ? {
7123
+ threshold: Math.min(SLOW_CLICK_THRESHOLD, slowClickTimeout),
7124
+ timeout: slowClickTimeout,
7125
+ scrollTimeout: SLOW_CLICK_SCROLL_TIMEOUT,
7126
+ ignoreSelector: slowClickIgnoreSelectors ? slowClickIgnoreSelectors.join(',') : '',
7127
+ multiClickTimeout: MULTI_CLICK_TIMEOUT,
7128
+ }
7129
+ : undefined;
7130
+
7131
+ if (slowClickConfig) {
7132
+ this.clickDetector = new ClickDetector(this, slowClickConfig);
7133
+ }
6937
7134
  }
6938
7135
 
6939
7136
  /** Get the event context. */
@@ -7299,6 +7496,13 @@ class ReplayContainer {
7299
7496
  return this.flushImmediate();
7300
7497
  }
7301
7498
 
7499
+ /**
7500
+ * Flush using debounce flush
7501
+ */
7502
+ flush() {
7503
+ return this._debouncedFlush() ;
7504
+ }
7505
+
7302
7506
  /**
7303
7507
  * Always flush via `_debouncedFlush` so that we do not have flushes triggered
7304
7508
  * from calling both `flush` and `_debouncedFlush`. Otherwise, there could be
@@ -7512,6 +7716,10 @@ class ReplayContainer {
7512
7716
  WINDOW.addEventListener('focus', this._handleWindowFocus);
7513
7717
  WINDOW.addEventListener('keydown', this._handleKeyboardEvent);
7514
7718
 
7719
+ if (this.clickDetector) {
7720
+ this.clickDetector.addListeners();
7721
+ }
7722
+
7515
7723
  // There is no way to remove these listeners, so ensure they are only added once
7516
7724
  if (!this._hasInitializedCoreListeners) {
7517
7725
  addGlobalListeners(this);
@@ -7541,6 +7749,10 @@ class ReplayContainer {
7541
7749
  WINDOW.removeEventListener('focus', this._handleWindowFocus);
7542
7750
  WINDOW.removeEventListener('keydown', this._handleKeyboardEvent);
7543
7751
 
7752
+ if (this.clickDetector) {
7753
+ this.clickDetector.removeListeners();
7754
+ }
7755
+
7544
7756
  if (this._performanceObserver) {
7545
7757
  this._performanceObserver.disconnect();
7546
7758
  this._performanceObserver = null;