@sentry/replay 7.55.1 → 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/index.d.ts +1 -1
- package/types/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 +16 -4
- package/types/types/replayFrame.d.ts.map +1 -1
- package/types/types/rrweb.d.ts +10 -1
- package/types/types/rrweb.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/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
|
-
*
|
|
3327
|
-
*
|
|
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
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
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
|
-
|
|
3341
|
-
|
|
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
|
-
|
|
3348
|
-
|
|
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
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
cleanup();
|
|
3355
|
-
}, config.timeout);
|
|
3335
|
+
function isEventWithTarget(event) {
|
|
3336
|
+
return typeof event === 'object' && !!event && 'target' in event;
|
|
3337
|
+
}
|
|
3356
3338
|
|
|
3357
|
-
|
|
3358
|
-
maybeHandleSlowClick(replay, clickBreadcrumb, config.threshold, config.timeout, 'mutation');
|
|
3359
|
-
cleanup();
|
|
3360
|
-
};
|
|
3339
|
+
let handlers;
|
|
3361
3340
|
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3370
|
-
|
|
3371
|
-
characterData: true,
|
|
3372
|
-
childList: true,
|
|
3373
|
-
subtree: true,
|
|
3373
|
+
return originalWindowOpen.apply(WINDOW, args);
|
|
3374
|
+
};
|
|
3374
3375
|
});
|
|
3376
|
+
}
|
|
3375
3377
|
|
|
3376
|
-
|
|
3378
|
+
/** Handle a click. */
|
|
3379
|
+
function handleClick(clickDetector, clickBreadcrumb, node) {
|
|
3380
|
+
clickDetector.handleClick(clickBreadcrumb, node);
|
|
3381
|
+
}
|
|
3377
3382
|
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
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
|
-
|
|
3384
|
-
clearTimeout(timeout);
|
|
3385
|
-
clearTimeout(scrollTimeout);
|
|
3386
|
-
obs.disconnect();
|
|
3387
|
-
WINDOW.removeEventListener('scroll', scrollHandler);
|
|
3388
|
-
};
|
|
3389
|
-
}
|
|
3389
|
+
__init3() {this._clicks = [];}
|
|
3390
3390
|
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
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
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
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
|
-
|
|
3407
|
-
|
|
3457
|
+
/** Clean up listeners. */
|
|
3458
|
+
removeListeners() {
|
|
3459
|
+
if (this._teardown) {
|
|
3460
|
+
this._teardown();
|
|
3461
|
+
}
|
|
3408
3462
|
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
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 &&
|
|
3526
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
4068
|
-
return this._worker.postMessage('addEvent',
|
|
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(
|
|
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;
|