@sentry/replay 7.51.2 → 7.52.1

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/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, uuid4, normalize, htmlTreeAsString, SENTRY_XHR_DATA_KEY, dropUndefinedKeys, stringMatchesSomePattern, addInstrumentationHandler, browserPerformanceTimeOrigin, createEnvelope, createEventEnvelopeHeaders, getSdkMetadataForEnvelopeHeader, isNodeEnv } from '@sentry/utils';
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,475 @@ 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
+ /**
3376
+ * Detect a slow click on a button/a tag,
3377
+ * and potentially create a corresponding breadcrumb.
3378
+ */
3379
+ function detectSlowClick(
3380
+ replay,
3381
+ config,
3382
+ clickBreadcrumb,
3383
+ node,
3384
+ ) {
3385
+ if (ignoreElement(node, config)) {
3386
+ return;
3387
+ }
3388
+
3389
+ /*
3390
+ We consider a slow click a click on a button/a, which does not trigger one of:
3391
+ - DOM mutation
3392
+ - Scroll (within 100ms)
3393
+ Within the given threshold time.
3394
+ After time timeout time, we stop listening and mark it as a slow click anyhow.
3395
+ */
3396
+
3397
+ let cleanup = () => {
3398
+ // replaced further down
3399
+ };
3400
+
3401
+ // After timeout time, def. consider this a slow click, and stop watching for mutations
3402
+ const timeout = setTimeout(() => {
3403
+ handleSlowClick(replay, clickBreadcrumb, config.timeout, 'timeout');
3404
+ cleanup();
3405
+ }, config.timeout);
3406
+
3407
+ const mutationHandler = () => {
3408
+ maybeHandleSlowClick(replay, clickBreadcrumb, config.threshold, config.timeout, 'mutation');
3409
+ cleanup();
3410
+ };
3411
+
3412
+ const scrollHandler = () => {
3413
+ maybeHandleSlowClick(replay, clickBreadcrumb, config.scrollTimeout, config.timeout, 'scroll');
3414
+ cleanup();
3415
+ };
3416
+
3417
+ const obs = new MutationObserver(mutationHandler);
3418
+
3419
+ obs.observe(WINDOW.document.documentElement, {
3420
+ attributes: true,
3421
+ characterData: true,
3422
+ childList: true,
3423
+ subtree: true,
3424
+ });
3425
+
3426
+ WINDOW.addEventListener('scroll', scrollHandler);
3427
+
3428
+ // Stop listening to scroll timeouts early
3429
+ const scrollTimeout = setTimeout(() => {
3430
+ WINDOW.removeEventListener('scroll', scrollHandler);
3431
+ }, config.scrollTimeout);
3432
+
3433
+ cleanup = () => {
3434
+ clearTimeout(timeout);
3435
+ clearTimeout(scrollTimeout);
3436
+ obs.disconnect();
3437
+ WINDOW.removeEventListener('scroll', scrollHandler);
3438
+ };
3439
+ }
3440
+
3441
+ function maybeHandleSlowClick(
3442
+ replay,
3443
+ clickBreadcrumb,
3444
+ threshold,
3445
+ timeout,
3446
+ endReason,
3447
+ ) {
3448
+ const now = Date.now();
3449
+ const timeAfterClickMs = now - clickBreadcrumb.timestamp * 1000;
3450
+
3451
+ if (timeAfterClickMs > threshold) {
3452
+ handleSlowClick(replay, clickBreadcrumb, Math.min(timeAfterClickMs, timeout), endReason);
3453
+ return true;
3454
+ }
3455
+
3456
+ return false;
3457
+ }
3458
+
3459
+ function handleSlowClick(
3460
+ replay,
3461
+ clickBreadcrumb,
3462
+ timeAfterClickMs,
3463
+ endReason,
3464
+ ) {
3465
+ const breadcrumb = {
3466
+ message: clickBreadcrumb.message,
3467
+ timestamp: clickBreadcrumb.timestamp,
3468
+ category: 'ui.slowClickDetected',
3469
+ data: {
3470
+ ...clickBreadcrumb.data,
3471
+ url: WINDOW.location.href,
3472
+ // TODO FN: add parametrized route, when possible
3473
+ timeAfterClickMs,
3474
+ endReason,
3475
+ },
3476
+ };
3477
+
3478
+ addBreadcrumbEvent(replay, breadcrumb);
3479
+ }
3480
+
3481
+ const SLOW_CLICK_IGNORE_TAGS = ['SELECT', 'OPTION'];
3482
+
3483
+ function ignoreElement(node, config) {
3484
+ // If <input> tag, we only want to consider input[type='submit'] & input[type='button']
3485
+ if (node.tagName === 'INPUT' && !['submit', 'button'].includes(node.getAttribute('type') || '')) {
3486
+ return true;
3487
+ }
3488
+
3489
+ if (SLOW_CLICK_IGNORE_TAGS.includes(node.tagName)) {
3490
+ return true;
3491
+ }
3492
+
3493
+ // If <a> tag, detect special variants that may not lead to an action
3494
+ // If target !== _self, we may open the link somewhere else, which would lead to no action
3495
+ // Also, when downloading a file, we may not leave the page, but still not trigger an action
3496
+ if (
3497
+ node.tagName === 'A' &&
3498
+ (node.hasAttribute('download') || (node.hasAttribute('target') && node.getAttribute('target') !== '_self'))
3499
+ ) {
3500
+ return true;
3501
+ }
3502
+
3503
+ if (config.ignoreSelector && node.matches(config.ignoreSelector)) {
3504
+ return true;
3505
+ }
3506
+
3507
+ return false;
3508
+ }
3509
+
3510
+ // Note that these are the serialized attributes and not attributes directly on
3511
+ // the DOM Node. Attributes we are interested in:
3512
+ const ATTRIBUTES_TO_RECORD = new Set([
3513
+ 'id',
3514
+ 'class',
3515
+ 'aria-label',
3516
+ 'role',
3517
+ 'name',
3518
+ 'alt',
3519
+ 'title',
3520
+ 'data-test-id',
3521
+ 'data-testid',
3522
+ ]);
3523
+
3524
+ /**
3525
+ * Inclusion list of attributes that we want to record from the DOM element
3526
+ */
3527
+ function getAttributesToRecord(attributes) {
3528
+ const obj = {};
3529
+ for (const key in attributes) {
3530
+ if (ATTRIBUTES_TO_RECORD.has(key)) {
3531
+ let normalizedKey = key;
3532
+
3533
+ if (key === 'data-testid' || key === 'data-test-id') {
3534
+ normalizedKey = 'testId';
3535
+ }
3536
+
3537
+ obj[normalizedKey] = attributes[key];
3538
+ }
3539
+ }
3540
+
3541
+ return obj;
3542
+ }
3543
+
3544
+ const handleDomListener = (
3545
+ replay,
3546
+ ) => {
3547
+ const slowClickExperiment = replay.getOptions()._experiments.slowClicks;
3548
+
3549
+ const slowClickConfig = slowClickExperiment
3550
+ ? {
3551
+ threshold: slowClickExperiment.threshold,
3552
+ timeout: slowClickExperiment.timeout,
3553
+ scrollTimeout: slowClickExperiment.scrollTimeout,
3554
+ ignoreSelector: slowClickExperiment.ignoreSelectors ? slowClickExperiment.ignoreSelectors.join(',') : '',
3555
+ }
3556
+ : undefined;
3557
+
3558
+ return (handlerData) => {
3559
+ if (!replay.isEnabled()) {
3560
+ return;
3561
+ }
3562
+
3563
+ const result = handleDom(handlerData);
3564
+
3565
+ if (!result) {
3566
+ return;
3567
+ }
3568
+
3569
+ const isClick = handlerData.name === 'click';
3570
+ const event = isClick && (handlerData.event );
3571
+ // Ignore clicks if ctrl/alt/meta keys are held down as they alter behavior of clicks (e.g. open in new tab)
3572
+ if (isClick && slowClickConfig && event && !event.altKey && !event.metaKey && !event.ctrlKey) {
3573
+ detectSlowClick(
3574
+ replay,
3575
+ slowClickConfig,
3576
+ result ,
3577
+ getClickTargetNode(handlerData.event) ,
3578
+ );
3579
+ }
3580
+
3581
+ addBreadcrumbEvent(replay, result);
3582
+ };
3583
+ };
3584
+
3585
+ /** Get the base DOM breadcrumb. */
3586
+ function getBaseDomBreadcrumb(target, message) {
3587
+ // `__sn` property is the serialized node created by rrweb
3588
+ const serializedNode = target && isRrwebNode(target) && target.__sn.type === NodeType.Element ? target.__sn : null;
3589
+
3590
+ return {
3591
+ message,
3592
+ data: serializedNode
3593
+ ? {
3594
+ nodeId: serializedNode.id,
3595
+ node: {
3596
+ id: serializedNode.id,
3597
+ tagName: serializedNode.tagName,
3598
+ textContent: target
3599
+ ? Array.from(target.childNodes)
3600
+ .map(
3601
+ (node) => '__sn' in node && node.__sn.type === NodeType.Text && node.__sn.textContent,
3602
+ )
3603
+ .filter(Boolean) // filter out empty values
3604
+ .map(text => (text ).trim())
3605
+ .join('')
3606
+ : '',
3607
+ attributes: getAttributesToRecord(serializedNode.attributes),
3608
+ },
3609
+ }
3610
+ : {},
3611
+ };
3612
+ }
3613
+
3614
+ /**
3615
+ * An event handler to react to DOM events.
3616
+ * Exported for tests.
3617
+ */
3618
+ function handleDom(handlerData) {
3619
+ const { target, message } = getDomTarget(handlerData);
3620
+
3621
+ return createBreadcrumb({
3622
+ category: `ui.${handlerData.name}`,
3623
+ ...getBaseDomBreadcrumb(target, message),
3624
+ });
3625
+ }
3626
+
3627
+ function getDomTarget(handlerData) {
3628
+ const isClick = handlerData.name === 'click';
3629
+
3630
+ let message;
3631
+ let target = null;
3632
+
3633
+ // Accessing event.target can throw (see getsentry/raven-js#838, #768)
3634
+ try {
3635
+ target = isClick ? getClickTargetNode(handlerData.event) : getTargetNode(handlerData.event);
3636
+ message = htmlTreeAsString(target, { maxStringLength: 200 }) || '<unknown>';
3637
+ } catch (e) {
3638
+ message = '<unknown>';
3639
+ }
3640
+
3641
+ return { target, message };
3642
+ }
3643
+
3644
+ function isRrwebNode(node) {
3645
+ return '__sn' in node;
3646
+ }
3647
+
3648
+ function getTargetNode(event) {
3649
+ if (isEventWithTarget(event)) {
3650
+ return event.target ;
3651
+ }
3652
+
3653
+ return event;
3654
+ }
3655
+
3656
+ const INTERACTIVE_SELECTOR = 'button,a';
3657
+
3658
+ // For clicks, we check if the target is inside of a button or link
3659
+ // If so, we use this as the target instead
3660
+ // This is useful because if you click on the image in <button><img></button>,
3661
+ // The target will be the image, not the button, which we don't want here
3662
+ function getClickTargetNode(event) {
3663
+ const target = getTargetNode(event);
3664
+
3665
+ if (!target || !(target instanceof Element)) {
3666
+ return target;
3667
+ }
3668
+
3669
+ const closestInteractive = target.closest(INTERACTIVE_SELECTOR);
3670
+ return closestInteractive || target;
3671
+ }
3672
+
3673
+ function isEventWithTarget(event) {
3674
+ return typeof event === 'object' && !!event && 'target' in event;
3675
+ }
3676
+
3677
+ /** Handle keyboard events & create breadcrumbs. */
3678
+ function handleKeyboardEvent(replay, event) {
3679
+ if (!replay.isEnabled()) {
3680
+ return;
3681
+ }
3682
+
3683
+ replay.triggerUserActivity();
3684
+
3685
+ const breadcrumb = getKeyboardBreadcrumb(event);
3686
+
3687
+ if (!breadcrumb) {
3688
+ return;
3689
+ }
3690
+
3691
+ addBreadcrumbEvent(replay, breadcrumb);
3692
+ }
3693
+
3694
+ /** exported only for tests */
3695
+ function getKeyboardBreadcrumb(event) {
3696
+ const { metaKey, shiftKey, ctrlKey, altKey, key, target } = event;
3697
+
3698
+ // never capture for input fields
3699
+ if (!target || isInputElement(target )) {
3700
+ return null;
3701
+ }
3702
+
3703
+ // Note: We do not consider shift here, as that means "uppercase"
3704
+ const hasModifierKey = metaKey || ctrlKey || altKey;
3705
+ const isCharacterKey = key.length === 1; // other keys like Escape, Tab, etc have a longer length
3706
+
3707
+ // Do not capture breadcrumb if only a word key is pressed
3708
+ // This could leak e.g. user input
3709
+ if (!hasModifierKey && isCharacterKey) {
3710
+ return null;
3711
+ }
3712
+
3713
+ const message = htmlTreeAsString(target, { maxStringLength: 200 }) || '<unknown>';
3714
+ const baseBreadcrumb = getBaseDomBreadcrumb(target , message);
3715
+
3716
+ return createBreadcrumb({
3717
+ category: 'ui.keyDown',
3718
+ message,
3719
+ data: {
3720
+ ...baseBreadcrumb.data,
3721
+ metaKey,
3722
+ shiftKey,
3723
+ ctrlKey,
3724
+ altKey,
3725
+ key,
3726
+ },
3727
+ });
3728
+ }
3729
+
3730
+ function isInputElement(target) {
3731
+ return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
3732
+ }
3733
+
3265
3734
  const NAVIGATION_ENTRY_KEYS = [
3266
3735
  'name',
3267
3736
  'type',
@@ -3399,14 +3868,6 @@ function t(t){let e=t.length;for(;--e>=0;)t[e]=0}const e=new Uint8Array([0,0,0,0
3399
3868
 
3400
3869
  function e(){const e=new Blob([r]);return URL.createObjectURL(e)}
3401
3870
 
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
3871
  /**
3411
3872
  * A basic event buffer that does not do any compression.
3412
3873
  * Used as fallback if the compression worker cannot be loaded or is disabled.
@@ -3984,53 +4445,6 @@ function getSession({
3984
4445
  return { type: 'new', session: newSession };
3985
4446
  }
3986
4447
 
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
4448
  /** If the event is an error event */
4035
4449
  function isErrorEvent(event) {
4036
4450
  return !event.type;
@@ -4116,187 +4530,6 @@ function isBaseTransportSend() {
4116
4530
  );
4117
4531
  }
4118
4532
 
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
4533
  /**
4301
4534
  * Returns true if we think the given event is an error originating inside of rrweb.
4302
4535
  */
@@ -5214,7 +5447,32 @@ function _strIsProbablyJson(str) {
5214
5447
 
5215
5448
  /** Match an URL against a list of strings/Regex. */
5216
5449
  function urlMatches(url, urls) {
5217
- return stringMatchesSomePattern(url, urls);
5450
+ const fullUrl = getFullUrl(url);
5451
+
5452
+ return stringMatchesSomePattern(fullUrl, urls);
5453
+ }
5454
+
5455
+ /** exported for tests */
5456
+ function getFullUrl(url, baseURI = WINDOW.document.baseURI) {
5457
+ // Short circuit for common cases:
5458
+ if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith(WINDOW.location.origin)) {
5459
+ return url;
5460
+ }
5461
+ const fixedUrl = new URL(url, baseURI);
5462
+
5463
+ // If these do not match, we are not dealing with a relative URL, so just return it
5464
+ if (fixedUrl.origin !== new URL(baseURI).origin) {
5465
+ return url;
5466
+ }
5467
+
5468
+ const fullUrl = fixedUrl.href;
5469
+
5470
+ // Remove trailing slashes, if they don't match the original URL
5471
+ if (!url.endsWith('/') && fullUrl.endsWith('/')) {
5472
+ return fullUrl.slice(0, -1);
5473
+ }
5474
+
5475
+ return fullUrl;
5218
5476
  }
5219
5477
 
5220
5478
  /**
@@ -7139,8 +7397,8 @@ class ReplayContainer {
7139
7397
  };}
7140
7398
 
7141
7399
  /** Ensure page remains active when a key is pressed. */
7142
- __init16() {this._handleKeyboardEvent = () => {
7143
- this.triggerUserActivity();
7400
+ __init16() {this._handleKeyboardEvent = (event) => {
7401
+ handleKeyboardEvent(this, event);
7144
7402
  };}
7145
7403
 
7146
7404
  /**