@sentry/replay 7.51.1 → 7.52.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 +337 -239
- package/cjs/index.js.map +1 -1
- package/esm/index.js +338 -240
- package/esm/index.js.map +1 -1
- package/package.json +5 -5
- package/types/coreHandlers/handleDom.d.ts +5 -2
- package/types/coreHandlers/handleDom.d.ts.map +1 -1
- package/types/coreHandlers/handleKeyboardEvent.d.ts +7 -0
- package/types/coreHandlers/handleKeyboardEvent.d.ts.map +1 -0
- package/types/coreHandlers/util/networkUtils.d.ts +2 -0
- package/types/coreHandlers/util/networkUtils.d.ts.map +1 -1
- package/types/replay.d.ts.map +1 -1
package/esm/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getCurrentHub, addBreadcrumb, addGlobalEventProcessor, prepareEvent, setContext, captureException } from '@sentry/core';
|
|
2
|
-
import { GLOBAL_OBJ, logger,
|
|
2
|
+
import { GLOBAL_OBJ, logger, normalize, htmlTreeAsString, 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
|
|
@@ -3262,6 +3262,315 @@ record.takeFullSnapshot = (isCheckout) => {
|
|
|
3262
3262
|
};
|
|
3263
3263
|
record.mirror = mirror;
|
|
3264
3264
|
|
|
3265
|
+
/**
|
|
3266
|
+
* Create a breadcrumb for a replay.
|
|
3267
|
+
*/
|
|
3268
|
+
function createBreadcrumb(
|
|
3269
|
+
breadcrumb,
|
|
3270
|
+
) {
|
|
3271
|
+
return {
|
|
3272
|
+
timestamp: Date.now() / 1000,
|
|
3273
|
+
type: 'default',
|
|
3274
|
+
...breadcrumb,
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
var NodeType;
|
|
3279
|
+
(function (NodeType) {
|
|
3280
|
+
NodeType[NodeType["Document"] = 0] = "Document";
|
|
3281
|
+
NodeType[NodeType["DocumentType"] = 1] = "DocumentType";
|
|
3282
|
+
NodeType[NodeType["Element"] = 2] = "Element";
|
|
3283
|
+
NodeType[NodeType["Text"] = 3] = "Text";
|
|
3284
|
+
NodeType[NodeType["CDATA"] = 4] = "CDATA";
|
|
3285
|
+
NodeType[NodeType["Comment"] = 5] = "Comment";
|
|
3286
|
+
})(NodeType || (NodeType = {}));
|
|
3287
|
+
|
|
3288
|
+
/**
|
|
3289
|
+
* Converts a timestamp to ms, if it was in s, or keeps it as ms.
|
|
3290
|
+
*/
|
|
3291
|
+
function timestampToMs(timestamp) {
|
|
3292
|
+
const isMs = timestamp > 9999999999;
|
|
3293
|
+
return isMs ? timestamp : timestamp * 1000;
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
/**
|
|
3297
|
+
* Add an event to the event buffer.
|
|
3298
|
+
* `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`.
|
|
3299
|
+
*/
|
|
3300
|
+
async function addEvent(
|
|
3301
|
+
replay,
|
|
3302
|
+
event,
|
|
3303
|
+
isCheckout,
|
|
3304
|
+
) {
|
|
3305
|
+
if (!replay.eventBuffer) {
|
|
3306
|
+
// This implies that `_isEnabled` is false
|
|
3307
|
+
return null;
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
if (replay.isPaused()) {
|
|
3311
|
+
// Do not add to event buffer when recording is paused
|
|
3312
|
+
return null;
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
const timestampInMs = timestampToMs(event.timestamp);
|
|
3316
|
+
|
|
3317
|
+
// Throw out events that happen more than 5 minutes ago. This can happen if
|
|
3318
|
+
// page has been left open and idle for a long period of time and user
|
|
3319
|
+
// comes back to trigger a new session. The performance entries rely on
|
|
3320
|
+
// `performance.timeOrigin`, which is when the page first opened.
|
|
3321
|
+
if (timestampInMs + replay.timeouts.sessionIdlePause < Date.now()) {
|
|
3322
|
+
return null;
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
try {
|
|
3326
|
+
if (isCheckout) {
|
|
3327
|
+
replay.eventBuffer.clear();
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
return await replay.eventBuffer.addEvent(event);
|
|
3331
|
+
} catch (error) {
|
|
3332
|
+
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error(error);
|
|
3333
|
+
await replay.stop('addEvent');
|
|
3334
|
+
|
|
3335
|
+
const client = getCurrentHub().getClient();
|
|
3336
|
+
|
|
3337
|
+
if (client) {
|
|
3338
|
+
client.recordDroppedEvent('internal_sdk_error', 'replay');
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
/**
|
|
3344
|
+
* Add a breadcrumb event to replay.
|
|
3345
|
+
*/
|
|
3346
|
+
function addBreadcrumbEvent(replay, breadcrumb) {
|
|
3347
|
+
if (breadcrumb.category === 'sentry.transaction') {
|
|
3348
|
+
return;
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
if (['ui.click', 'ui.input'].includes(breadcrumb.category )) {
|
|
3352
|
+
replay.triggerUserActivity();
|
|
3353
|
+
} else {
|
|
3354
|
+
replay.checkAndHandleExpiredSession();
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
replay.addUpdate(() => {
|
|
3358
|
+
void addEvent(replay, {
|
|
3359
|
+
type: EventType$1.Custom,
|
|
3360
|
+
// TODO: We were converting from ms to seconds for breadcrumbs, spans,
|
|
3361
|
+
// but maybe we should just keep them as milliseconds
|
|
3362
|
+
timestamp: (breadcrumb.timestamp || 0) * 1000,
|
|
3363
|
+
data: {
|
|
3364
|
+
tag: 'breadcrumb',
|
|
3365
|
+
// normalize to max. 10 depth and 1_000 properties per object
|
|
3366
|
+
payload: normalize(breadcrumb, 10, 1000),
|
|
3367
|
+
},
|
|
3368
|
+
});
|
|
3369
|
+
|
|
3370
|
+
// Do not flush after console log messages
|
|
3371
|
+
return breadcrumb.category === 'console';
|
|
3372
|
+
});
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
// Note that these are the serialized attributes and not attributes directly on
|
|
3376
|
+
// the DOM Node. Attributes we are interested in:
|
|
3377
|
+
const ATTRIBUTES_TO_RECORD = new Set([
|
|
3378
|
+
'id',
|
|
3379
|
+
'class',
|
|
3380
|
+
'aria-label',
|
|
3381
|
+
'role',
|
|
3382
|
+
'name',
|
|
3383
|
+
'alt',
|
|
3384
|
+
'title',
|
|
3385
|
+
'data-test-id',
|
|
3386
|
+
'data-testid',
|
|
3387
|
+
]);
|
|
3388
|
+
|
|
3389
|
+
/**
|
|
3390
|
+
* Inclusion list of attributes that we want to record from the DOM element
|
|
3391
|
+
*/
|
|
3392
|
+
function getAttributesToRecord(attributes) {
|
|
3393
|
+
const obj = {};
|
|
3394
|
+
for (const key in attributes) {
|
|
3395
|
+
if (ATTRIBUTES_TO_RECORD.has(key)) {
|
|
3396
|
+
let normalizedKey = key;
|
|
3397
|
+
|
|
3398
|
+
if (key === 'data-testid' || key === 'data-test-id') {
|
|
3399
|
+
normalizedKey = 'testId';
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
obj[normalizedKey] = attributes[key];
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
return obj;
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
const handleDomListener =
|
|
3410
|
+
(replay) =>
|
|
3411
|
+
(handlerData) => {
|
|
3412
|
+
if (!replay.isEnabled()) {
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
const result = handleDom(handlerData);
|
|
3417
|
+
|
|
3418
|
+
if (!result) {
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
addBreadcrumbEvent(replay, result);
|
|
3423
|
+
};
|
|
3424
|
+
|
|
3425
|
+
/** Get the base DOM breadcrumb. */
|
|
3426
|
+
function getBaseDomBreadcrumb(target, message) {
|
|
3427
|
+
// `__sn` property is the serialized node created by rrweb
|
|
3428
|
+
const serializedNode = target && isRrwebNode(target) && target.__sn.type === NodeType.Element ? target.__sn : null;
|
|
3429
|
+
|
|
3430
|
+
return {
|
|
3431
|
+
message,
|
|
3432
|
+
data: serializedNode
|
|
3433
|
+
? {
|
|
3434
|
+
nodeId: serializedNode.id,
|
|
3435
|
+
node: {
|
|
3436
|
+
id: serializedNode.id,
|
|
3437
|
+
tagName: serializedNode.tagName,
|
|
3438
|
+
textContent: target
|
|
3439
|
+
? Array.from(target.childNodes)
|
|
3440
|
+
.map(
|
|
3441
|
+
(node) => '__sn' in node && node.__sn.type === NodeType.Text && node.__sn.textContent,
|
|
3442
|
+
)
|
|
3443
|
+
.filter(Boolean) // filter out empty values
|
|
3444
|
+
.map(text => (text ).trim())
|
|
3445
|
+
.join('')
|
|
3446
|
+
: '',
|
|
3447
|
+
attributes: getAttributesToRecord(serializedNode.attributes),
|
|
3448
|
+
},
|
|
3449
|
+
}
|
|
3450
|
+
: {},
|
|
3451
|
+
};
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
/**
|
|
3455
|
+
* An event handler to react to DOM events.
|
|
3456
|
+
* Exported for tests.
|
|
3457
|
+
*/
|
|
3458
|
+
function handleDom(handlerData) {
|
|
3459
|
+
const { target, message } = getDomTarget(handlerData);
|
|
3460
|
+
|
|
3461
|
+
return createBreadcrumb({
|
|
3462
|
+
category: `ui.${handlerData.name}`,
|
|
3463
|
+
...getBaseDomBreadcrumb(target, message),
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
function getDomTarget(handlerData) {
|
|
3468
|
+
const isClick = handlerData.name === 'click';
|
|
3469
|
+
|
|
3470
|
+
let message;
|
|
3471
|
+
let target = null;
|
|
3472
|
+
|
|
3473
|
+
// Accessing event.target can throw (see getsentry/raven-js#838, #768)
|
|
3474
|
+
try {
|
|
3475
|
+
target = isClick ? getClickTargetNode(handlerData.event) : getTargetNode(handlerData.event);
|
|
3476
|
+
message = htmlTreeAsString(target, { maxStringLength: 200 }) || '<unknown>';
|
|
3477
|
+
} catch (e) {
|
|
3478
|
+
message = '<unknown>';
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
return { target, message };
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
function isRrwebNode(node) {
|
|
3485
|
+
return '__sn' in node;
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
function getTargetNode(event) {
|
|
3489
|
+
if (isEventWithTarget(event)) {
|
|
3490
|
+
return event.target ;
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
return event;
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
const INTERACTIVE_SELECTOR = 'button,a';
|
|
3497
|
+
|
|
3498
|
+
// For clicks, we check if the target is inside of a button or link
|
|
3499
|
+
// If so, we use this as the target instead
|
|
3500
|
+
// This is useful because if you click on the image in <button><img></button>,
|
|
3501
|
+
// The target will be the image, not the button, which we don't want here
|
|
3502
|
+
function getClickTargetNode(event) {
|
|
3503
|
+
const target = getTargetNode(event);
|
|
3504
|
+
|
|
3505
|
+
if (!target || !(target instanceof Element)) {
|
|
3506
|
+
return target;
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
const closestInteractive = target.closest(INTERACTIVE_SELECTOR);
|
|
3510
|
+
return closestInteractive || target;
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3513
|
+
function isEventWithTarget(event) {
|
|
3514
|
+
return typeof event === 'object' && !!event && 'target' in event;
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
/** Handle keyboard events & create breadcrumbs. */
|
|
3518
|
+
function handleKeyboardEvent(replay, event) {
|
|
3519
|
+
if (!replay.isEnabled()) {
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
replay.triggerUserActivity();
|
|
3524
|
+
|
|
3525
|
+
const breadcrumb = getKeyboardBreadcrumb(event);
|
|
3526
|
+
|
|
3527
|
+
if (!breadcrumb) {
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
addBreadcrumbEvent(replay, breadcrumb);
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
/** exported only for tests */
|
|
3535
|
+
function getKeyboardBreadcrumb(event) {
|
|
3536
|
+
const { metaKey, shiftKey, ctrlKey, altKey, key, target } = event;
|
|
3537
|
+
|
|
3538
|
+
// never capture for input fields
|
|
3539
|
+
if (!target || isInputElement(target )) {
|
|
3540
|
+
return null;
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
// Note: We do not consider shift here, as that means "uppercase"
|
|
3544
|
+
const hasModifierKey = metaKey || ctrlKey || altKey;
|
|
3545
|
+
const isCharacterKey = key.length === 1; // other keys like Escape, Tab, etc have a longer length
|
|
3546
|
+
|
|
3547
|
+
// Do not capture breadcrumb if only a word key is pressed
|
|
3548
|
+
// This could leak e.g. user input
|
|
3549
|
+
if (!hasModifierKey && isCharacterKey) {
|
|
3550
|
+
return null;
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3553
|
+
const message = htmlTreeAsString(target, { maxStringLength: 200 }) || '<unknown>';
|
|
3554
|
+
const baseBreadcrumb = getBaseDomBreadcrumb(target , message);
|
|
3555
|
+
|
|
3556
|
+
return createBreadcrumb({
|
|
3557
|
+
category: 'ui.keyDown',
|
|
3558
|
+
message,
|
|
3559
|
+
data: {
|
|
3560
|
+
...baseBreadcrumb.data,
|
|
3561
|
+
metaKey,
|
|
3562
|
+
shiftKey,
|
|
3563
|
+
ctrlKey,
|
|
3564
|
+
altKey,
|
|
3565
|
+
key,
|
|
3566
|
+
},
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
function isInputElement(target) {
|
|
3571
|
+
return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3265
3574
|
const NAVIGATION_ENTRY_KEYS = [
|
|
3266
3575
|
'name',
|
|
3267
3576
|
'type',
|
|
@@ -3399,14 +3708,6 @@ function t(t){let e=t.length;for(;--e>=0;)t[e]=0}const e=new Uint8Array([0,0,0,0
|
|
|
3399
3708
|
|
|
3400
3709
|
function e(){const e=new Blob([r]);return URL.createObjectURL(e)}
|
|
3401
3710
|
|
|
3402
|
-
/**
|
|
3403
|
-
* Converts a timestamp to ms, if it was in s, or keeps it as ms.
|
|
3404
|
-
*/
|
|
3405
|
-
function timestampToMs(timestamp) {
|
|
3406
|
-
const isMs = timestamp > 9999999999;
|
|
3407
|
-
return isMs ? timestamp : timestamp * 1000;
|
|
3408
|
-
}
|
|
3409
|
-
|
|
3410
3711
|
/**
|
|
3411
3712
|
* A basic event buffer that does not do any compression.
|
|
3412
3713
|
* Used as fallback if the compression worker cannot be loaded or is disabled.
|
|
@@ -3984,53 +4285,6 @@ function getSession({
|
|
|
3984
4285
|
return { type: 'new', session: newSession };
|
|
3985
4286
|
}
|
|
3986
4287
|
|
|
3987
|
-
/**
|
|
3988
|
-
* Add an event to the event buffer.
|
|
3989
|
-
* `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`.
|
|
3990
|
-
*/
|
|
3991
|
-
async function addEvent(
|
|
3992
|
-
replay,
|
|
3993
|
-
event,
|
|
3994
|
-
isCheckout,
|
|
3995
|
-
) {
|
|
3996
|
-
if (!replay.eventBuffer) {
|
|
3997
|
-
// This implies that `_isEnabled` is false
|
|
3998
|
-
return null;
|
|
3999
|
-
}
|
|
4000
|
-
|
|
4001
|
-
if (replay.isPaused()) {
|
|
4002
|
-
// Do not add to event buffer when recording is paused
|
|
4003
|
-
return null;
|
|
4004
|
-
}
|
|
4005
|
-
|
|
4006
|
-
const timestampInMs = timestampToMs(event.timestamp);
|
|
4007
|
-
|
|
4008
|
-
// Throw out events that happen more than 5 minutes ago. This can happen if
|
|
4009
|
-
// page has been left open and idle for a long period of time and user
|
|
4010
|
-
// comes back to trigger a new session. The performance entries rely on
|
|
4011
|
-
// `performance.timeOrigin`, which is when the page first opened.
|
|
4012
|
-
if (timestampInMs + replay.timeouts.sessionIdlePause < Date.now()) {
|
|
4013
|
-
return null;
|
|
4014
|
-
}
|
|
4015
|
-
|
|
4016
|
-
try {
|
|
4017
|
-
if (isCheckout) {
|
|
4018
|
-
replay.eventBuffer.clear();
|
|
4019
|
-
}
|
|
4020
|
-
|
|
4021
|
-
return await replay.eventBuffer.addEvent(event);
|
|
4022
|
-
} catch (error) {
|
|
4023
|
-
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error(error);
|
|
4024
|
-
await replay.stop('addEvent');
|
|
4025
|
-
|
|
4026
|
-
const client = getCurrentHub().getClient();
|
|
4027
|
-
|
|
4028
|
-
if (client) {
|
|
4029
|
-
client.recordDroppedEvent('internal_sdk_error', 'replay');
|
|
4030
|
-
}
|
|
4031
|
-
}
|
|
4032
|
-
}
|
|
4033
|
-
|
|
4034
4288
|
/** If the event is an error event */
|
|
4035
4289
|
function isErrorEvent(event) {
|
|
4036
4290
|
return !event.type;
|
|
@@ -4116,187 +4370,6 @@ function isBaseTransportSend() {
|
|
|
4116
4370
|
);
|
|
4117
4371
|
}
|
|
4118
4372
|
|
|
4119
|
-
var NodeType;
|
|
4120
|
-
(function (NodeType) {
|
|
4121
|
-
NodeType[NodeType["Document"] = 0] = "Document";
|
|
4122
|
-
NodeType[NodeType["DocumentType"] = 1] = "DocumentType";
|
|
4123
|
-
NodeType[NodeType["Element"] = 2] = "Element";
|
|
4124
|
-
NodeType[NodeType["Text"] = 3] = "Text";
|
|
4125
|
-
NodeType[NodeType["CDATA"] = 4] = "CDATA";
|
|
4126
|
-
NodeType[NodeType["Comment"] = 5] = "Comment";
|
|
4127
|
-
})(NodeType || (NodeType = {}));
|
|
4128
|
-
|
|
4129
|
-
/**
|
|
4130
|
-
* Create a breadcrumb for a replay.
|
|
4131
|
-
*/
|
|
4132
|
-
function createBreadcrumb(
|
|
4133
|
-
breadcrumb,
|
|
4134
|
-
) {
|
|
4135
|
-
return {
|
|
4136
|
-
timestamp: Date.now() / 1000,
|
|
4137
|
-
type: 'default',
|
|
4138
|
-
...breadcrumb,
|
|
4139
|
-
};
|
|
4140
|
-
}
|
|
4141
|
-
|
|
4142
|
-
/**
|
|
4143
|
-
* Add a breadcrumb event to replay.
|
|
4144
|
-
*/
|
|
4145
|
-
function addBreadcrumbEvent(replay, breadcrumb) {
|
|
4146
|
-
if (breadcrumb.category === 'sentry.transaction') {
|
|
4147
|
-
return;
|
|
4148
|
-
}
|
|
4149
|
-
|
|
4150
|
-
if (['ui.click', 'ui.input'].includes(breadcrumb.category )) {
|
|
4151
|
-
replay.triggerUserActivity();
|
|
4152
|
-
} else {
|
|
4153
|
-
replay.checkAndHandleExpiredSession();
|
|
4154
|
-
}
|
|
4155
|
-
|
|
4156
|
-
replay.addUpdate(() => {
|
|
4157
|
-
void addEvent(replay, {
|
|
4158
|
-
type: EventType$1.Custom,
|
|
4159
|
-
// TODO: We were converting from ms to seconds for breadcrumbs, spans,
|
|
4160
|
-
// but maybe we should just keep them as milliseconds
|
|
4161
|
-
timestamp: (breadcrumb.timestamp || 0) * 1000,
|
|
4162
|
-
data: {
|
|
4163
|
-
tag: 'breadcrumb',
|
|
4164
|
-
// normalize to max. 10 depth and 1_000 properties per object
|
|
4165
|
-
payload: normalize(breadcrumb, 10, 1000),
|
|
4166
|
-
},
|
|
4167
|
-
});
|
|
4168
|
-
|
|
4169
|
-
// Do not flush after console log messages
|
|
4170
|
-
return breadcrumb.category === 'console';
|
|
4171
|
-
});
|
|
4172
|
-
}
|
|
4173
|
-
|
|
4174
|
-
// Note that these are the serialized attributes and not attributes directly on
|
|
4175
|
-
// the DOM Node. Attributes we are interested in:
|
|
4176
|
-
const ATTRIBUTES_TO_RECORD = new Set([
|
|
4177
|
-
'id',
|
|
4178
|
-
'class',
|
|
4179
|
-
'aria-label',
|
|
4180
|
-
'role',
|
|
4181
|
-
'name',
|
|
4182
|
-
'alt',
|
|
4183
|
-
'title',
|
|
4184
|
-
'data-test-id',
|
|
4185
|
-
'data-testid',
|
|
4186
|
-
]);
|
|
4187
|
-
|
|
4188
|
-
/**
|
|
4189
|
-
* Inclusion list of attributes that we want to record from the DOM element
|
|
4190
|
-
*/
|
|
4191
|
-
function getAttributesToRecord(attributes) {
|
|
4192
|
-
const obj = {};
|
|
4193
|
-
for (const key in attributes) {
|
|
4194
|
-
if (ATTRIBUTES_TO_RECORD.has(key)) {
|
|
4195
|
-
let normalizedKey = key;
|
|
4196
|
-
|
|
4197
|
-
if (key === 'data-testid' || key === 'data-test-id') {
|
|
4198
|
-
normalizedKey = 'testId';
|
|
4199
|
-
}
|
|
4200
|
-
|
|
4201
|
-
obj[normalizedKey] = attributes[key];
|
|
4202
|
-
}
|
|
4203
|
-
}
|
|
4204
|
-
|
|
4205
|
-
return obj;
|
|
4206
|
-
}
|
|
4207
|
-
|
|
4208
|
-
const handleDomListener =
|
|
4209
|
-
(replay) =>
|
|
4210
|
-
(handlerData) => {
|
|
4211
|
-
if (!replay.isEnabled()) {
|
|
4212
|
-
return;
|
|
4213
|
-
}
|
|
4214
|
-
|
|
4215
|
-
const result = handleDom(handlerData);
|
|
4216
|
-
|
|
4217
|
-
if (!result) {
|
|
4218
|
-
return;
|
|
4219
|
-
}
|
|
4220
|
-
|
|
4221
|
-
addBreadcrumbEvent(replay, result);
|
|
4222
|
-
};
|
|
4223
|
-
|
|
4224
|
-
/**
|
|
4225
|
-
* An event handler to react to DOM events.
|
|
4226
|
-
* Exported for tests only.
|
|
4227
|
-
*/
|
|
4228
|
-
function handleDom(handlerData) {
|
|
4229
|
-
let target;
|
|
4230
|
-
let targetNode;
|
|
4231
|
-
|
|
4232
|
-
const isClick = handlerData.name === 'click';
|
|
4233
|
-
|
|
4234
|
-
// Accessing event.target can throw (see getsentry/raven-js#838, #768)
|
|
4235
|
-
try {
|
|
4236
|
-
targetNode = isClick ? getClickTargetNode(handlerData.event) : getTargetNode(handlerData.event);
|
|
4237
|
-
target = htmlTreeAsString(targetNode, { maxStringLength: 200 });
|
|
4238
|
-
} catch (e) {
|
|
4239
|
-
target = '<unknown>';
|
|
4240
|
-
}
|
|
4241
|
-
|
|
4242
|
-
// `__sn` property is the serialized node created by rrweb
|
|
4243
|
-
const serializedNode =
|
|
4244
|
-
targetNode && '__sn' in targetNode && targetNode.__sn.type === NodeType.Element ? targetNode.__sn : null;
|
|
4245
|
-
|
|
4246
|
-
return createBreadcrumb({
|
|
4247
|
-
category: `ui.${handlerData.name}`,
|
|
4248
|
-
message: target,
|
|
4249
|
-
data: serializedNode
|
|
4250
|
-
? {
|
|
4251
|
-
nodeId: serializedNode.id,
|
|
4252
|
-
node: {
|
|
4253
|
-
id: serializedNode.id,
|
|
4254
|
-
tagName: serializedNode.tagName,
|
|
4255
|
-
textContent: targetNode
|
|
4256
|
-
? Array.from(targetNode.childNodes)
|
|
4257
|
-
.map(
|
|
4258
|
-
(node) => '__sn' in node && node.__sn.type === NodeType.Text && node.__sn.textContent,
|
|
4259
|
-
)
|
|
4260
|
-
.filter(Boolean) // filter out empty values
|
|
4261
|
-
.map(text => (text ).trim())
|
|
4262
|
-
.join('')
|
|
4263
|
-
: '',
|
|
4264
|
-
attributes: getAttributesToRecord(serializedNode.attributes),
|
|
4265
|
-
},
|
|
4266
|
-
}
|
|
4267
|
-
: {},
|
|
4268
|
-
});
|
|
4269
|
-
}
|
|
4270
|
-
|
|
4271
|
-
function getTargetNode(event) {
|
|
4272
|
-
if (isEventWithTarget(event)) {
|
|
4273
|
-
return event.target;
|
|
4274
|
-
}
|
|
4275
|
-
|
|
4276
|
-
return event;
|
|
4277
|
-
}
|
|
4278
|
-
|
|
4279
|
-
const INTERACTIVE_SELECTOR = 'button,a';
|
|
4280
|
-
|
|
4281
|
-
// For clicks, we check if the target is inside of a button or link
|
|
4282
|
-
// If so, we use this as the target instead
|
|
4283
|
-
// This is useful because if you click on the image in <button><img></button>,
|
|
4284
|
-
// The target will be the image, not the button, which we don't want here
|
|
4285
|
-
function getClickTargetNode(event) {
|
|
4286
|
-
const target = getTargetNode(event);
|
|
4287
|
-
|
|
4288
|
-
if (!target || !(target instanceof Element)) {
|
|
4289
|
-
return target;
|
|
4290
|
-
}
|
|
4291
|
-
|
|
4292
|
-
const closestInteractive = target.closest(INTERACTIVE_SELECTOR);
|
|
4293
|
-
return closestInteractive || target;
|
|
4294
|
-
}
|
|
4295
|
-
|
|
4296
|
-
function isEventWithTarget(event) {
|
|
4297
|
-
return !!(event ).target;
|
|
4298
|
-
}
|
|
4299
|
-
|
|
4300
4373
|
/**
|
|
4301
4374
|
* Returns true if we think the given event is an error originating inside of rrweb.
|
|
4302
4375
|
*/
|
|
@@ -5214,7 +5287,32 @@ function _strIsProbablyJson(str) {
|
|
|
5214
5287
|
|
|
5215
5288
|
/** Match an URL against a list of strings/Regex. */
|
|
5216
5289
|
function urlMatches(url, urls) {
|
|
5217
|
-
|
|
5290
|
+
const fullUrl = getFullUrl(url);
|
|
5291
|
+
|
|
5292
|
+
return stringMatchesSomePattern(fullUrl, urls);
|
|
5293
|
+
}
|
|
5294
|
+
|
|
5295
|
+
/** exported for tests */
|
|
5296
|
+
function getFullUrl(url, baseURI = WINDOW.document.baseURI) {
|
|
5297
|
+
// Short circuit for common cases:
|
|
5298
|
+
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith(WINDOW.location.origin)) {
|
|
5299
|
+
return url;
|
|
5300
|
+
}
|
|
5301
|
+
const fixedUrl = new URL(url, baseURI);
|
|
5302
|
+
|
|
5303
|
+
// If these do not match, we are not dealing with a relative URL, so just return it
|
|
5304
|
+
if (fixedUrl.origin !== new URL(baseURI).origin) {
|
|
5305
|
+
return url;
|
|
5306
|
+
}
|
|
5307
|
+
|
|
5308
|
+
const fullUrl = fixedUrl.href;
|
|
5309
|
+
|
|
5310
|
+
// Remove trailing slashes, if they don't match the original URL
|
|
5311
|
+
if (!url.endsWith('/') && fullUrl.endsWith('/')) {
|
|
5312
|
+
return fullUrl.slice(0, -1);
|
|
5313
|
+
}
|
|
5314
|
+
|
|
5315
|
+
return fullUrl;
|
|
5218
5316
|
}
|
|
5219
5317
|
|
|
5220
5318
|
/**
|
|
@@ -7139,8 +7237,8 @@ class ReplayContainer {
|
|
|
7139
7237
|
};}
|
|
7140
7238
|
|
|
7141
7239
|
/** Ensure page remains active when a key is pressed. */
|
|
7142
|
-
__init16() {this._handleKeyboardEvent = () => {
|
|
7143
|
-
this
|
|
7240
|
+
__init16() {this._handleKeyboardEvent = (event) => {
|
|
7241
|
+
handleKeyboardEvent(this, event);
|
|
7144
7242
|
};}
|
|
7145
7243
|
|
|
7146
7244
|
/**
|