@sentry/replay 7.53.1 → 7.55.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.
Files changed (46) hide show
  1. package/cjs/index.js +344 -196
  2. package/cjs/index.js.map +1 -1
  3. package/esm/index.js +324 -176
  4. package/esm/index.js.map +1 -1
  5. package/package.json +5 -5
  6. package/types/constants.d.ts +2 -0
  7. package/types/constants.d.ts.map +1 -1
  8. package/types/coreHandlers/handleDom.d.ts.map +1 -1
  9. package/types/coreHandlers/handleGlobalEvent.d.ts.map +1 -1
  10. package/types/coreHandlers/handleKeyboardEvent.d.ts.map +1 -1
  11. package/types/coreHandlers/handleScope.d.ts +4 -1
  12. package/types/coreHandlers/handleScope.d.ts.map +1 -1
  13. package/types/coreHandlers/handleSlowClick.d.ts +2 -0
  14. package/types/coreHandlers/handleSlowClick.d.ts.map +1 -1
  15. package/types/coreHandlers/util/addBreadcrumbEvent.d.ts.map +1 -1
  16. package/types/coreHandlers/util/getAttributesToRecord.d.ts.map +1 -1
  17. package/types/index.d.ts +5 -0
  18. package/types/index.d.ts.map +1 -1
  19. package/types/integration.d.ts +1 -1
  20. package/types/integration.d.ts.map +1 -1
  21. package/types/replay.d.ts +28 -2
  22. package/types/replay.d.ts.map +1 -1
  23. package/types/session/getSession.d.ts.map +1 -1
  24. package/types/types/index.d.ts +6 -0
  25. package/types/types/index.d.ts.map +1 -0
  26. package/types/types/performance.d.ts +124 -0
  27. package/types/types/performance.d.ts.map +1 -0
  28. package/types/{types.d.ts → types/replay.d.ts} +37 -146
  29. package/types/types/replay.d.ts.map +1 -0
  30. package/types/types/replayFrame.d.ts +160 -0
  31. package/types/types/replayFrame.d.ts.map +1 -0
  32. package/types/types/request.d.ts +15 -0
  33. package/types/types/request.d.ts.map +1 -0
  34. package/types/types/rrweb.d.ts +1 -9
  35. package/types/types/rrweb.d.ts.map +1 -1
  36. package/types/util/addEvent.d.ts.map +1 -1
  37. package/types/util/addGlobalListeners.d.ts.map +1 -1
  38. package/types/util/createBreadcrumb.d.ts +2 -4
  39. package/types/util/createBreadcrumb.d.ts.map +1 -1
  40. package/types/util/createPerformanceSpans.d.ts +1 -1
  41. package/types/util/createPerformanceSpans.d.ts.map +1 -1
  42. package/types/util/handleRecordingEmit.d.ts +2 -2
  43. package/types/util/handleRecordingEmit.d.ts.map +1 -1
  44. package/types/util/throttle.d.ts +12 -0
  45. package/types/util/throttle.d.ts.map +1 -0
  46. package/types/types.d.ts.map +0 -1
package/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { getCurrentHub, addBreadcrumb, addGlobalEventProcessor, prepareEvent, setContext, captureException } from '@sentry/core';
2
- import { GLOBAL_OBJ, logger, normalize, htmlTreeAsString, uuid4, SENTRY_XHR_DATA_KEY, dropUndefinedKeys, stringMatchesSomePattern, addInstrumentationHandler, browserPerformanceTimeOrigin, createEnvelope, createEventEnvelopeHeaders, getSdkMetadataForEnvelopeHeader, isNodeEnv } from '@sentry/utils';
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';
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
@@ -38,6 +38,11 @@ const NETWORK_BODY_MAX_SIZE = 150000;
38
38
  /* The max size of a single console arg that is captured. Any arg larger than this will be truncated. */
39
39
  const CONSOLE_ARG_MAX_SIZE = 5000;
40
40
 
41
+ /* Min. time to wait before we consider something a slow click. */
42
+ const SLOW_CLICK_THRESHOLD = 3000;
43
+ /* For scroll actions after a click, we only look for a very short time period to detect programmatic scrolling. */
44
+ const SLOW_CLICK_SCROLL_TIMEOUT = 300;
45
+
41
46
  var NodeType$1;
42
47
  (function (NodeType) {
43
48
  NodeType[NodeType["Document"] = 0] = "Document";
@@ -991,7 +996,7 @@ function skipAttribute(tagName, attributeName, value) {
991
996
  return ((tagName === 'video' || tagName === 'audio') && attributeName === 'autoplay');
992
997
  }
993
998
 
994
- var EventType$1;
999
+ var EventType;
995
1000
  (function (EventType) {
996
1001
  EventType[EventType["DomContentLoaded"] = 0] = "DomContentLoaded";
997
1002
  EventType[EventType["Load"] = 1] = "Load";
@@ -1000,7 +1005,7 @@ var EventType$1;
1000
1005
  EventType[EventType["Meta"] = 4] = "Meta";
1001
1006
  EventType[EventType["Custom"] = 5] = "Custom";
1002
1007
  EventType[EventType["Plugin"] = 6] = "Plugin";
1003
- })(EventType$1 || (EventType$1 = {}));
1008
+ })(EventType || (EventType = {}));
1004
1009
  var IncrementalSource;
1005
1010
  (function (IncrementalSource) {
1006
1011
  IncrementalSource[IncrementalSource["Mutation"] = 0] = "Mutation";
@@ -1133,7 +1138,7 @@ if (typeof window !== 'undefined' && window.Proxy && window.Reflect) {
1133
1138
  },
1134
1139
  });
1135
1140
  }
1136
- function throttle(func, wait, options = {}) {
1141
+ function throttle$1(func, wait, options = {}) {
1137
1142
  let timeout = null;
1138
1143
  let previous = 0;
1139
1144
  return function (arg) {
@@ -1868,7 +1873,7 @@ function initMoveObserver({ mousemoveCb, sampling, doc, mirror, }) {
1868
1873
  : 500;
1869
1874
  let positions = [];
1870
1875
  let timeBaseline;
1871
- const wrappedCb = throttle((source) => {
1876
+ const wrappedCb = throttle$1((source) => {
1872
1877
  const totalOffset = Date.now() - timeBaseline;
1873
1878
  callbackWrapper(mousemoveCb)(positions.map((p) => {
1874
1879
  p.timeOffset -= totalOffset;
@@ -1877,7 +1882,7 @@ function initMoveObserver({ mousemoveCb, sampling, doc, mirror, }) {
1877
1882
  positions = [];
1878
1883
  timeBaseline = null;
1879
1884
  }, callbackThreshold);
1880
- const updatePosition = throttle((evt) => {
1885
+ const updatePosition = throttle$1((evt) => {
1881
1886
  const target = getEventTarget(evt);
1882
1887
  const { clientX, clientY } = isTouchEvent(evt)
1883
1888
  ? evt.changedTouches[0]
@@ -1951,7 +1956,7 @@ function initMouseInteractionObserver({ mouseInteractionCb, doc, mirror, blockCl
1951
1956
  });
1952
1957
  }
1953
1958
  function initScrollObserver({ scrollCb, doc, mirror, blockClass, blockSelector, unblockSelector, sampling, }) {
1954
- const updatePosition = throttle((evt) => {
1959
+ const updatePosition = throttle$1((evt) => {
1955
1960
  const target = getEventTarget(evt);
1956
1961
  if (!target ||
1957
1962
  isBlocked(target, blockClass, blockSelector, unblockSelector)) {
@@ -1979,7 +1984,7 @@ function initScrollObserver({ scrollCb, doc, mirror, blockClass, blockSelector,
1979
1984
  function initViewportResizeObserver({ viewportResizeCb, }) {
1980
1985
  let lastH = -1;
1981
1986
  let lastW = -1;
1982
- const updateDimension = throttle(() => {
1987
+ const updateDimension = throttle$1(() => {
1983
1988
  const height = getWindowHeight();
1984
1989
  const width = getWindowWidth();
1985
1990
  if (lastH !== height || lastW !== width) {
@@ -2268,7 +2273,7 @@ function initStyleDeclarationObserver({ styleDeclarationCb, mirror }, { win }) {
2268
2273
  });
2269
2274
  }
2270
2275
  function initMediaInteractionObserver({ mediaInteractionCb, blockClass, blockSelector, unblockSelector, mirror, sampling, }) {
2271
- const handler = (type) => throttle(callbackWrapper((event) => {
2276
+ const handler = (type) => throttle$1(callbackWrapper((event) => {
2272
2277
  const target = getEventTarget(event);
2273
2278
  if (!target ||
2274
2279
  isBlocked(target, blockClass, blockSelector, unblockSelector)) {
@@ -2973,17 +2978,17 @@ function record(options = {}) {
2973
2978
  wrappedEmit = (e, isCheckout) => {
2974
2979
  var _a;
2975
2980
  if (((_a = mutationBuffers[0]) === null || _a === void 0 ? void 0 : _a.isFrozen()) &&
2976
- e.type !== EventType$1.FullSnapshot &&
2977
- !(e.type === EventType$1.IncrementalSnapshot &&
2981
+ e.type !== EventType.FullSnapshot &&
2982
+ !(e.type === EventType.IncrementalSnapshot &&
2978
2983
  e.data.source === IncrementalSource.Mutation)) {
2979
2984
  mutationBuffers.forEach((buf) => buf.unfreeze());
2980
2985
  }
2981
2986
  emit(eventProcessor(e), isCheckout);
2982
- if (e.type === EventType$1.FullSnapshot) {
2987
+ if (e.type === EventType.FullSnapshot) {
2983
2988
  lastFullSnapshotEvent = e;
2984
2989
  incrementalSnapshotCount = 0;
2985
2990
  }
2986
- else if (e.type === EventType$1.IncrementalSnapshot) {
2991
+ else if (e.type === EventType.IncrementalSnapshot) {
2987
2992
  if (e.data.source === IncrementalSource.Mutation &&
2988
2993
  e.data.isAttachIframe) {
2989
2994
  return;
@@ -2999,16 +3004,16 @@ function record(options = {}) {
2999
3004
  };
3000
3005
  const wrappedMutationEmit = (m) => {
3001
3006
  wrappedEmit(wrapEvent({
3002
- type: EventType$1.IncrementalSnapshot,
3007
+ type: EventType.IncrementalSnapshot,
3003
3008
  data: Object.assign({ source: IncrementalSource.Mutation }, m),
3004
3009
  }));
3005
3010
  };
3006
3011
  const wrappedScrollEmit = (p) => wrappedEmit(wrapEvent({
3007
- type: EventType$1.IncrementalSnapshot,
3012
+ type: EventType.IncrementalSnapshot,
3008
3013
  data: Object.assign({ source: IncrementalSource.Scroll }, p),
3009
3014
  }));
3010
3015
  const wrappedCanvasMutationEmit = (p) => wrappedEmit(wrapEvent({
3011
- type: EventType$1.IncrementalSnapshot,
3016
+ type: EventType.IncrementalSnapshot,
3012
3017
  data: Object.assign({ source: IncrementalSource.CanvasMutation }, p),
3013
3018
  }));
3014
3019
  const iframeManager = new IframeManager({
@@ -3053,7 +3058,7 @@ function record(options = {}) {
3053
3058
  takeFullSnapshot = (isCheckout = false) => {
3054
3059
  var _a, _b, _c, _d;
3055
3060
  wrappedEmit(wrapEvent({
3056
- type: EventType$1.Meta,
3061
+ type: EventType.Meta,
3057
3062
  data: {
3058
3063
  href: window.location.href,
3059
3064
  width: getWindowWidth(),
@@ -3096,7 +3101,7 @@ function record(options = {}) {
3096
3101
  }
3097
3102
  mirror.map = idNodeMap;
3098
3103
  wrappedEmit(wrapEvent({
3099
- type: EventType$1.FullSnapshot,
3104
+ type: EventType.FullSnapshot,
3100
3105
  data: {
3101
3106
  node,
3102
3107
  initialOffset: {
@@ -3121,7 +3126,7 @@ function record(options = {}) {
3121
3126
  const handlers = [];
3122
3127
  handlers.push(on('DOMContentLoaded', () => {
3123
3128
  wrappedEmit(wrapEvent({
3124
- type: EventType$1.DomContentLoaded,
3129
+ type: EventType.DomContentLoaded,
3125
3130
  data: {},
3126
3131
  }));
3127
3132
  }));
@@ -3131,40 +3136,40 @@ function record(options = {}) {
3131
3136
  onMutation,
3132
3137
  mutationCb: wrappedMutationEmit,
3133
3138
  mousemoveCb: (positions, source) => wrappedEmit(wrapEvent({
3134
- type: EventType$1.IncrementalSnapshot,
3139
+ type: EventType.IncrementalSnapshot,
3135
3140
  data: {
3136
3141
  source,
3137
3142
  positions,
3138
3143
  },
3139
3144
  })),
3140
3145
  mouseInteractionCb: (d) => wrappedEmit(wrapEvent({
3141
- type: EventType$1.IncrementalSnapshot,
3146
+ type: EventType.IncrementalSnapshot,
3142
3147
  data: Object.assign({ source: IncrementalSource.MouseInteraction }, d),
3143
3148
  })),
3144
3149
  scrollCb: wrappedScrollEmit,
3145
3150
  viewportResizeCb: (d) => wrappedEmit(wrapEvent({
3146
- type: EventType$1.IncrementalSnapshot,
3151
+ type: EventType.IncrementalSnapshot,
3147
3152
  data: Object.assign({ source: IncrementalSource.ViewportResize }, d),
3148
3153
  })),
3149
3154
  inputCb: (v) => wrappedEmit(wrapEvent({
3150
- type: EventType$1.IncrementalSnapshot,
3155
+ type: EventType.IncrementalSnapshot,
3151
3156
  data: Object.assign({ source: IncrementalSource.Input }, v),
3152
3157
  })),
3153
3158
  mediaInteractionCb: (p) => wrappedEmit(wrapEvent({
3154
- type: EventType$1.IncrementalSnapshot,
3159
+ type: EventType.IncrementalSnapshot,
3155
3160
  data: Object.assign({ source: IncrementalSource.MediaInteraction }, p),
3156
3161
  })),
3157
3162
  styleSheetRuleCb: (r) => wrappedEmit(wrapEvent({
3158
- type: EventType$1.IncrementalSnapshot,
3163
+ type: EventType.IncrementalSnapshot,
3159
3164
  data: Object.assign({ source: IncrementalSource.StyleSheetRule }, r),
3160
3165
  })),
3161
3166
  styleDeclarationCb: (r) => wrappedEmit(wrapEvent({
3162
- type: EventType$1.IncrementalSnapshot,
3167
+ type: EventType.IncrementalSnapshot,
3163
3168
  data: Object.assign({ source: IncrementalSource.StyleDeclaration }, r),
3164
3169
  })),
3165
3170
  canvasMutationCb: wrappedCanvasMutationEmit,
3166
3171
  fontCb: (p) => wrappedEmit(wrapEvent({
3167
- type: EventType$1.IncrementalSnapshot,
3172
+ type: EventType.IncrementalSnapshot,
3168
3173
  data: Object.assign({ source: IncrementalSource.Font }, p),
3169
3174
  })),
3170
3175
  blockClass,
@@ -3197,7 +3202,7 @@ function record(options = {}) {
3197
3202
  observer: p.observer,
3198
3203
  options: p.options,
3199
3204
  callback: (payload) => wrappedEmit(wrapEvent({
3200
- type: EventType$1.Plugin,
3205
+ type: EventType.Plugin,
3201
3206
  data: {
3202
3207
  plugin: p.name,
3203
3208
  payload,
@@ -3225,7 +3230,7 @@ function record(options = {}) {
3225
3230
  else {
3226
3231
  handlers.push(on('load', () => {
3227
3232
  wrappedEmit(wrapEvent({
3228
- type: EventType$1.Load,
3233
+ type: EventType.Load,
3229
3234
  data: {},
3230
3235
  }));
3231
3236
  init();
@@ -3244,7 +3249,7 @@ record.addCustomEvent = (tag, payload) => {
3244
3249
  throw new Error('please add custom event after start recording');
3245
3250
  }
3246
3251
  wrappedEmit(wrapEvent({
3247
- type: EventType$1.Custom,
3252
+ type: EventType.Custom,
3248
3253
  data: {
3249
3254
  tag,
3250
3255
  payload,
@@ -3285,89 +3290,6 @@ var NodeType;
3285
3290
  NodeType[NodeType["Comment"] = 5] = "Comment";
3286
3291
  })(NodeType || (NodeType = {}));
3287
3292
 
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
-
3305
- /**
3306
- * Converts a timestamp to ms, if it was in s, or keeps it as ms.
3307
- */
3308
- function timestampToMs(timestamp) {
3309
- const isMs = timestamp > 9999999999;
3310
- return isMs ? timestamp : timestamp * 1000;
3311
- }
3312
-
3313
- /**
3314
- * Add an event to the event buffer.
3315
- * `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`.
3316
- */
3317
- async function addEvent(
3318
- replay,
3319
- event,
3320
- isCheckout,
3321
- ) {
3322
- if (!replay.eventBuffer) {
3323
- // This implies that `_isEnabled` is false
3324
- return null;
3325
- }
3326
-
3327
- if (replay.isPaused()) {
3328
- // Do not add to event buffer when recording is paused
3329
- return null;
3330
- }
3331
-
3332
- const timestampInMs = timestampToMs(event.timestamp);
3333
-
3334
- // Throw out events that happen more than 5 minutes ago. This can happen if
3335
- // page has been left open and idle for a long period of time and user
3336
- // comes back to trigger a new session. The performance entries rely on
3337
- // `performance.timeOrigin`, which is when the page first opened.
3338
- if (timestampInMs + replay.timeouts.sessionIdlePause < Date.now()) {
3339
- return null;
3340
- }
3341
-
3342
- try {
3343
- if (isCheckout) {
3344
- replay.eventBuffer.clear();
3345
- }
3346
-
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);
3359
- } catch (error) {
3360
- (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error(error);
3361
- await replay.stop('addEvent');
3362
-
3363
- const client = getCurrentHub().getClient();
3364
-
3365
- if (client) {
3366
- client.recordDroppedEvent('internal_sdk_error', 'replay');
3367
- }
3368
- }
3369
- }
3370
-
3371
3293
  /**
3372
3294
  * Add a breadcrumb event to replay.
3373
3295
  */
@@ -3383,8 +3305,8 @@ function addBreadcrumbEvent(replay, breadcrumb) {
3383
3305
  }
3384
3306
 
3385
3307
  replay.addUpdate(() => {
3386
- void addEvent(replay, {
3387
- type: EventType$1.Custom,
3308
+ void replay.throttledAddEvent({
3309
+ type: EventType.Custom,
3388
3310
  // TODO: We were converting from ms to seconds for breadcrumbs, spans,
3389
3311
  // but maybe we should just keep them as milliseconds
3390
3312
  timestamp: (breadcrumb.timestamp || 0) * 1000,
@@ -3506,15 +3428,16 @@ function handleSlowClick(
3506
3428
  addBreadcrumbEvent(replay, breadcrumb);
3507
3429
  }
3508
3430
 
3509
- const SLOW_CLICK_IGNORE_TAGS = ['SELECT', 'OPTION'];
3431
+ const SLOW_CLICK_TAGS = ['A', 'BUTTON', 'INPUT'];
3510
3432
 
3433
+ /** exported for tests only */
3511
3434
  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') || '')) {
3435
+ if (!SLOW_CLICK_TAGS.includes(node.tagName)) {
3514
3436
  return true;
3515
3437
  }
3516
3438
 
3517
- if (SLOW_CLICK_IGNORE_TAGS.includes(node.tagName)) {
3439
+ // If <input> tag, we only want to consider input[type='submit'] & input[type='button']
3440
+ if (node.tagName === 'INPUT' && !['submit', 'button'].includes(node.getAttribute('type') || '')) {
3518
3441
  return true;
3519
3442
  }
3520
3443
 
@@ -3547,6 +3470,8 @@ const ATTRIBUTES_TO_RECORD = new Set([
3547
3470
  'title',
3548
3471
  'data-test-id',
3549
3472
  'data-testid',
3473
+ 'disabled',
3474
+ 'aria-disabled',
3550
3475
  ]);
3551
3476
 
3552
3477
  /**
@@ -3572,14 +3497,14 @@ function getAttributesToRecord(attributes) {
3572
3497
  const handleDomListener = (
3573
3498
  replay,
3574
3499
  ) => {
3575
- const slowClickExperiment = replay.getOptions()._experiments.slowClicks;
3500
+ const { slowClickTimeout, slowClickIgnoreSelectors } = replay.getOptions();
3576
3501
 
3577
- const slowClickConfig = slowClickExperiment
3502
+ const slowClickConfig = slowClickTimeout
3578
3503
  ? {
3579
- threshold: slowClickExperiment.threshold,
3580
- timeout: slowClickExperiment.timeout,
3581
- scrollTimeout: slowClickExperiment.scrollTimeout,
3582
- ignoreSelector: slowClickExperiment.ignoreSelectors ? slowClickExperiment.ignoreSelectors.join(',') : '',
3504
+ threshold: Math.min(SLOW_CLICK_THRESHOLD, slowClickTimeout),
3505
+ timeout: slowClickTimeout,
3506
+ scrollTimeout: SLOW_CLICK_SCROLL_TIMEOUT,
3507
+ ignoreSelector: slowClickIgnoreSelectors ? slowClickIgnoreSelectors.join(',') : '',
3583
3508
  }
3584
3509
  : undefined;
3585
3510
 
@@ -3708,7 +3633,10 @@ function handleKeyboardEvent(replay, event) {
3708
3633
  return;
3709
3634
  }
3710
3635
 
3711
- replay.triggerUserActivity();
3636
+ // Update user activity, but do not restart recording as it can create
3637
+ // noisy/low-value replays (e.g. user comes back from idle, hits alt-tab, new
3638
+ // session with a single "keydown" breadcrumb is created)
3639
+ replay.updateUserActivity();
3712
3640
 
3713
3641
  const breadcrumb = getKeyboardBreadcrumb(event);
3714
3642
 
@@ -3724,7 +3652,7 @@ function getKeyboardBreadcrumb(event) {
3724
3652
  const { metaKey, shiftKey, ctrlKey, altKey, key, target } = event;
3725
3653
 
3726
3654
  // never capture for input fields
3727
- if (!target || isInputElement(target )) {
3655
+ if (!target || isInputElement(target ) || !key) {
3728
3656
  return null;
3729
3657
  }
3730
3658
 
@@ -3896,6 +3824,14 @@ function t(t){let e=t.length;for(;--e>=0;)t[e]=0}const e=new Uint8Array([0,0,0,0
3896
3824
 
3897
3825
  function e(){const e=new Blob([r]);return URL.createObjectURL(e)}
3898
3826
 
3827
+ /**
3828
+ * Converts a timestamp to ms, if it was in s, or keeps it as ms.
3829
+ */
3830
+ function timestampToMs(timestamp) {
3831
+ const isMs = timestamp > 9999999999;
3832
+ return isMs ? timestamp : timestamp * 1000;
3833
+ }
3834
+
3899
3835
  /**
3900
3836
  * A basic event buffer that does not do any compression.
3901
3837
  * Used as fallback if the compression worker cannot be loaded or is disabled.
@@ -4451,11 +4387,13 @@ function getSession({
4451
4387
  // within "max session time").
4452
4388
  const isExpired = isSessionExpired(session, timeouts);
4453
4389
 
4454
- if (!isExpired) {
4390
+ if (!isExpired || (allowBuffering && session.shouldRefresh)) {
4455
4391
  return { type: 'saved', session };
4456
4392
  } else if (!session.shouldRefresh) {
4457
- // In this case, stop
4458
- // This is the case if we have an error session that is completed (=triggered an error)
4393
+ // This is the case if we have an error session that is completed
4394
+ // (=triggered an error). Session will continue as session-based replay,
4395
+ // and when this session is expired, it will not be renewed until user
4396
+ // reloads.
4459
4397
  const discardedSession = makeSession({ sampled: false });
4460
4398
  return { type: 'new', session: discardedSession };
4461
4399
  } else {
@@ -4473,6 +4411,68 @@ function getSession({
4473
4411
  return { type: 'new', session: newSession };
4474
4412
  }
4475
4413
 
4414
+ function isCustomEvent(event) {
4415
+ return event.type === EventType.Custom;
4416
+ }
4417
+
4418
+ /**
4419
+ * Add an event to the event buffer.
4420
+ * `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`.
4421
+ */
4422
+ async function addEvent(
4423
+ replay,
4424
+ event,
4425
+ isCheckout,
4426
+ ) {
4427
+ if (!replay.eventBuffer) {
4428
+ // This implies that `_isEnabled` is false
4429
+ return null;
4430
+ }
4431
+
4432
+ if (replay.isPaused()) {
4433
+ // Do not add to event buffer when recording is paused
4434
+ return null;
4435
+ }
4436
+
4437
+ const timestampInMs = timestampToMs(event.timestamp);
4438
+
4439
+ // Throw out events that happen more than 5 minutes ago. This can happen if
4440
+ // page has been left open and idle for a long period of time and user
4441
+ // comes back to trigger a new session. The performance entries rely on
4442
+ // `performance.timeOrigin`, which is when the page first opened.
4443
+ if (timestampInMs + replay.timeouts.sessionIdlePause < Date.now()) {
4444
+ return null;
4445
+ }
4446
+
4447
+ try {
4448
+ if (isCheckout) {
4449
+ replay.eventBuffer.clear();
4450
+ }
4451
+
4452
+ const replayOptions = replay.getOptions();
4453
+
4454
+ const eventAfterPossibleCallback =
4455
+ typeof replayOptions.beforeAddRecordingEvent === 'function' && isCustomEvent(event)
4456
+ ? replayOptions.beforeAddRecordingEvent(event)
4457
+ : event;
4458
+
4459
+ if (!eventAfterPossibleCallback) {
4460
+ return;
4461
+ }
4462
+
4463
+ return await replay.eventBuffer.addEvent(eventAfterPossibleCallback);
4464
+ } catch (error) {
4465
+ (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error(error);
4466
+ await replay.stop('addEvent');
4467
+
4468
+ const client = getCurrentHub().getClient();
4469
+
4470
+ if (client) {
4471
+ client.recordDroppedEvent('internal_sdk_error', 'replay');
4472
+ }
4473
+ }
4474
+ }
4475
+
4476
4476
  /** If the event is an error event */
4477
4477
  function isErrorEvent(event) {
4478
4478
  return !event.type;
@@ -4647,13 +4647,6 @@ function handleGlobalEventListener(
4647
4647
  event.tags = { ...event.tags, replayId: replay.getSessionId() };
4648
4648
  }
4649
4649
 
4650
- if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && replay.getOptions()._experiments.traceInternals && isErrorEvent(event)) {
4651
- const exc = getEventExceptionValues(event);
4652
- addInternalBreadcrumb({
4653
- message: `Tagging event (${event.event_id}) - ${event.message} - ${exc.type}: ${exc.value}`,
4654
- });
4655
- }
4656
-
4657
4650
  // In cases where a custom client is used that does not support the new hooks (yet),
4658
4651
  // we manually call this hook method here
4659
4652
  if (afterSendHandler) {
@@ -4665,35 +4658,16 @@ function handleGlobalEventListener(
4665
4658
  };
4666
4659
  }
4667
4660
 
4668
- function addInternalBreadcrumb(arg) {
4669
- const { category, level, message, ...rest } = arg;
4670
-
4671
- addBreadcrumb({
4672
- category: category || 'console',
4673
- level: level || 'debug',
4674
- message: `[debug]: ${message}`,
4675
- ...rest,
4676
- });
4677
- }
4678
-
4679
- function getEventExceptionValues(event) {
4680
- return {
4681
- type: 'Unknown',
4682
- value: 'n/a',
4683
- ...(event.exception && event.exception.values && event.exception.values[0]),
4684
- };
4685
- }
4686
-
4687
4661
  /**
4688
- * Create a "span" for each performance entry. The parent transaction is `this.replayEvent`.
4662
+ * Create a "span" for each performance entry.
4689
4663
  */
4690
4664
  function createPerformanceSpans(
4691
4665
  replay,
4692
4666
  entries,
4693
4667
  ) {
4694
- return entries.map(({ type, start, end, name, data }) =>
4695
- addEvent(replay, {
4696
- type: EventType$1.Custom,
4668
+ return entries.map(({ type, start, end, name, data }) => {
4669
+ const response = replay.throttledAddEvent({
4670
+ type: EventType.Custom,
4697
4671
  timestamp: start,
4698
4672
  data: {
4699
4673
  tag: 'performanceSpan',
@@ -4705,8 +4679,11 @@ function createPerformanceSpans(
4705
4679
  data,
4706
4680
  },
4707
4681
  },
4708
- }),
4709
- );
4682
+ });
4683
+
4684
+ // If response is a string, it means its either THROTTLED or SKIPPED
4685
+ return typeof response === 'string' ? Promise.resolve(null) : response;
4686
+ });
4710
4687
  }
4711
4688
 
4712
4689
  function handleHistory(handlerData) {
@@ -5924,6 +5901,10 @@ function _isFetchHint(hint) {
5924
5901
 
5925
5902
  let _LAST_BREADCRUMB = null;
5926
5903
 
5904
+ function isBreadcrumbWithCategory(breadcrumb) {
5905
+ return !!breadcrumb.category;
5906
+ }
5907
+
5927
5908
  const handleScopeListener =
5928
5909
  (replay) =>
5929
5910
  (scope) => {
@@ -5959,9 +5940,9 @@ function handleScope(scope) {
5959
5940
  _LAST_BREADCRUMB = newBreadcrumb;
5960
5941
 
5961
5942
  if (
5962
- newBreadcrumb.category &&
5963
- (['fetch', 'xhr', 'sentry.event', 'sentry.transaction'].includes(newBreadcrumb.category) ||
5964
- newBreadcrumb.category.startsWith('ui.'))
5943
+ !isBreadcrumbWithCategory(newBreadcrumb) ||
5944
+ ['fetch', 'xhr', 'sentry.event', 'sentry.transaction'].includes(newBreadcrumb.category) ||
5945
+ newBreadcrumb.category.startsWith('ui.')
5965
5946
  ) {
5966
5947
  return null;
5967
5948
  }
@@ -5974,7 +5955,9 @@ function handleScope(scope) {
5974
5955
  }
5975
5956
 
5976
5957
  /** exported for tests only */
5977
- function normalizeConsoleBreadcrumb(breadcrumb) {
5958
+ function normalizeConsoleBreadcrumb(
5959
+ breadcrumb,
5960
+ ) {
5978
5961
  const args = breadcrumb.data && breadcrumb.data.arguments;
5979
5962
 
5980
5963
  if (!Array.isArray(args) || args.length === 0) {
@@ -6055,6 +6038,16 @@ function addGlobalListeners(replay) {
6055
6038
  dsc.replay_id = replayId;
6056
6039
  }
6057
6040
  });
6041
+
6042
+ client.on('startTransaction', transaction => {
6043
+ replay.lastTransaction = transaction;
6044
+ });
6045
+
6046
+ // We may be missing the initial startTransaction due to timing issues,
6047
+ // so we capture it on finish again.
6048
+ client.on('finishTransaction', transaction => {
6049
+ replay.lastTransaction = transaction;
6050
+ });
6058
6051
  }
6059
6052
  }
6060
6053
 
@@ -6413,6 +6406,10 @@ function getHandleRecordingEmit(replay) {
6413
6406
  // a previous session ID. In this case, we want to buffer events
6414
6407
  // for a set amount of time before flushing. This can help avoid
6415
6408
  // capturing replays of users that immediately close the window.
6409
+ // TODO: We should check `recordingMode` here and do nothing if it's
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.
6416
6413
  setTimeout(() => replay.conditionalFlush(), options._experiments.delayFlushOnCheckout);
6417
6414
 
6418
6415
  // Cancel any previously debounced flushes to ensure there are no [near]
@@ -6778,6 +6775,62 @@ async function sendReplay(
6778
6775
  }
6779
6776
  }
6780
6777
 
6778
+ const THROTTLED = '__THROTTLED';
6779
+ const SKIPPED = '__SKIPPED';
6780
+
6781
+ /**
6782
+ * Create a throttled function off a given function.
6783
+ * When calling the throttled function, it will call the original function only
6784
+ * if it hasn't been called more than `maxCount` times in the last `durationSeconds`.
6785
+ *
6786
+ * Returns `THROTTLED` if throttled for the first time, after that `SKIPPED`,
6787
+ * or else the return value of the original function.
6788
+ */
6789
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6790
+ function throttle(
6791
+ fn,
6792
+ maxCount,
6793
+ durationSeconds,
6794
+ ) {
6795
+ const counter = new Map();
6796
+
6797
+ const _cleanup = (now) => {
6798
+ const threshold = now - durationSeconds;
6799
+ counter.forEach((_value, key) => {
6800
+ if (key < threshold) {
6801
+ counter.delete(key);
6802
+ }
6803
+ });
6804
+ };
6805
+
6806
+ const _getTotalCount = () => {
6807
+ return [...counter.values()].reduce((a, b) => a + b, 0);
6808
+ };
6809
+
6810
+ let isThrottled = false;
6811
+
6812
+ return (...rest) => {
6813
+ // Date in second-precision, which we use as basis for the throttling
6814
+ const now = Math.floor(Date.now() / 1000);
6815
+
6816
+ // First, make sure to delete any old entries
6817
+ _cleanup(now);
6818
+
6819
+ // If already over limit, do nothing
6820
+ if (_getTotalCount() >= maxCount) {
6821
+ const wasThrottled = isThrottled;
6822
+ isThrottled = true;
6823
+ return wasThrottled ? SKIPPED : THROTTLED;
6824
+ }
6825
+
6826
+ isThrottled = false;
6827
+ const count = counter.get(now) || 0;
6828
+ counter.set(now, count + 1);
6829
+
6830
+ return fn(...rest);
6831
+ };
6832
+ }
6833
+
6781
6834
  /* eslint-disable max-lines */ // TODO: We might want to split this file up
6782
6835
 
6783
6836
  /**
@@ -6800,6 +6853,11 @@ class ReplayContainer {
6800
6853
  */
6801
6854
  __init3() {this.recordingMode = 'session';}
6802
6855
 
6856
+ /**
6857
+ * The current or last active transcation.
6858
+ * This is only available when performance is enabled.
6859
+ */
6860
+
6803
6861
  /**
6804
6862
  * These are here so we can overwrite them in tests etc.
6805
6863
  * @hidden
@@ -6866,6 +6924,14 @@ class ReplayContainer {
6866
6924
  this._debouncedFlush = debounce(() => this._flush(), this._options.flushMinDelay, {
6867
6925
  maxWait: this._options.flushMaxDelay,
6868
6926
  });
6927
+
6928
+ this._throttledAddEvent = throttle(
6929
+ (event, isCheckout) => addEvent(this, event, isCheckout),
6930
+ // Max 300 events...
6931
+ 300,
6932
+ // ... per 5s
6933
+ 5,
6934
+ );
6869
6935
  }
6870
6936
 
6871
6937
  /** Get the event context. */
@@ -7110,6 +7176,8 @@ class ReplayContainer {
7110
7176
  return this.flushImmediate();
7111
7177
  }
7112
7178
 
7179
+ const activityTime = Date.now();
7180
+
7113
7181
  // Allow flush to complete before resuming as a session recording, otherwise
7114
7182
  // the checkout from `startRecording` may be included in the payload.
7115
7183
  // Prefer to keep the error replay as a separate (and smaller) segment
@@ -7131,6 +7199,18 @@ class ReplayContainer {
7131
7199
  // Once this session ends, we do not want to refresh it
7132
7200
  if (this.session) {
7133
7201
  this.session.shouldRefresh = false;
7202
+
7203
+ // It's possible that the session lifespan is > max session lifespan
7204
+ // because we have been buffering beyond max session lifespan (we ignore
7205
+ // expiration given that `shouldRefresh` is true). Since we flip
7206
+ // `shouldRefresh`, the session could be considered expired due to
7207
+ // lifespan, which is not what we want. Update session start date to be
7208
+ // the current timestamp, so that session is not considered to be
7209
+ // expired. This means that max replay duration can be MAX_SESSION_LIFE +
7210
+ // (length of buffer), which we are ok with.
7211
+ this._updateUserActivity(activityTime);
7212
+ this._updateSessionActivity(activityTime);
7213
+ this.session.started = activityTime;
7134
7214
  this._maybeSaveSession();
7135
7215
  }
7136
7216
 
@@ -7194,6 +7274,18 @@ class ReplayContainer {
7194
7274
  this._updateSessionActivity();
7195
7275
  }
7196
7276
 
7277
+ /**
7278
+ * Updates the user activity timestamp *without* resuming
7279
+ * recording. Some user events (e.g. keydown) can be create
7280
+ * low-value replays that only contain the keypress as a
7281
+ * breadcrumb. Instead this would require other events to
7282
+ * create a new replay after a session has expired.
7283
+ */
7284
+ updateUserActivity() {
7285
+ this._updateUserActivity();
7286
+ this._updateSessionActivity();
7287
+ }
7288
+
7197
7289
  /**
7198
7290
  * Only flush if `this.recordingMode === 'session'`
7199
7291
  */
@@ -7295,6 +7387,52 @@ class ReplayContainer {
7295
7387
  this._context.urls.push(url);
7296
7388
  }
7297
7389
 
7390
+ /**
7391
+ * Add a breadcrumb event, that may be throttled.
7392
+ * If it was throttled, we add a custom breadcrumb to indicate that.
7393
+ */
7394
+ throttledAddEvent(
7395
+ event,
7396
+ isCheckout,
7397
+ ) {
7398
+ const res = this._throttledAddEvent(event, isCheckout);
7399
+
7400
+ // If this is THROTTLED, it means we have throttled the event for the first time
7401
+ // In this case, we want to add a breadcrumb indicating that something was skipped
7402
+ if (res === THROTTLED) {
7403
+ const breadcrumb = createBreadcrumb({
7404
+ category: 'replay.throttled',
7405
+ });
7406
+
7407
+ this.addUpdate(() => {
7408
+ void addEvent(this, {
7409
+ type: EventType.Custom,
7410
+ timestamp: breadcrumb.timestamp || 0,
7411
+ data: {
7412
+ tag: 'breadcrumb',
7413
+ payload: breadcrumb,
7414
+ metric: true,
7415
+ },
7416
+ });
7417
+ });
7418
+ }
7419
+
7420
+ return res;
7421
+ }
7422
+
7423
+ /**
7424
+ * This will get the parametrized route name of the current page.
7425
+ * This is only available if performance is enabled, and if an instrumented router is used.
7426
+ */
7427
+ getCurrentRoute() {
7428
+ const lastTransaction = this.lastTransaction || getCurrentHub().getScope().getTransaction();
7429
+ if (!lastTransaction || !['route', 'custom'].includes(lastTransaction.metadata.source)) {
7430
+ return undefined;
7431
+ }
7432
+
7433
+ return lastTransaction.name;
7434
+ }
7435
+
7298
7436
  /**
7299
7437
  * Initialize and start all listeners to varying events (DOM,
7300
7438
  * Performance Observer, Recording, Sentry SDK, etc)
@@ -7338,7 +7476,7 @@ class ReplayContainer {
7338
7476
  stickySession: Boolean(this._options.stickySession),
7339
7477
  currentSession: this.session,
7340
7478
  sessionSampleRate: this._options.sessionSampleRate,
7341
- allowBuffering: this._options.errorSampleRate > 0,
7479
+ allowBuffering: this._options.errorSampleRate > 0 || this.recordingMode === 'buffer',
7342
7480
  });
7343
7481
 
7344
7482
  // If session was newly created (i.e. was not loaded from storage), then
@@ -7533,8 +7671,8 @@ class ReplayContainer {
7533
7671
  */
7534
7672
  _createCustomBreadcrumb(breadcrumb) {
7535
7673
  this.addUpdate(() => {
7536
- void addEvent(this, {
7537
- type: EventType$1.Custom,
7674
+ void this.throttledAddEvent({
7675
+ type: EventType.Custom,
7538
7676
  timestamp: breadcrumb.timestamp || 0,
7539
7677
  data: {
7540
7678
  tag: 'breadcrumb',
@@ -7732,8 +7870,8 @@ class ReplayContainer {
7732
7870
  __init18() {this._onMutationHandler = (mutations) => {
7733
7871
  const count = mutations.length;
7734
7872
 
7735
- const mutationLimit = this._options._experiments.mutationLimit || 0;
7736
- const mutationBreadcrumbLimit = this._options._experiments.mutationBreadcrumbLimit || 1000;
7873
+ const mutationLimit = this._options.mutationLimit;
7874
+ const mutationBreadcrumbLimit = this._options.mutationBreadcrumbLimit;
7737
7875
  const overMutationLimit = mutationLimit && count > mutationLimit;
7738
7876
 
7739
7877
  // Create a breadcrumb if a lot of mutations happen at the same time
@@ -7743,15 +7881,15 @@ class ReplayContainer {
7743
7881
  category: 'replay.mutations',
7744
7882
  data: {
7745
7883
  count,
7884
+ limit: overMutationLimit,
7746
7885
  },
7747
7886
  });
7748
7887
  this._createCustomBreadcrumb(breadcrumb);
7749
7888
  }
7750
7889
 
7890
+ // Stop replay if over the mutation limit
7751
7891
  if (overMutationLimit) {
7752
- // We want to skip doing an incremental snapshot if there are too many mutations
7753
- // Instead, we do a full snapshot
7754
- this._triggerFullSnapshot(false);
7892
+ void this.stop('mutationLimit');
7755
7893
  return false;
7756
7894
  }
7757
7895
 
@@ -7905,6 +8043,12 @@ class Replay {
7905
8043
  maskAllInputs = true,
7906
8044
  blockAllMedia = true,
7907
8045
 
8046
+ mutationBreadcrumbLimit = 750,
8047
+ mutationLimit = 10000,
8048
+
8049
+ slowClickTimeout = 7000,
8050
+ slowClickIgnoreSelectors = [],
8051
+
7908
8052
  networkDetailAllowUrls = [],
7909
8053
  networkCaptureBodies = true,
7910
8054
  networkRequestHeaders = [],
@@ -7972,6 +8116,10 @@ class Replay {
7972
8116
  blockAllMedia,
7973
8117
  maskAllInputs,
7974
8118
  maskAllText,
8119
+ mutationBreadcrumbLimit,
8120
+ mutationLimit,
8121
+ slowClickTimeout,
8122
+ slowClickIgnoreSelectors,
7975
8123
  networkDetailAllowUrls,
7976
8124
  networkCaptureBodies,
7977
8125
  networkRequestHeaders: _getMergedNetworkHeaders(networkRequestHeaders),
@@ -8178,5 +8326,5 @@ function _getMergedNetworkHeaders(headers) {
8178
8326
  return [...DEFAULT_NETWORK_HEADERS, ...headers.map(header => header.toLowerCase())];
8179
8327
  }
8180
8328
 
8181
- export { Replay };
8329
+ export { EventType, NodeType, Replay };
8182
8330
  //# sourceMappingURL=index.js.map