@sentry/replay 7.52.0 → 7.53.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/README.md +11 -11
- package/cjs/index.js +238 -34
- package/cjs/index.js.map +1 -1
- package/esm/index.js +238 -34
- package/esm/index.js.map +1 -1
- package/package.json +5 -5
- package/types/coreHandlers/handleDom.d.ts.map +1 -1
- package/types/coreHandlers/handleSlowClick.d.ts +12 -0
- package/types/coreHandlers/handleSlowClick.d.ts.map +1 -0
- package/types/integration.d.ts +1 -1
- package/types/integration.d.ts.map +1 -1
- package/types/replay.d.ts +8 -5
- package/types/replay.d.ts.map +1 -1
- package/types/types.d.ts +31 -1
- package/types/types.d.ts.map +1 -1
- package/types/util/addEvent.d.ts.map +1 -1
- package/types/util/handleRecordingEmit.d.ts.map +1 -1
package/esm/index.js
CHANGED
|
@@ -3285,6 +3285,23 @@ var NodeType;
|
|
|
3285
3285
|
NodeType[NodeType["Comment"] = 5] = "Comment";
|
|
3286
3286
|
})(NodeType || (NodeType = {}));
|
|
3287
3287
|
|
|
3288
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
3289
|
+
|
|
3290
|
+
var EventType; (function (EventType) {
|
|
3291
|
+
const DomContentLoaded = 0; EventType[EventType["DomContentLoaded"] = DomContentLoaded] = "DomContentLoaded";
|
|
3292
|
+
const Load = 1; EventType[EventType["Load"] = Load] = "Load";
|
|
3293
|
+
const FullSnapshot = 2; EventType[EventType["FullSnapshot"] = FullSnapshot] = "FullSnapshot";
|
|
3294
|
+
const IncrementalSnapshot = 3; EventType[EventType["IncrementalSnapshot"] = IncrementalSnapshot] = "IncrementalSnapshot";
|
|
3295
|
+
const Meta = 4; EventType[EventType["Meta"] = Meta] = "Meta";
|
|
3296
|
+
const Custom = 5; EventType[EventType["Custom"] = Custom] = "Custom";
|
|
3297
|
+
const Plugin = 6; EventType[EventType["Plugin"] = Plugin] = "Plugin";
|
|
3298
|
+
})(EventType || (EventType = {}));
|
|
3299
|
+
|
|
3300
|
+
/**
|
|
3301
|
+
* This is a partial copy of rrweb's eventWithTime type which only contains the properties
|
|
3302
|
+
* we specifcally need in the SDK.
|
|
3303
|
+
*/
|
|
3304
|
+
|
|
3288
3305
|
/**
|
|
3289
3306
|
* Converts a timestamp to ms, if it was in s, or keeps it as ms.
|
|
3290
3307
|
*/
|
|
@@ -3327,7 +3344,18 @@ async function addEvent(
|
|
|
3327
3344
|
replay.eventBuffer.clear();
|
|
3328
3345
|
}
|
|
3329
3346
|
|
|
3330
|
-
|
|
3347
|
+
const replayOptions = replay.getOptions();
|
|
3348
|
+
|
|
3349
|
+
const eventAfterPossibleCallback =
|
|
3350
|
+
typeof replayOptions.beforeAddRecordingEvent === 'function' && event.type === EventType.Custom
|
|
3351
|
+
? replayOptions.beforeAddRecordingEvent(event)
|
|
3352
|
+
: event;
|
|
3353
|
+
|
|
3354
|
+
if (!eventAfterPossibleCallback) {
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
return await replay.eventBuffer.addEvent(eventAfterPossibleCallback);
|
|
3331
3359
|
} catch (error) {
|
|
3332
3360
|
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error(error);
|
|
3333
3361
|
await replay.stop('addEvent');
|
|
@@ -3372,6 +3400,141 @@ function addBreadcrumbEvent(replay, breadcrumb) {
|
|
|
3372
3400
|
});
|
|
3373
3401
|
}
|
|
3374
3402
|
|
|
3403
|
+
/**
|
|
3404
|
+
* Detect a slow click on a button/a tag,
|
|
3405
|
+
* and potentially create a corresponding breadcrumb.
|
|
3406
|
+
*/
|
|
3407
|
+
function detectSlowClick(
|
|
3408
|
+
replay,
|
|
3409
|
+
config,
|
|
3410
|
+
clickBreadcrumb,
|
|
3411
|
+
node,
|
|
3412
|
+
) {
|
|
3413
|
+
if (ignoreElement(node, config)) {
|
|
3414
|
+
return;
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
/*
|
|
3418
|
+
We consider a slow click a click on a button/a, which does not trigger one of:
|
|
3419
|
+
- DOM mutation
|
|
3420
|
+
- Scroll (within 100ms)
|
|
3421
|
+
Within the given threshold time.
|
|
3422
|
+
After time timeout time, we stop listening and mark it as a slow click anyhow.
|
|
3423
|
+
*/
|
|
3424
|
+
|
|
3425
|
+
let cleanup = () => {
|
|
3426
|
+
// replaced further down
|
|
3427
|
+
};
|
|
3428
|
+
|
|
3429
|
+
// After timeout time, def. consider this a slow click, and stop watching for mutations
|
|
3430
|
+
const timeout = setTimeout(() => {
|
|
3431
|
+
handleSlowClick(replay, clickBreadcrumb, config.timeout, 'timeout');
|
|
3432
|
+
cleanup();
|
|
3433
|
+
}, config.timeout);
|
|
3434
|
+
|
|
3435
|
+
const mutationHandler = () => {
|
|
3436
|
+
maybeHandleSlowClick(replay, clickBreadcrumb, config.threshold, config.timeout, 'mutation');
|
|
3437
|
+
cleanup();
|
|
3438
|
+
};
|
|
3439
|
+
|
|
3440
|
+
const scrollHandler = () => {
|
|
3441
|
+
maybeHandleSlowClick(replay, clickBreadcrumb, config.scrollTimeout, config.timeout, 'scroll');
|
|
3442
|
+
cleanup();
|
|
3443
|
+
};
|
|
3444
|
+
|
|
3445
|
+
const obs = new MutationObserver(mutationHandler);
|
|
3446
|
+
|
|
3447
|
+
obs.observe(WINDOW.document.documentElement, {
|
|
3448
|
+
attributes: true,
|
|
3449
|
+
characterData: true,
|
|
3450
|
+
childList: true,
|
|
3451
|
+
subtree: true,
|
|
3452
|
+
});
|
|
3453
|
+
|
|
3454
|
+
WINDOW.addEventListener('scroll', scrollHandler);
|
|
3455
|
+
|
|
3456
|
+
// Stop listening to scroll timeouts early
|
|
3457
|
+
const scrollTimeout = setTimeout(() => {
|
|
3458
|
+
WINDOW.removeEventListener('scroll', scrollHandler);
|
|
3459
|
+
}, config.scrollTimeout);
|
|
3460
|
+
|
|
3461
|
+
cleanup = () => {
|
|
3462
|
+
clearTimeout(timeout);
|
|
3463
|
+
clearTimeout(scrollTimeout);
|
|
3464
|
+
obs.disconnect();
|
|
3465
|
+
WINDOW.removeEventListener('scroll', scrollHandler);
|
|
3466
|
+
};
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
function maybeHandleSlowClick(
|
|
3470
|
+
replay,
|
|
3471
|
+
clickBreadcrumb,
|
|
3472
|
+
threshold,
|
|
3473
|
+
timeout,
|
|
3474
|
+
endReason,
|
|
3475
|
+
) {
|
|
3476
|
+
const now = Date.now();
|
|
3477
|
+
const timeAfterClickMs = now - clickBreadcrumb.timestamp * 1000;
|
|
3478
|
+
|
|
3479
|
+
if (timeAfterClickMs > threshold) {
|
|
3480
|
+
handleSlowClick(replay, clickBreadcrumb, Math.min(timeAfterClickMs, timeout), endReason);
|
|
3481
|
+
return true;
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
return false;
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
function handleSlowClick(
|
|
3488
|
+
replay,
|
|
3489
|
+
clickBreadcrumb,
|
|
3490
|
+
timeAfterClickMs,
|
|
3491
|
+
endReason,
|
|
3492
|
+
) {
|
|
3493
|
+
const breadcrumb = {
|
|
3494
|
+
message: clickBreadcrumb.message,
|
|
3495
|
+
timestamp: clickBreadcrumb.timestamp,
|
|
3496
|
+
category: 'ui.slowClickDetected',
|
|
3497
|
+
data: {
|
|
3498
|
+
...clickBreadcrumb.data,
|
|
3499
|
+
url: WINDOW.location.href,
|
|
3500
|
+
// TODO FN: add parametrized route, when possible
|
|
3501
|
+
timeAfterClickMs,
|
|
3502
|
+
endReason,
|
|
3503
|
+
},
|
|
3504
|
+
};
|
|
3505
|
+
|
|
3506
|
+
addBreadcrumbEvent(replay, breadcrumb);
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
const SLOW_CLICK_IGNORE_TAGS = ['SELECT', 'OPTION'];
|
|
3510
|
+
|
|
3511
|
+
function ignoreElement(node, config) {
|
|
3512
|
+
// If <input> tag, we only want to consider input[type='submit'] & input[type='button']
|
|
3513
|
+
if (node.tagName === 'INPUT' && !['submit', 'button'].includes(node.getAttribute('type') || '')) {
|
|
3514
|
+
return true;
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
if (SLOW_CLICK_IGNORE_TAGS.includes(node.tagName)) {
|
|
3518
|
+
return true;
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
// If <a> tag, detect special variants that may not lead to an action
|
|
3522
|
+
// If target !== _self, we may open the link somewhere else, which would lead to no action
|
|
3523
|
+
// Also, when downloading a file, we may not leave the page, but still not trigger an action
|
|
3524
|
+
if (
|
|
3525
|
+
node.tagName === 'A' &&
|
|
3526
|
+
(node.hasAttribute('download') || (node.hasAttribute('target') && node.getAttribute('target') !== '_self'))
|
|
3527
|
+
) {
|
|
3528
|
+
return true;
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
if (config.ignoreSelector && node.matches(config.ignoreSelector)) {
|
|
3532
|
+
return true;
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3535
|
+
return false;
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3375
3538
|
// Note that these are the serialized attributes and not attributes directly on
|
|
3376
3539
|
// the DOM Node. Attributes we are interested in:
|
|
3377
3540
|
const ATTRIBUTES_TO_RECORD = new Set([
|
|
@@ -3406,9 +3569,21 @@ function getAttributesToRecord(attributes) {
|
|
|
3406
3569
|
return obj;
|
|
3407
3570
|
}
|
|
3408
3571
|
|
|
3409
|
-
const handleDomListener =
|
|
3410
|
-
|
|
3411
|
-
|
|
3572
|
+
const handleDomListener = (
|
|
3573
|
+
replay,
|
|
3574
|
+
) => {
|
|
3575
|
+
const slowClickExperiment = replay.getOptions()._experiments.slowClicks;
|
|
3576
|
+
|
|
3577
|
+
const slowClickConfig = slowClickExperiment
|
|
3578
|
+
? {
|
|
3579
|
+
threshold: slowClickExperiment.threshold,
|
|
3580
|
+
timeout: slowClickExperiment.timeout,
|
|
3581
|
+
scrollTimeout: slowClickExperiment.scrollTimeout,
|
|
3582
|
+
ignoreSelector: slowClickExperiment.ignoreSelectors ? slowClickExperiment.ignoreSelectors.join(',') : '',
|
|
3583
|
+
}
|
|
3584
|
+
: undefined;
|
|
3585
|
+
|
|
3586
|
+
return (handlerData) => {
|
|
3412
3587
|
if (!replay.isEnabled()) {
|
|
3413
3588
|
return;
|
|
3414
3589
|
}
|
|
@@ -3419,8 +3594,21 @@ const handleDomListener =
|
|
|
3419
3594
|
return;
|
|
3420
3595
|
}
|
|
3421
3596
|
|
|
3597
|
+
const isClick = handlerData.name === 'click';
|
|
3598
|
+
const event = isClick && (handlerData.event );
|
|
3599
|
+
// Ignore clicks if ctrl/alt/meta keys are held down as they alter behavior of clicks (e.g. open in new tab)
|
|
3600
|
+
if (isClick && slowClickConfig && event && !event.altKey && !event.metaKey && !event.ctrlKey) {
|
|
3601
|
+
detectSlowClick(
|
|
3602
|
+
replay,
|
|
3603
|
+
slowClickConfig,
|
|
3604
|
+
result ,
|
|
3605
|
+
getClickTargetNode(handlerData.event) ,
|
|
3606
|
+
);
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3422
3609
|
addBreadcrumbEvent(replay, result);
|
|
3423
3610
|
};
|
|
3611
|
+
};
|
|
3424
3612
|
|
|
3425
3613
|
/** Get the base DOM breadcrumb. */
|
|
3426
3614
|
function getBaseDomBreadcrumb(target, message) {
|
|
@@ -6143,23 +6331,6 @@ function debounce(func, wait, options) {
|
|
|
6143
6331
|
return debounced;
|
|
6144
6332
|
}
|
|
6145
6333
|
|
|
6146
|
-
/* eslint-disable @typescript-eslint/naming-convention */
|
|
6147
|
-
|
|
6148
|
-
var EventType; (function (EventType) {
|
|
6149
|
-
const DomContentLoaded = 0; EventType[EventType["DomContentLoaded"] = DomContentLoaded] = "DomContentLoaded";
|
|
6150
|
-
const Load = 1; EventType[EventType["Load"] = Load] = "Load";
|
|
6151
|
-
const FullSnapshot = 2; EventType[EventType["FullSnapshot"] = FullSnapshot] = "FullSnapshot";
|
|
6152
|
-
const IncrementalSnapshot = 3; EventType[EventType["IncrementalSnapshot"] = IncrementalSnapshot] = "IncrementalSnapshot";
|
|
6153
|
-
const Meta = 4; EventType[EventType["Meta"] = Meta] = "Meta";
|
|
6154
|
-
const Custom = 5; EventType[EventType["Custom"] = Custom] = "Custom";
|
|
6155
|
-
const Plugin = 6; EventType[EventType["Plugin"] = Plugin] = "Plugin";
|
|
6156
|
-
})(EventType || (EventType = {}));
|
|
6157
|
-
|
|
6158
|
-
/**
|
|
6159
|
-
* This is a partial copy of rrweb's eventWithTime type which only contains the properties
|
|
6160
|
-
* we specifcally need in the SDK.
|
|
6161
|
-
*/
|
|
6162
|
-
|
|
6163
6334
|
/**
|
|
6164
6335
|
* Handler for recording events.
|
|
6165
6336
|
*
|
|
@@ -6233,6 +6404,30 @@ function getHandleRecordingEmit(replay) {
|
|
|
6233
6404
|
}
|
|
6234
6405
|
}
|
|
6235
6406
|
|
|
6407
|
+
const options = replay.getOptions();
|
|
6408
|
+
|
|
6409
|
+
// TODO: We want this as an experiment so that we can test
|
|
6410
|
+
// internally and create metrics before making this the default
|
|
6411
|
+
if (options._experiments.delayFlushOnCheckout) {
|
|
6412
|
+
// If the full snapshot is due to an initial load, we will not have
|
|
6413
|
+
// a previous session ID. In this case, we want to buffer events
|
|
6414
|
+
// for a set amount of time before flushing. This can help avoid
|
|
6415
|
+
// capturing replays of users that immediately close the window.
|
|
6416
|
+
setTimeout(() => replay.conditionalFlush(), options._experiments.delayFlushOnCheckout);
|
|
6417
|
+
|
|
6418
|
+
// Cancel any previously debounced flushes to ensure there are no [near]
|
|
6419
|
+
// simultaneous flushes happening. The latter request should be
|
|
6420
|
+
// insignificant in this case, so wait for additional user interaction to
|
|
6421
|
+
// trigger a new flush.
|
|
6422
|
+
//
|
|
6423
|
+
// This can happen because there's no guarantee that a recording event
|
|
6424
|
+
// happens first. e.g. a mouse click can happen and trigger a debounced
|
|
6425
|
+
// flush before the checkout.
|
|
6426
|
+
replay.cancelFlush();
|
|
6427
|
+
|
|
6428
|
+
return true;
|
|
6429
|
+
}
|
|
6430
|
+
|
|
6236
6431
|
// Flush immediately so that we do not miss the first segment, otherwise
|
|
6237
6432
|
// it can prevent loading on the UI. This will cause an increase in short
|
|
6238
6433
|
// replays (e.g. opening and closing a tab quickly), but these can be
|
|
@@ -6998,7 +7193,17 @@ class ReplayContainer {
|
|
|
6998
7193
|
}
|
|
6999
7194
|
|
|
7000
7195
|
/**
|
|
7001
|
-
*
|
|
7196
|
+
* Only flush if `this.recordingMode === 'session'`
|
|
7197
|
+
*/
|
|
7198
|
+
conditionalFlush() {
|
|
7199
|
+
if (this.recordingMode === 'buffer') {
|
|
7200
|
+
return Promise.resolve();
|
|
7201
|
+
}
|
|
7202
|
+
|
|
7203
|
+
return this.flushImmediate();
|
|
7204
|
+
}
|
|
7205
|
+
|
|
7206
|
+
/**
|
|
7002
7207
|
* Always flush via `_debouncedFlush` so that we do not have flushes triggered
|
|
7003
7208
|
* from calling both `flush` and `_debouncedFlush`. Otherwise, there could be
|
|
7004
7209
|
* cases of mulitple flushes happening closely together.
|
|
@@ -7009,6 +7214,13 @@ class ReplayContainer {
|
|
|
7009
7214
|
return this._debouncedFlush.flush() ;
|
|
7010
7215
|
}
|
|
7011
7216
|
|
|
7217
|
+
/**
|
|
7218
|
+
* Cancels queued up flushes.
|
|
7219
|
+
*/
|
|
7220
|
+
cancelFlush() {
|
|
7221
|
+
this._debouncedFlush.cancel();
|
|
7222
|
+
}
|
|
7223
|
+
|
|
7012
7224
|
/** Get the current sesion (=replay) ID */
|
|
7013
7225
|
getSessionId() {
|
|
7014
7226
|
return this.session && this.session.id;
|
|
@@ -7258,7 +7470,7 @@ class ReplayContainer {
|
|
|
7258
7470
|
// Send replay when the page/tab becomes hidden. There is no reason to send
|
|
7259
7471
|
// replay if it becomes visible, since no actions we care about were done
|
|
7260
7472
|
// while it was hidden
|
|
7261
|
-
this.
|
|
7473
|
+
void this.conditionalFlush();
|
|
7262
7474
|
}
|
|
7263
7475
|
|
|
7264
7476
|
/**
|
|
@@ -7342,17 +7554,6 @@ class ReplayContainer {
|
|
|
7342
7554
|
return Promise.all(createPerformanceSpans(this, createPerformanceEntries(entries)));
|
|
7343
7555
|
}
|
|
7344
7556
|
|
|
7345
|
-
/**
|
|
7346
|
-
* Only flush if `this.recordingMode === 'session'`
|
|
7347
|
-
*/
|
|
7348
|
-
_conditionalFlush() {
|
|
7349
|
-
if (this.recordingMode === 'buffer') {
|
|
7350
|
-
return;
|
|
7351
|
-
}
|
|
7352
|
-
|
|
7353
|
-
void this.flushImmediate();
|
|
7354
|
-
}
|
|
7355
|
-
|
|
7356
7557
|
/**
|
|
7357
7558
|
* Clear _context
|
|
7358
7559
|
*/
|
|
@@ -7714,6 +7915,8 @@ class Replay {
|
|
|
7714
7915
|
ignore = [],
|
|
7715
7916
|
maskFn,
|
|
7716
7917
|
|
|
7918
|
+
beforeAddRecordingEvent,
|
|
7919
|
+
|
|
7717
7920
|
// eslint-disable-next-line deprecation/deprecation
|
|
7718
7921
|
blockClass,
|
|
7719
7922
|
// eslint-disable-next-line deprecation/deprecation
|
|
@@ -7771,6 +7974,7 @@ class Replay {
|
|
|
7771
7974
|
networkCaptureBodies,
|
|
7772
7975
|
networkRequestHeaders: _getMergedNetworkHeaders(networkRequestHeaders),
|
|
7773
7976
|
networkResponseHeaders: _getMergedNetworkHeaders(networkResponseHeaders),
|
|
7977
|
+
beforeAddRecordingEvent,
|
|
7774
7978
|
|
|
7775
7979
|
_experiments,
|
|
7776
7980
|
};
|