@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.
- package/cjs/index.js +403 -191
- package/cjs/index.js.map +1 -1
- package/esm/index.js +404 -192
- package/esm/index.js.map +1 -1
- package/package.json +13 -6
- package/types/constants.d.ts +3 -0
- package/types/constants.d.ts.map +1 -1
- package/types/coreHandlers/handleAfterSendEvent.d.ts +1 -1
- package/types/coreHandlers/handleAfterSendEvent.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/handleScope.d.ts +1 -1
- package/types/coreHandlers/handleScope.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 +6 -0
- package/types/replay.d.ts.map +1 -1
- package/types/types/performance.d.ts +9 -9
- package/types/types/performance.d.ts.map +1 -1
- package/types/types/replay.d.ts +20 -13
- package/types/types/replay.d.ts.map +1 -1
- package/types/types/replayFrame.d.ts +19 -7
- package/types/types/replayFrame.d.ts.map +1 -1
- package/types/types/request.d.ts +4 -4
- package/types/types/request.d.ts.map +1 -1
- package/types/types/rrweb.d.ts +4 -4
- package/types/types/rrweb.d.ts.map +1 -1
- package/types/util/addEvent.d.ts.map +1 -1
- package/types/util/createPerformanceEntries.d.ts.map +1 -1
- package/types/util/debounce.d.ts +3 -3
- package/types/util/debounce.d.ts.map +1 -1
- package/types/util/dedupePerformanceEntries.d.ts.map +1 -1
- package/types/util/getPrivacyOptions.d.ts +1 -1
- package/types/util/getPrivacyOptions.d.ts.map +1 -1
- package/types/util/handleRecordingEmit.d.ts +1 -1
- package/types/util/handleRecordingEmit.d.ts.map +1 -1
- package/types/util/truncateJson/constants.d.ts +1 -1
- package/types/util/truncateJson/constants.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
|
+
}
|
|
3350
3331
|
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
handleSlowClick(replay, clickBreadcrumb, config.timeout, 'timeout');
|
|
3354
|
-
cleanup();
|
|
3355
|
-
}, config.timeout);
|
|
3332
|
+
return event;
|
|
3333
|
+
}
|
|
3356
3334
|
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3335
|
+
function isEventWithTarget(event) {
|
|
3336
|
+
return typeof event === 'object' && !!event && 'target' in event;
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
let handlers;
|
|
3340
|
+
|
|
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
|
+
}
|
|
3361
3351
|
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
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
|
|
|
@@ -4795,7 +5005,7 @@ function handleFetch(handlerData) {
|
|
|
4795
5005
|
name: url,
|
|
4796
5006
|
data: {
|
|
4797
5007
|
method,
|
|
4798
|
-
statusCode: response
|
|
5008
|
+
statusCode: response ? (response ).status : undefined,
|
|
4799
5009
|
},
|
|
4800
5010
|
};
|
|
4801
5011
|
}
|
|
@@ -6397,41 +6607,12 @@ function getHandleRecordingEmit(replay) {
|
|
|
6397
6607
|
}
|
|
6398
6608
|
}
|
|
6399
6609
|
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
// TODO: We want this as an experiment so that we can test
|
|
6403
|
-
// internally and create metrics before making this the default
|
|
6404
|
-
if (options._experiments.delayFlushOnCheckout) {
|
|
6610
|
+
if (replay.recordingMode === 'session') {
|
|
6405
6611
|
// If the full snapshot is due to an initial load, we will not have
|
|
6406
6612
|
// a previous session ID. In this case, we want to buffer events
|
|
6407
6613
|
// for a set amount of time before flushing. This can help avoid
|
|
6408
6614
|
// capturing replays of users that immediately close the window.
|
|
6409
|
-
|
|
6410
|
-
// buffer, instead of checking inside of timeout, this will make our
|
|
6411
|
-
// tests a bit cleaner as we will need to wait on the delay in order to
|
|
6412
|
-
// do nothing.
|
|
6413
|
-
setTimeout(() => replay.conditionalFlush(), options._experiments.delayFlushOnCheckout);
|
|
6414
|
-
|
|
6415
|
-
// Cancel any previously debounced flushes to ensure there are no [near]
|
|
6416
|
-
// simultaneous flushes happening. The latter request should be
|
|
6417
|
-
// insignificant in this case, so wait for additional user interaction to
|
|
6418
|
-
// trigger a new flush.
|
|
6419
|
-
//
|
|
6420
|
-
// This can happen because there's no guarantee that a recording event
|
|
6421
|
-
// happens first. e.g. a mouse click can happen and trigger a debounced
|
|
6422
|
-
// flush before the checkout.
|
|
6423
|
-
replay.cancelFlush();
|
|
6424
|
-
|
|
6425
|
-
return true;
|
|
6426
|
-
}
|
|
6427
|
-
|
|
6428
|
-
// Flush immediately so that we do not miss the first segment, otherwise
|
|
6429
|
-
// it can prevent loading on the UI. This will cause an increase in short
|
|
6430
|
-
// replays (e.g. opening and closing a tab quickly), but these can be
|
|
6431
|
-
// filtered on the UI.
|
|
6432
|
-
if (replay.recordingMode === 'session') {
|
|
6433
|
-
// We want to ensure the worker is ready, as otherwise we'd always send the first event uncompressed
|
|
6434
|
-
void replay.flushImmediate();
|
|
6615
|
+
void replay.flush();
|
|
6435
6616
|
}
|
|
6436
6617
|
|
|
6437
6618
|
return true;
|
|
@@ -6932,6 +7113,22 @@ class ReplayContainer {
|
|
|
6932
7113
|
// ... per 5s
|
|
6933
7114
|
5,
|
|
6934
7115
|
);
|
|
7116
|
+
|
|
7117
|
+
const { slowClickTimeout, slowClickIgnoreSelectors } = this.getOptions();
|
|
7118
|
+
|
|
7119
|
+
const slowClickConfig = slowClickTimeout
|
|
7120
|
+
? {
|
|
7121
|
+
threshold: Math.min(SLOW_CLICK_THRESHOLD, slowClickTimeout),
|
|
7122
|
+
timeout: slowClickTimeout,
|
|
7123
|
+
scrollTimeout: SLOW_CLICK_SCROLL_TIMEOUT,
|
|
7124
|
+
ignoreSelector: slowClickIgnoreSelectors ? slowClickIgnoreSelectors.join(',') : '',
|
|
7125
|
+
multiClickTimeout: MULTI_CLICK_TIMEOUT,
|
|
7126
|
+
}
|
|
7127
|
+
: undefined;
|
|
7128
|
+
|
|
7129
|
+
if (slowClickConfig) {
|
|
7130
|
+
this.clickDetector = new ClickDetector(this, slowClickConfig);
|
|
7131
|
+
}
|
|
6935
7132
|
}
|
|
6936
7133
|
|
|
6937
7134
|
/** Get the event context. */
|
|
@@ -7297,6 +7494,13 @@ class ReplayContainer {
|
|
|
7297
7494
|
return this.flushImmediate();
|
|
7298
7495
|
}
|
|
7299
7496
|
|
|
7497
|
+
/**
|
|
7498
|
+
* Flush using debounce flush
|
|
7499
|
+
*/
|
|
7500
|
+
flush() {
|
|
7501
|
+
return this._debouncedFlush() ;
|
|
7502
|
+
}
|
|
7503
|
+
|
|
7300
7504
|
/**
|
|
7301
7505
|
* Always flush via `_debouncedFlush` so that we do not have flushes triggered
|
|
7302
7506
|
* from calling both `flush` and `_debouncedFlush`. Otherwise, there could be
|
|
@@ -7510,6 +7714,10 @@ class ReplayContainer {
|
|
|
7510
7714
|
WINDOW.addEventListener('focus', this._handleWindowFocus);
|
|
7511
7715
|
WINDOW.addEventListener('keydown', this._handleKeyboardEvent);
|
|
7512
7716
|
|
|
7717
|
+
if (this.clickDetector) {
|
|
7718
|
+
this.clickDetector.addListeners();
|
|
7719
|
+
}
|
|
7720
|
+
|
|
7513
7721
|
// There is no way to remove these listeners, so ensure they are only added once
|
|
7514
7722
|
if (!this._hasInitializedCoreListeners) {
|
|
7515
7723
|
addGlobalListeners(this);
|
|
@@ -7539,6 +7747,10 @@ class ReplayContainer {
|
|
|
7539
7747
|
WINDOW.removeEventListener('focus', this._handleWindowFocus);
|
|
7540
7748
|
WINDOW.removeEventListener('keydown', this._handleKeyboardEvent);
|
|
7541
7749
|
|
|
7750
|
+
if (this.clickDetector) {
|
|
7751
|
+
this.clickDetector.removeListeners();
|
|
7752
|
+
}
|
|
7753
|
+
|
|
7542
7754
|
if (this._performanceObserver) {
|
|
7543
7755
|
this._performanceObserver.disconnect();
|
|
7544
7756
|
this._performanceObserver = null;
|