@sentry/replay 7.74.2-alpha.1 → 7.75.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
@@ -2,6 +2,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
2
2
 
3
3
  const core = require('@sentry/core');
4
4
  const utils = require('@sentry/utils');
5
+ const tracing = require('@sentry-internal/tracing');
5
6
 
6
7
  // exporting a separate copy of `WINDOW` rather than exporting the one from `@sentry/browser`
7
8
  // prevents the browser package from being bundled in the CDN bundle, and avoids a
@@ -5125,136 +5126,193 @@ function isInputElement(target) {
5125
5126
  return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
5126
5127
  }
5127
5128
 
5128
- const NAVIGATION_ENTRY_KEYS = [
5129
- 'name',
5130
- 'type',
5131
- 'startTime',
5132
- 'transferSize',
5133
- 'duration',
5134
- ];
5135
-
5136
- function isNavigationEntryEqual(a) {
5137
- return function (b) {
5138
- return NAVIGATION_ENTRY_KEYS.every(key => a[key] === b[key]);
5139
- };
5140
- }
5129
+ // Map entryType -> function to normalize data for event
5130
+ const ENTRY_TYPES
5131
+
5132
+ = {
5133
+ // @ts-expect-error TODO: entry type does not fit the create* functions entry type
5134
+ resource: createResourceEntry,
5135
+ paint: createPaintEntry,
5136
+ // @ts-expect-error TODO: entry type does not fit the create* functions entry type
5137
+ navigation: createNavigationEntry,
5138
+ };
5141
5139
 
5142
5140
  /**
5143
- * There are some difficulties diagnosing why there are duplicate navigation
5144
- * entries. We've witnessed several intermittent results:
5145
- * - duplicate entries have duration = 0
5146
- * - duplicate entries are the same object reference
5147
- * - none of the above
5148
- *
5149
- * Compare the values of several keys to determine if the entries are duplicates or not.
5141
+ * Create replay performance entries from the browser performance entries.
5150
5142
  */
5151
- // TODO (high-prio): Figure out wth is returned here
5152
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
5153
- function dedupePerformanceEntries(
5154
- currentList,
5155
- newList,
5143
+ function createPerformanceEntries(
5144
+ entries,
5156
5145
  ) {
5157
- // Partition `currentList` into 3 different lists based on entryType
5158
- const [existingNavigationEntries, existingLcpEntries, existingEntries] = currentList.reduce(
5159
- (acc, entry) => {
5160
- if (entry.entryType === 'navigation') {
5161
- acc[0].push(entry );
5162
- } else if (entry.entryType === 'largest-contentful-paint') {
5163
- acc[1].push(entry );
5164
- } else {
5165
- acc[2].push(entry);
5166
- }
5167
- return acc;
5146
+ return entries.map(createPerformanceEntry).filter(Boolean) ;
5147
+ }
5148
+
5149
+ function createPerformanceEntry(entry) {
5150
+ if (!ENTRY_TYPES[entry.entryType]) {
5151
+ return null;
5152
+ }
5153
+
5154
+ return ENTRY_TYPES[entry.entryType](entry);
5155
+ }
5156
+
5157
+ function getAbsoluteTime(time) {
5158
+ // browserPerformanceTimeOrigin can be undefined if `performance` or
5159
+ // `performance.now` doesn't exist, but this is already checked by this integration
5160
+ return ((utils.browserPerformanceTimeOrigin || WINDOW.performance.timeOrigin) + time) / 1000;
5161
+ }
5162
+
5163
+ function createPaintEntry(entry) {
5164
+ const { duration, entryType, name, startTime } = entry;
5165
+
5166
+ const start = getAbsoluteTime(startTime);
5167
+ return {
5168
+ type: entryType,
5169
+ name,
5170
+ start,
5171
+ end: start + duration,
5172
+ data: undefined,
5173
+ };
5174
+ }
5175
+
5176
+ function createNavigationEntry(entry) {
5177
+ const {
5178
+ entryType,
5179
+ name,
5180
+ decodedBodySize,
5181
+ duration,
5182
+ domComplete,
5183
+ encodedBodySize,
5184
+ domContentLoadedEventStart,
5185
+ domContentLoadedEventEnd,
5186
+ domInteractive,
5187
+ loadEventStart,
5188
+ loadEventEnd,
5189
+ redirectCount,
5190
+ startTime,
5191
+ transferSize,
5192
+ type,
5193
+ } = entry;
5194
+
5195
+ // Ignore entries with no duration, they do not seem to be useful and cause dupes
5196
+ if (duration === 0) {
5197
+ return null;
5198
+ }
5199
+
5200
+ return {
5201
+ type: `${entryType}.${type}`,
5202
+ start: getAbsoluteTime(startTime),
5203
+ end: getAbsoluteTime(domComplete),
5204
+ name,
5205
+ data: {
5206
+ size: transferSize,
5207
+ decodedBodySize,
5208
+ encodedBodySize,
5209
+ duration,
5210
+ domInteractive,
5211
+ domContentLoadedEventStart,
5212
+ domContentLoadedEventEnd,
5213
+ loadEventStart,
5214
+ loadEventEnd,
5215
+ domComplete,
5216
+ redirectCount,
5168
5217
  },
5169
- [[], [], []],
5170
- );
5218
+ };
5219
+ }
5171
5220
 
5172
- const newEntries = [];
5173
- const newNavigationEntries = [];
5174
- let newLcpEntry = existingLcpEntries.length
5175
- ? existingLcpEntries[existingLcpEntries.length - 1] // Take the last element as list is sorted
5176
- : undefined;
5177
-
5178
- newList.forEach(entry => {
5179
- if (entry.entryType === 'largest-contentful-paint') {
5180
- // We want the latest LCP event only
5181
- if (!newLcpEntry || newLcpEntry.startTime < entry.startTime) {
5182
- newLcpEntry = entry;
5183
- }
5184
- return;
5185
- }
5221
+ function createResourceEntry(
5222
+ entry,
5223
+ ) {
5224
+ const {
5225
+ entryType,
5226
+ initiatorType,
5227
+ name,
5228
+ responseEnd,
5229
+ startTime,
5230
+ decodedBodySize,
5231
+ encodedBodySize,
5232
+ responseStatus,
5233
+ transferSize,
5234
+ } = entry;
5186
5235
 
5187
- if (entry.entryType === 'navigation') {
5188
- const navigationEntry = entry ;
5236
+ // Core SDK handles these
5237
+ if (['fetch', 'xmlhttprequest'].includes(initiatorType)) {
5238
+ return null;
5239
+ }
5189
5240
 
5190
- // Check if the navigation entry is contained in currentList or newList
5191
- if (
5192
- // Ignore any navigation entries with duration 0, as they are likely duplicates
5193
- entry.duration > 0 &&
5194
- // Ensure new entry does not already exist in existing entries
5195
- !existingNavigationEntries.find(isNavigationEntryEqual(navigationEntry)) &&
5196
- // Ensure new entry does not already exist in new list of navigation entries
5197
- !newNavigationEntries.find(isNavigationEntryEqual(navigationEntry))
5198
- ) {
5199
- newNavigationEntries.push(navigationEntry);
5200
- }
5241
+ return {
5242
+ type: `${entryType}.${initiatorType}`,
5243
+ start: getAbsoluteTime(startTime),
5244
+ end: getAbsoluteTime(responseEnd),
5245
+ name,
5246
+ data: {
5247
+ size: transferSize,
5248
+ statusCode: responseStatus,
5249
+ decodedBodySize,
5250
+ encodedBodySize,
5251
+ },
5252
+ };
5253
+ }
5201
5254
 
5202
- // Otherwise this navigation entry is considered a duplicate and is thrown away
5203
- return;
5204
- }
5255
+ /**
5256
+ * Add a LCP event to the replay based on an LCP metric.
5257
+ */
5258
+ function getLargestContentfulPaint(metric
5205
5259
 
5206
- newEntries.push(entry);
5207
- });
5260
+ ) {
5261
+ const entries = metric.entries;
5262
+ const lastEntry = entries[entries.length - 1] ;
5263
+ const element = lastEntry ? lastEntry.element : undefined;
5264
+
5265
+ const value = metric.value;
5208
5266
 
5209
- // Re-combine and sort by startTime
5210
- return [
5211
- ...(newLcpEntry ? [newLcpEntry] : []),
5212
- ...existingNavigationEntries,
5213
- ...existingEntries,
5214
- ...newEntries,
5215
- ...newNavigationEntries,
5216
- ].sort((a, b) => a.startTime - b.startTime);
5267
+ const end = getAbsoluteTime(value);
5268
+
5269
+ const data = {
5270
+ type: 'largest-contentful-paint',
5271
+ name: 'largest-contentful-paint',
5272
+ start: end,
5273
+ end,
5274
+ data: {
5275
+ value,
5276
+ size: value,
5277
+ nodeId: element ? record.mirror.getId(element) : undefined,
5278
+ },
5279
+ };
5280
+
5281
+ return data;
5217
5282
  }
5218
5283
 
5219
5284
  /**
5220
5285
  * Sets up a PerformanceObserver to listen to all performance entry types.
5286
+ * Returns a callback to stop observing.
5221
5287
  */
5222
5288
  function setupPerformanceObserver(replay) {
5223
- const performanceObserverHandler = (list) => {
5224
- // For whatever reason the observer was returning duplicate navigation
5225
- // entries (the other entry types were not duplicated).
5226
- const newPerformanceEntries = dedupePerformanceEntries(
5227
- replay.performanceEvents,
5228
- list.getEntries() ,
5229
- );
5230
- replay.performanceEvents = newPerformanceEntries;
5231
- };
5232
-
5233
- const performanceObserver = new PerformanceObserver(performanceObserverHandler);
5234
-
5235
- [
5236
- 'element',
5237
- 'event',
5238
- 'first-input',
5239
- 'largest-contentful-paint',
5240
- 'layout-shift',
5241
- 'longtask',
5242
- 'navigation',
5243
- 'paint',
5244
- 'resource',
5245
- ].forEach(type => {
5246
- try {
5247
- performanceObserver.observe({
5248
- type,
5249
- buffered: true,
5250
- });
5251
- } catch (e) {
5252
- // This can throw if an entry type is not supported in the browser.
5253
- // Ignore these errors.
5289
+ function addPerformanceEntry(entry) {
5290
+ // It is possible for entries to come up multiple times
5291
+ if (!replay.performanceEntries.includes(entry)) {
5292
+ replay.performanceEntries.push(entry);
5254
5293
  }
5294
+ }
5295
+
5296
+ function onEntries({ entries }) {
5297
+ entries.forEach(addPerformanceEntry);
5298
+ }
5299
+
5300
+ const clearCallbacks = [];
5301
+
5302
+ (['navigation', 'paint', 'resource'] ).forEach(type => {
5303
+ clearCallbacks.push(tracing.addPerformanceInstrumentationHandler(type, onEntries));
5255
5304
  });
5256
5305
 
5257
- return performanceObserver;
5306
+ clearCallbacks.push(
5307
+ tracing.addLcpInstrumentationHandler(({ metric }) => {
5308
+ replay.replayPerformanceEntries.push(getLargestContentfulPaint(metric));
5309
+ }),
5310
+ );
5311
+
5312
+ // A callback to cleanup all handlers
5313
+ return () => {
5314
+ clearCallbacks.forEach(clearCallback => clearCallback());
5315
+ };
5258
5316
  }
5259
5317
 
5260
5318
  const r = `/*! pako 2.1.0 https://github.com/nodeca/pako @license (MIT AND Zlib) */
@@ -7734,169 +7792,6 @@ function createMemoryEntry(memoryEntry) {
7734
7792
  };
7735
7793
  }
7736
7794
 
7737
- // Map entryType -> function to normalize data for event
7738
- const ENTRY_TYPES
7739
-
7740
- = {
7741
- // @ts-expect-error TODO: entry type does not fit the create* functions entry type
7742
- resource: createResourceEntry,
7743
- paint: createPaintEntry,
7744
- // @ts-expect-error TODO: entry type does not fit the create* functions entry type
7745
- navigation: createNavigationEntry,
7746
- // @ts-expect-error TODO: entry type does not fit the create* functions entry type
7747
- ['largest-contentful-paint']: createLargestContentfulPaint,
7748
- };
7749
-
7750
- /**
7751
- * Create replay performance entries from the browser performance entries.
7752
- */
7753
- function createPerformanceEntries(
7754
- entries,
7755
- ) {
7756
- return entries.map(createPerformanceEntry).filter(Boolean) ;
7757
- }
7758
-
7759
- function createPerformanceEntry(entry) {
7760
- if (ENTRY_TYPES[entry.entryType] === undefined) {
7761
- return null;
7762
- }
7763
-
7764
- return ENTRY_TYPES[entry.entryType](entry);
7765
- }
7766
-
7767
- function getAbsoluteTime(time) {
7768
- // browserPerformanceTimeOrigin can be undefined if `performance` or
7769
- // `performance.now` doesn't exist, but this is already checked by this integration
7770
- return ((utils.browserPerformanceTimeOrigin || WINDOW.performance.timeOrigin) + time) / 1000;
7771
- }
7772
-
7773
- function createPaintEntry(entry) {
7774
- const { duration, entryType, name, startTime } = entry;
7775
-
7776
- const start = getAbsoluteTime(startTime);
7777
- return {
7778
- type: entryType,
7779
- name,
7780
- start,
7781
- end: start + duration,
7782
- data: undefined,
7783
- };
7784
- }
7785
-
7786
- function createNavigationEntry(entry) {
7787
- const {
7788
- entryType,
7789
- name,
7790
- decodedBodySize,
7791
- duration,
7792
- domComplete,
7793
- encodedBodySize,
7794
- domContentLoadedEventStart,
7795
- domContentLoadedEventEnd,
7796
- domInteractive,
7797
- loadEventStart,
7798
- loadEventEnd,
7799
- redirectCount,
7800
- startTime,
7801
- transferSize,
7802
- type,
7803
- } = entry;
7804
-
7805
- // Ignore entries with no duration, they do not seem to be useful and cause dupes
7806
- if (duration === 0) {
7807
- return null;
7808
- }
7809
-
7810
- return {
7811
- type: `${entryType}.${type}`,
7812
- start: getAbsoluteTime(startTime),
7813
- end: getAbsoluteTime(domComplete),
7814
- name,
7815
- data: {
7816
- size: transferSize,
7817
- decodedBodySize,
7818
- encodedBodySize,
7819
- duration,
7820
- domInteractive,
7821
- domContentLoadedEventStart,
7822
- domContentLoadedEventEnd,
7823
- loadEventStart,
7824
- loadEventEnd,
7825
- domComplete,
7826
- redirectCount,
7827
- },
7828
- };
7829
- }
7830
-
7831
- function createResourceEntry(
7832
- entry,
7833
- ) {
7834
- const {
7835
- entryType,
7836
- initiatorType,
7837
- name,
7838
- responseEnd,
7839
- startTime,
7840
- decodedBodySize,
7841
- encodedBodySize,
7842
- responseStatus,
7843
- transferSize,
7844
- } = entry;
7845
-
7846
- // Core SDK handles these
7847
- if (['fetch', 'xmlhttprequest'].includes(initiatorType)) {
7848
- return null;
7849
- }
7850
-
7851
- return {
7852
- type: `${entryType}.${initiatorType}`,
7853
- start: getAbsoluteTime(startTime),
7854
- end: getAbsoluteTime(responseEnd),
7855
- name,
7856
- data: {
7857
- size: transferSize,
7858
- statusCode: responseStatus,
7859
- decodedBodySize,
7860
- encodedBodySize,
7861
- },
7862
- };
7863
- }
7864
-
7865
- function createLargestContentfulPaint(
7866
- entry,
7867
- ) {
7868
- const { entryType, startTime, size } = entry;
7869
-
7870
- let startTimeOrNavigationActivation = 0;
7871
-
7872
- if (WINDOW.performance) {
7873
- const navEntry = WINDOW.performance.getEntriesByType('navigation')[0]
7874
-
7875
- ;
7876
-
7877
- // See https://github.com/GoogleChrome/web-vitals/blob/9f11c4c6578fb4c5ee6fa4e32b9d1d756475f135/src/lib/getActivationStart.ts#L21
7878
- startTimeOrNavigationActivation = (navEntry && navEntry.activationStart) || 0;
7879
- }
7880
-
7881
- // value is in ms
7882
- const value = Math.max(startTime - startTimeOrNavigationActivation, 0);
7883
- // LCP doesn't have a "duration", it just happens at a single point in time.
7884
- // But the UI expects both, so use end (in seconds) for both timestamps.
7885
- const end = getAbsoluteTime(startTimeOrNavigationActivation) + value / 1000;
7886
-
7887
- return {
7888
- type: entryType,
7889
- name: entryType,
7890
- start: end,
7891
- end,
7892
- data: {
7893
- value, // LCP "duration" in ms
7894
- size,
7895
- nodeId: record.mirror.getId(entry.element),
7896
- },
7897
- };
7898
- }
7899
-
7900
7795
  /**
7901
7796
  * Heavily simplified debounce function based on lodash.debounce.
7902
7797
  *
@@ -8467,10 +8362,6 @@ function throttle(
8467
8362
  */
8468
8363
  class ReplayContainer {
8469
8364
 
8470
- /**
8471
- * List of PerformanceEntry from PerformanceObserver
8472
- */
8473
-
8474
8365
  /**
8475
8366
  * Recording can happen in one of three modes:
8476
8367
  * - session: Record the whole session, sending it continuously
@@ -8523,7 +8414,8 @@ class ReplayContainer {
8523
8414
 
8524
8415
  ) {ReplayContainer.prototype.__init.call(this);ReplayContainer.prototype.__init2.call(this);ReplayContainer.prototype.__init3.call(this);ReplayContainer.prototype.__init4.call(this);ReplayContainer.prototype.__init5.call(this);ReplayContainer.prototype.__init6.call(this);
8525
8416
  this.eventBuffer = null;
8526
- this.performanceEvents = [];
8417
+ this.performanceEntries = [];
8418
+ this.replayPerformanceEntries = [];
8527
8419
  this.recordingMode = 'session';
8528
8420
  this.timeouts = {
8529
8421
  sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
@@ -9017,7 +8909,8 @@ class ReplayContainer {
9017
8909
  const urlPath = `${WINDOW.location.pathname}${WINDOW.location.hash}${WINDOW.location.search}`;
9018
8910
  const url = `${WINDOW.location.origin}${urlPath}`;
9019
8911
 
9020
- this.performanceEvents = [];
8912
+ this.performanceEntries = [];
8913
+ this.replayPerformanceEntries = [];
9021
8914
 
9022
8915
  // Reset _context as well
9023
8916
  this._clearContext();
@@ -9196,12 +9089,7 @@ class ReplayContainer {
9196
9089
  this._handleException(err);
9197
9090
  }
9198
9091
 
9199
- // PerformanceObserver //
9200
- if (!('PerformanceObserver' in WINDOW)) {
9201
- return;
9202
- }
9203
-
9204
- this._performanceObserver = setupPerformanceObserver(this);
9092
+ this._performanceCleanupCallback = setupPerformanceObserver(this);
9205
9093
  }
9206
9094
 
9207
9095
  /**
@@ -9219,9 +9107,8 @@ class ReplayContainer {
9219
9107
  this.clickDetector.removeListeners();
9220
9108
  }
9221
9109
 
9222
- if (this._performanceObserver) {
9223
- this._performanceObserver.disconnect();
9224
- this._performanceObserver = undefined;
9110
+ if (this._performanceCleanupCallback) {
9111
+ this._performanceCleanupCallback();
9225
9112
  }
9226
9113
  } catch (err) {
9227
9114
  this._handleException(err);
@@ -9370,15 +9257,16 @@ class ReplayContainer {
9370
9257
  }
9371
9258
 
9372
9259
  /**
9373
- * Observed performance events are added to `this.performanceEvents`. These
9260
+ * Observed performance events are added to `this.performanceEntries`. These
9374
9261
  * are included in the replay event before it is finished and sent to Sentry.
9375
9262
  */
9376
9263
  _addPerformanceEntries() {
9377
- // Copy and reset entries before processing
9378
- const entries = [...this.performanceEvents];
9379
- this.performanceEvents = [];
9264
+ const performanceEntries = createPerformanceEntries(this.performanceEntries).concat(this.replayPerformanceEntries);
9265
+
9266
+ this.performanceEntries = [];
9267
+ this.replayPerformanceEntries = [];
9380
9268
 
9381
- return Promise.all(createPerformanceSpans(this, createPerformanceEntries(entries)));
9269
+ return Promise.all(createPerformanceSpans(this, performanceEntries));
9382
9270
  }
9383
9271
 
9384
9272
  /**
@@ -9711,19 +9599,6 @@ function getPrivacyOptions({
9711
9599
  return options;
9712
9600
  }
9713
9601
 
9714
- /**
9715
- * Returns true if we are in the browser.
9716
- */
9717
- function isBrowser() {
9718
- // eslint-disable-next-line no-restricted-globals
9719
- return typeof window !== 'undefined' && (!utils.isNodeEnv() || isElectronNodeRenderer());
9720
- }
9721
-
9722
- // Electron renderers with nodeIntegration enabled are detected as Node.js so we specifically test for them
9723
- function isElectronNodeRenderer() {
9724
- return typeof process !== 'undefined' && (process ).type === 'renderer';
9725
- }
9726
-
9727
9602
  /**
9728
9603
  * Masks an attribute if necessary, otherwise return attribute value as-is.
9729
9604
  */
@@ -9947,7 +9822,7 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
9947
9822
  : `${this._recordingOptions.blockSelector},${MEDIA_SELECTORS}`;
9948
9823
  }
9949
9824
 
9950
- if (this._isInitialized && isBrowser()) {
9825
+ if (this._isInitialized && utils.isBrowser()) {
9951
9826
  throw new Error('Multiple Sentry Session Replay instances are not supported');
9952
9827
  }
9953
9828
 
@@ -9968,7 +9843,7 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
9968
9843
  * Setup and initialize replay container
9969
9844
  */
9970
9845
  setupOnce() {
9971
- if (!isBrowser()) {
9846
+ if (!utils.isBrowser()) {
9972
9847
  return;
9973
9848
  }
9974
9849