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