@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/cjs/index.js +393 -159
- package/cjs/index.js.map +1 -1
- package/esm/index.js +394 -160
- package/esm/index.js.map +1 -1
- package/package.json +5 -5
- package/types/constants.d.ts +3 -0
- package/types/constants.d.ts.map +1 -1
- package/types/coreHandlers/handleClick.d.ts +40 -0
- package/types/coreHandlers/handleClick.d.ts.map +1 -0
- package/types/coreHandlers/handleDom.d.ts +1 -6
- package/types/coreHandlers/handleDom.d.ts.map +1 -1
- package/types/coreHandlers/util/domUtils.d.ts +19 -0
- package/types/coreHandlers/util/domUtils.d.ts.map +1 -0
- package/types/coreHandlers/util/onWindowOpen.d.ts +8 -0
- package/types/coreHandlers/util/onWindowOpen.d.ts.map +1 -0
- package/types/eventBuffer/EventBufferArray.d.ts +1 -0
- package/types/eventBuffer/EventBufferArray.d.ts.map +1 -1
- package/types/eventBuffer/EventBufferCompressionWorker.d.ts +1 -0
- package/types/eventBuffer/EventBufferCompressionWorker.d.ts.map +1 -1
- package/types/eventBuffer/index.d.ts +4 -0
- package/types/eventBuffer/index.d.ts.map +1 -1
- package/types/replay.d.ts +2 -0
- package/types/replay.d.ts.map +1 -1
- package/types/types/replay.d.ts +8 -1
- package/types/types/replay.d.ts.map +1 -1
- package/types/types/replayFrame.d.ts +15 -3
- package/types/types/replayFrame.d.ts.map +1 -1
- package/types/util/addEvent.d.ts.map +1 -1
- package/types/coreHandlers/handleSlowClick.d.ts +0 -14
- 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
|
-
*
|
|
3329
|
-
*
|
|
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
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
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
|
-
|
|
3343
|
-
|
|
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
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3328
|
+
/** Get the event target node. */
|
|
3329
|
+
function getTargetNode(event) {
|
|
3330
|
+
if (isEventWithTarget(event)) {
|
|
3331
|
+
return event.target ;
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
return event;
|
|
3335
|
+
}
|
|
3352
3336
|
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
cleanup();
|
|
3357
|
-
}, config.timeout);
|
|
3337
|
+
function isEventWithTarget(event) {
|
|
3338
|
+
return typeof event === 'object' && !!event && 'target' in event;
|
|
3339
|
+
}
|
|
3358
3340
|
|
|
3359
|
-
|
|
3360
|
-
maybeHandleSlowClick(replay, clickBreadcrumb, config.threshold, config.timeout, 'mutation');
|
|
3361
|
-
cleanup();
|
|
3362
|
-
};
|
|
3341
|
+
let handlers;
|
|
3363
3342
|
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
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
|
+
}
|
|
3353
|
+
|
|
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
|
-
|
|
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
|
-
|
|
3372
|
-
|
|
3373
|
-
characterData: true,
|
|
3374
|
-
childList: true,
|
|
3375
|
-
subtree: true,
|
|
3375
|
+
return originalWindowOpen.apply(WINDOW, args);
|
|
3376
|
+
};
|
|
3376
3377
|
});
|
|
3378
|
+
}
|
|
3377
3379
|
|
|
3378
|
-
|
|
3380
|
+
/** Handle a click. */
|
|
3381
|
+
function handleClick(clickDetector, clickBreadcrumb, node) {
|
|
3382
|
+
clickDetector.handleClick(clickBreadcrumb, node);
|
|
3383
|
+
}
|
|
3379
3384
|
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
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
|
-
|
|
3386
|
-
clearTimeout(timeout);
|
|
3387
|
-
clearTimeout(scrollTimeout);
|
|
3388
|
-
obs.disconnect();
|
|
3389
|
-
WINDOW.removeEventListener('scroll', scrollHandler);
|
|
3390
|
-
};
|
|
3391
|
-
}
|
|
3391
|
+
__init3() {this._clicks = [];}
|
|
3392
3392
|
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
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
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
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
|
-
|
|
3409
|
-
|
|
3459
|
+
/** Clean up listeners. */
|
|
3460
|
+
removeListeners() {
|
|
3461
|
+
if (this._teardown) {
|
|
3462
|
+
this._teardown();
|
|
3463
|
+
}
|
|
3410
3464
|
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
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 &&
|
|
3528
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
4070
|
-
return this._worker.postMessage('addEvent',
|
|
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(
|
|
4678
|
+
await replay.stop(reason);
|
|
4469
4679
|
|
|
4470
4680
|
const client = core.getCurrentHub().getClient();
|
|
4471
4681
|
|
|
@@ -6934,6 +7144,22 @@ class ReplayContainer {
|
|
|
6934
7144
|
// ... per 5s
|
|
6935
7145
|
5,
|
|
6936
7146
|
);
|
|
7147
|
+
|
|
7148
|
+
const { slowClickTimeout, slowClickIgnoreSelectors } = this.getOptions();
|
|
7149
|
+
|
|
7150
|
+
const slowClickConfig = slowClickTimeout
|
|
7151
|
+
? {
|
|
7152
|
+
threshold: Math.min(SLOW_CLICK_THRESHOLD, slowClickTimeout),
|
|
7153
|
+
timeout: slowClickTimeout,
|
|
7154
|
+
scrollTimeout: SLOW_CLICK_SCROLL_TIMEOUT,
|
|
7155
|
+
ignoreSelector: slowClickIgnoreSelectors ? slowClickIgnoreSelectors.join(',') : '',
|
|
7156
|
+
multiClickTimeout: MULTI_CLICK_TIMEOUT,
|
|
7157
|
+
}
|
|
7158
|
+
: undefined;
|
|
7159
|
+
|
|
7160
|
+
if (slowClickConfig) {
|
|
7161
|
+
this.clickDetector = new ClickDetector(this, slowClickConfig);
|
|
7162
|
+
}
|
|
6937
7163
|
}
|
|
6938
7164
|
|
|
6939
7165
|
/** Get the event context. */
|
|
@@ -7512,6 +7738,10 @@ class ReplayContainer {
|
|
|
7512
7738
|
WINDOW.addEventListener('focus', this._handleWindowFocus);
|
|
7513
7739
|
WINDOW.addEventListener('keydown', this._handleKeyboardEvent);
|
|
7514
7740
|
|
|
7741
|
+
if (this.clickDetector) {
|
|
7742
|
+
this.clickDetector.addListeners();
|
|
7743
|
+
}
|
|
7744
|
+
|
|
7515
7745
|
// There is no way to remove these listeners, so ensure they are only added once
|
|
7516
7746
|
if (!this._hasInitializedCoreListeners) {
|
|
7517
7747
|
addGlobalListeners(this);
|
|
@@ -7541,6 +7771,10 @@ class ReplayContainer {
|
|
|
7541
7771
|
WINDOW.removeEventListener('focus', this._handleWindowFocus);
|
|
7542
7772
|
WINDOW.removeEventListener('keydown', this._handleKeyboardEvent);
|
|
7543
7773
|
|
|
7774
|
+
if (this.clickDetector) {
|
|
7775
|
+
this.clickDetector.removeListeners();
|
|
7776
|
+
}
|
|
7777
|
+
|
|
7544
7778
|
if (this._performanceObserver) {
|
|
7545
7779
|
this._performanceObserver.disconnect();
|
|
7546
7780
|
this._performanceObserver = null;
|