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