@newrelic/browser-agent 1.317.0 → 1.318.0-rc.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 (78) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cjs/common/config/configurable.js +1 -1
  3. package/dist/cjs/common/config/init-types.js +2 -0
  4. package/dist/cjs/common/config/init.js +3 -0
  5. package/dist/cjs/common/constants/env.cdn.js +1 -1
  6. package/dist/cjs/common/constants/env.npm.js +1 -1
  7. package/dist/cjs/common/harvest/harvester.js +3 -219
  8. package/dist/cjs/common/harvest/send.js +232 -0
  9. package/dist/cjs/common/harvest/types.js +9 -5
  10. package/dist/cjs/common/session/session-entity.js +8 -8
  11. package/dist/cjs/common/v2/mfe-vitals.js +303 -0
  12. package/dist/cjs/features/generic_events/aggregate/index.js +1 -1
  13. package/dist/cjs/features/generic_events/instrument/index.js +1 -1
  14. package/dist/cjs/features/logging/aggregate/index.js +5 -5
  15. package/dist/cjs/features/page_view_event/aggregate/index.js +65 -30
  16. package/dist/cjs/features/session_replay/aggregate/index.js +32 -28
  17. package/dist/cjs/features/session_trace/aggregate/index.js +29 -27
  18. package/dist/cjs/features/session_trace/constants.js +17 -3
  19. package/dist/cjs/features/utils/aggregate-base.js +2 -0
  20. package/dist/cjs/loaders/api/register.js +27 -4
  21. package/dist/esm/common/config/configurable.js +1 -1
  22. package/dist/esm/common/config/init-types.js +2 -0
  23. package/dist/esm/common/config/init.js +3 -0
  24. package/dist/esm/common/constants/env.cdn.js +1 -1
  25. package/dist/esm/common/constants/env.npm.js +1 -1
  26. package/dist/esm/common/harvest/harvester.js +3 -219
  27. package/dist/esm/common/harvest/send.js +226 -0
  28. package/dist/esm/common/harvest/types.js +9 -5
  29. package/dist/esm/common/session/session-entity.js +9 -9
  30. package/dist/esm/common/v2/mfe-vitals.js +298 -0
  31. package/dist/esm/features/generic_events/aggregate/index.js +1 -1
  32. package/dist/esm/features/generic_events/instrument/index.js +1 -1
  33. package/dist/esm/features/logging/aggregate/index.js +5 -5
  34. package/dist/esm/features/page_view_event/aggregate/index.js +64 -29
  35. package/dist/esm/features/session_replay/aggregate/index.js +32 -28
  36. package/dist/esm/features/session_trace/aggregate/index.js +30 -28
  37. package/dist/esm/features/session_trace/constants.js +16 -2
  38. package/dist/esm/features/utils/aggregate-base.js +2 -0
  39. package/dist/esm/loaders/api/register.js +27 -4
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/dist/types/common/config/init-types.d.ts +6 -0
  42. package/dist/types/common/config/init.d.ts.map +1 -1
  43. package/dist/types/common/harvest/harvester.d.ts +0 -17
  44. package/dist/types/common/harvest/harvester.d.ts.map +1 -1
  45. package/dist/types/common/harvest/send.d.ts +9 -0
  46. package/dist/types/common/harvest/send.d.ts.map +1 -0
  47. package/dist/types/common/harvest/types.d.ts +39 -10
  48. package/dist/types/common/harvest/types.d.ts.map +1 -1
  49. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  50. package/dist/types/common/v2/mfe-vitals.d.ts +14 -0
  51. package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -0
  52. package/dist/types/features/page_view_event/aggregate/index.d.ts +1 -0
  53. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  54. package/dist/types/features/session_replay/aggregate/index.d.ts +2 -2
  55. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  56. package/dist/types/features/session_trace/aggregate/index.d.ts +1 -1
  57. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  58. package/dist/types/features/session_trace/constants.d.ts +17 -0
  59. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  60. package/dist/types/loaders/api/register.d.ts.map +1 -1
  61. package/package.json +2 -2
  62. package/src/common/config/configurable.js +1 -1
  63. package/src/common/config/init-types.js +2 -0
  64. package/src/common/config/init.js +2 -1
  65. package/src/common/harvest/harvester.js +3 -202
  66. package/src/common/harvest/send.js +209 -0
  67. package/src/common/harvest/types.js +9 -5
  68. package/src/common/session/session-entity.js +8 -9
  69. package/src/common/v2/mfe-vitals.js +285 -0
  70. package/src/features/generic_events/aggregate/index.js +1 -1
  71. package/src/features/generic_events/instrument/index.js +1 -1
  72. package/src/features/logging/aggregate/index.js +5 -5
  73. package/src/features/page_view_event/aggregate/index.js +52 -27
  74. package/src/features/session_replay/aggregate/index.js +30 -27
  75. package/src/features/session_trace/aggregate/index.js +31 -28
  76. package/src/features/session_trace/constants.js +7 -1
  77. package/src/features/utils/aggregate-base.js +2 -0
  78. package/src/loaders/api/register.js +25 -4
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.trackMFEVitals = trackMFEVitals;
7
+ var _runtime = require("../constants/runtime");
8
+ var _now = require("../timing/now");
9
+ /**
10
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
11
+ * SPDX-License-Identifier: Apache-2.0
12
+ */
13
+
14
+ /**
15
+ * @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
16
+ */
17
+
18
+ const isObservable = node => {
19
+ try {
20
+ return node?.textContent?.trim() || ['img', 'video', 'canvas', 'svg'].includes(node?.nodeName?.toLowerCase());
21
+ } catch (e) {
22
+ return false;
23
+ }
24
+ };
25
+
26
+ /**
27
+ * Check if node is within a specific MFE
28
+ * @param {Node} node - DOM node to check
29
+ * @param {string} id - MFE ID to match
30
+ * @returns {boolean}
31
+ */
32
+ const isInMFE = (node, id) => {
33
+ if (!node || !id) return false;
34
+ try {
35
+ let curr = node.nodeType === 1 ? node : node.parentElement;
36
+ while (curr?.tagName) {
37
+ if (curr.dataset?.nrMfeId === id) return true;
38
+ curr = curr.parentNode;
39
+ }
40
+ } catch (e) {}
41
+ return false;
42
+ };
43
+
44
+ /**
45
+ * Create mutation observer for MFE nodes
46
+ * @param {string} id - MFE ID to track
47
+ * @param {Function} onMatch - Callback when matching node is *added*
48
+ * @returns {MutationObserver}
49
+ */
50
+ const observeMutations = (id, onMatch) => {
51
+ // Try to find existing MFE root
52
+ const mfeRoot = _runtime.globalScope.document?.querySelector("[data-nr-mfe-id=\"".concat(id, "\"]"));
53
+ let observingRoot = !!mfeRoot;
54
+ const obs = new _runtime.globalScope.MutationObserver(mutations => {
55
+ mutations.forEach(m => {
56
+ m.addedNodes.forEach(node => {
57
+ // Check if this is the MFE root being added
58
+ const elem = node.nodeType === 1 ? node : null;
59
+ if (elem?.dataset?.nrMfeId === id) {
60
+ // Found the root! Lets switch to observing just this subtree for performance reasons
61
+ obs.disconnect();
62
+ obs.observe(elem, {
63
+ childList: true,
64
+ subtree: true
65
+ });
66
+ observingRoot = true;
67
+ }
68
+
69
+ // Only check isInMFE if we're observing the whole document; skip expensive ancestor walk when observing root
70
+ if (isObservable(node) && (observingRoot || isInMFE(node, id))) {
71
+ onMatch(node, obs);
72
+ }
73
+ });
74
+ });
75
+ });
76
+
77
+ // If root exists, observe just that subtree; otherwise observe document to catch when root is added
78
+ obs.observe(mfeRoot || _runtime.globalScope.document, {
79
+ childList: true,
80
+ subtree: true
81
+ });
82
+ return obs;
83
+ };
84
+
85
+ /**
86
+ * Create performance observer for MFE entries
87
+ * @param {Array} observers - Array to track observers
88
+ * @param {Object} config - Observer configuration
89
+ * @param {Function} onEntry - Callback for each entry
90
+ * @returns {PerformanceObserver|null} Observer if successful, null if unsupported
91
+ */
92
+ const observePerformance = (observers, config, onEntry) => {
93
+ try {
94
+ const obs = new _runtime.globalScope.PerformanceObserver(list => {
95
+ list.getEntries().forEach(onEntry);
96
+ });
97
+ obs.observe(config);
98
+ observers.push(obs);
99
+ return obs;
100
+ } catch (e) {
101
+ return null;
102
+ }
103
+ };
104
+
105
+ /**
106
+ * Tracks all Core Web Vitals for a specific MFE.
107
+ * @param {string} id - The MFE ID to track
108
+ * @returns {{fcp: object|null, lcp: object|null, cls: object|null, inp: object|null, disconnect: Function}}
109
+ */
110
+ function trackMFEVitals(id, timings) {
111
+ let fcpObservedAt = null;
112
+ let lcpObservedAt = null;
113
+ const getTimeRelativeToScriptStart = capturedAt => {
114
+ if (capturedAt === null) return null;
115
+ return capturedAt - (timings?.scriptStart || timings?.registeredAt || 0);
116
+ };
117
+ const vitals = {
118
+ fcp: {
119
+ get value() {
120
+ return getTimeRelativeToScriptStart(fcpObservedAt);
121
+ }
122
+ },
123
+ lcp: {
124
+ get value() {
125
+ return getTimeRelativeToScriptStart(lcpObservedAt);
126
+ }
127
+ },
128
+ cls: {
129
+ value: null
130
+ },
131
+ inp: {
132
+ value: null
133
+ },
134
+ disconnect: () => {}
135
+ };
136
+ if (!id || !_runtime.isBrowserScope || !_runtime.globalScope.MutationObserver || !_runtime.globalScope.PerformanceObserver) return vitals;
137
+ const observers = [];
138
+ const populateVitalMinimums = () => {
139
+ fcpObservedAt ??= (0, _now.now)();
140
+ lcpObservedAt ??= (0, _now.now)();
141
+ vitals.cls.value ??= 0;
142
+ };
143
+
144
+ // if the MFE has already rendered something on the page before we could set up listeners, just populate vital minimums immediately
145
+ if (_runtime.globalScope.document?.querySelector("[data-nr-mfe-id=\"".concat(id, "\"]"))) populateVitalMinimums();
146
+
147
+ // Track FCP - first contentful paint
148
+ observeMutations(id, (_, obs) => {
149
+ // An observed "FCP" means _something_ rendered, so at minimum we can populate all the baseline values for the vitals
150
+ populateVitalMinimums();
151
+ obs.disconnect();
152
+ });
153
+
154
+ // Track LCP - largest contentful paint
155
+ let largestSize = 0;
156
+ let resizeObs = null;
157
+ const observedElements = new Set();
158
+ try {
159
+ resizeObs = new _runtime.globalScope.ResizeObserver(entries => {
160
+ entries.forEach(entry => {
161
+ try {
162
+ const size = entry.contentRect.width * entry.contentRect.height;
163
+ if (size > largestSize) {
164
+ largestSize = size;
165
+ lcpObservedAt = (0, _now.now)();
166
+ }
167
+ resizeObs.unobserve(entry.target);
168
+ } catch (e) {
169
+ // Element may be detached from DOM
170
+ }
171
+ });
172
+ });
173
+ } catch (e) {
174
+ // ResizeObserver not supported
175
+ }
176
+ const lcpObs = observeMutations(id, node => {
177
+ // an observed "LCP" means _something_ rendered, so at minimum we can make sure all the baseline values are populated for the vitals
178
+ populateVitalMinimums();
179
+ if (resizeObs) {
180
+ try {
181
+ const elem = node.nodeType === 1 ? node : node.parentElement;
182
+ if (elem && !observedElements.has(elem)) {
183
+ observedElements.add(elem);
184
+
185
+ // For media elements, wait for content to load before observing size
186
+ if (elem.tagName === 'IMG') {
187
+ if (elem.complete) {
188
+ resizeObs.observe(elem);
189
+ } else {
190
+ elem.addEventListener('load', () => {
191
+ resizeObs.observe(elem);
192
+ }, {
193
+ once: true
194
+ });
195
+ }
196
+ } else if (elem.tagName === 'VIDEO') {
197
+ // For video, wait for first frame (HAVE_CURRENT_DATA = 2)
198
+ if (elem.readyState >= 2) {
199
+ resizeObs.observe(elem);
200
+ } else {
201
+ elem.addEventListener('loadeddata', () => {
202
+ resizeObs.observe(elem);
203
+ }, {
204
+ once: true
205
+ });
206
+ }
207
+ } else {
208
+ // For other elements, observe immediately
209
+ resizeObs.observe(elem);
210
+ }
211
+ }
212
+ } catch (e) {
213
+ // Element may not be observable
214
+ }
215
+ }
216
+ });
217
+ if (resizeObs) observers.push(resizeObs);
218
+ observers.push(lcpObs);
219
+
220
+ // Track CLS - cumulative layout shift
221
+ // Initialize CLS to 0 if browser supports it
222
+ observePerformance(observers, {
223
+ type: 'layout-shift',
224
+ buffered: true
225
+ }, entry => {
226
+ if (entry.hadRecentInput) return;
227
+ (entry.sources || []).some(source => {
228
+ if (isInMFE(source.node, id)) {
229
+ // an observed "CLS" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
230
+ populateVitalMinimums();
231
+ vitals.cls.value += entry.value;
232
+ return true;
233
+ }
234
+ return false;
235
+ });
236
+ });
237
+
238
+ // Track INP - interaction to next paint
239
+ observePerformance(observers, {
240
+ type: 'event',
241
+ buffered: true,
242
+ durationThreshold: 40
243
+ }, entry => {
244
+ if (!entry.interactionId || !isInMFE(entry.target, id)) return;
245
+ if (vitals.inp.value === null || entry.duration > vitals.inp.value) {
246
+ // an observed "INP" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
247
+ populateVitalMinimums();
248
+ vitals.inp.value = entry.duration;
249
+ }
250
+ });
251
+ const interactionEvents = ['pointerdown', 'keydown'];
252
+ const disconnectInteractionListeners = () => {
253
+ interactionEvents.forEach(type => {
254
+ _runtime.globalScope.removeEventListener(type, handleInteraction, {
255
+ passive: true
256
+ });
257
+ });
258
+ };
259
+
260
+ // Disconnect all observers
261
+ vitals.disconnect = () => {
262
+ // Disconnect all observers
263
+ observers.forEach(obs => {
264
+ try {
265
+ obs?.disconnect();
266
+ } catch (e) {
267
+ // Observer may already be disconnected
268
+ }
269
+ });
270
+ disconnectInteractionListeners();
271
+ };
272
+
273
+ // Auto-disconnect LCP observer on user interaction (per Web Vitals spec)
274
+ // CLS and INP continue tracking until visibility change or deregister
275
+ const disconnectLCP = () => {
276
+ try {
277
+ lcpObs?.disconnect();
278
+ resizeObs?.disconnect();
279
+ } catch (e) {
280
+ // Observer may already be disconnected
281
+ }
282
+ };
283
+ const handleInteraction = event => {
284
+ if (!isInMFE(event?.target, id)) return;
285
+ disconnectLCP();
286
+ disconnectInteractionListeners();
287
+ };
288
+ interactionEvents.forEach(type => {
289
+ _runtime.globalScope.addEventListener(type, handleInteraction, {
290
+ passive: true
291
+ });
292
+ })
293
+
294
+ // Disconnect all observers on visibility change or page unload
295
+ ;
296
+ ['visibilitychange', 'pagehide'].forEach(type => {
297
+ _runtime.globalScope.addEventListener(type, vitals.disconnect, {
298
+ once: true,
299
+ passive: true
300
+ });
301
+ });
302
+ return vitals;
303
+ }
@@ -261,7 +261,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
261
261
  };
262
262
  this.addEvent(event, target);
263
263
  }, this.featureName, this.ee);
264
- if (agentRef.init.feature_flags.includes('websockets')) {
264
+ if (agentRef.init.feature_flags.includes('websockets') || agentRef.init.web_sockets?.enabled) {
265
265
  (0, _registerHandler.registerHandler)('ws-complete', nrData => {
266
266
  const event = {
267
267
  ...nrData,
@@ -30,7 +30,7 @@ class Instrument extends _instrumentBase.InstrumentBase {
30
30
  static featureName = _constants.FEATURE_NAME;
31
31
  constructor(agentRef) {
32
32
  super(agentRef, _constants.FEATURE_NAME);
33
- const websocketsEnabled = agentRef.init.feature_flags.includes('websockets');
33
+ const websocketsEnabled = agentRef.init.feature_flags.includes('websockets') || agentRef.init.web_sockets?.enabled;
34
34
  const securityPolicyViolationEnabled = !agentRef.init.feature_flags.includes('no_spv');
35
35
 
36
36
  /** config values that gate whether the generic events aggregator should be imported at all */
@@ -61,7 +61,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
61
61
  }; // likewise, don't want to overwrite the mode if it was set already
62
62
  const session = this.agentRef.runtime.session;
63
63
  if ((0, _featureGates.canEnableSessionTracking)(agentRef.init) && session) {
64
- if (session.isNew) this.#syncWithSessionManager();else updateLocalLoggingMode(session.state.loggingMode, session.state.logApiMode);
64
+ if (session.state.loggingMode === null || session.state.logApiMode === null) this.#writeToStorage(this.loggingMode);else updateLocalLoggingMode(session.state.loggingMode, session.state.logApiMode);
65
65
  }
66
66
  if (this.loggingMode.auto === _constants.LOGGING_MODE.OFF && this.loggingMode.api === _constants.LOGGING_MODE.OFF) {
67
67
  this.blocked = true;
@@ -169,13 +169,13 @@ class Aggregate extends _aggregateBase.AggregateBase {
169
169
  auto: _constants.LOGGING_MODE.OFF,
170
170
  api: _constants.LOGGING_MODE.OFF
171
171
  };
172
- this.#syncWithSessionManager();
172
+ this.#writeToStorage(this.loggingMode);
173
173
  this.deregisterDrain();
174
174
  }
175
- #syncWithSessionManager() {
175
+ #writeToStorage(logModes) {
176
176
  this.agentRef.runtime.session?.write({
177
- loggingMode: this.loggingMode.auto,
178
- logApiMode: this.loggingMode.api
177
+ loggingMode: logModes.auto,
178
+ logApiMode: logModes.api
179
179
  });
180
180
  }
181
181
  }
@@ -17,8 +17,9 @@ var _firstPaint = require("../../../common/vitals/first-paint");
17
17
  var _timeToFirstByte = require("../../../common/vitals/time-to-first-byte");
18
18
  var _now = require("../../../common/timing/now");
19
19
  var _timeKeeper = require("../../../common/timing/time-keeper");
20
+ var _send = require("../../../common/harvest/send");
21
+ var _featureGates = require("../../utils/feature-gates");
20
22
  var _obfuscate = require("../../../common/util/obfuscate");
21
- var _harvester = require("../../../common/harvest/harvester");
22
23
  var _features = require("../../../loaders/features/features");
23
24
  var _submitData = require("../../../common/util/submit-data");
24
25
  var _webdriverDetection = require("../../../common/util/webdriver-detection");
@@ -33,6 +34,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
33
34
  static featureName = CONSTANTS.FEATURE_NAME;
34
35
  constructor(agentRef) {
35
36
  super(agentRef, CONSTANTS.FEATURE_NAME);
37
+ this.isSessionTrackingEnabled = (0, _featureGates.canEnableSessionTracking)(agentRef.init) && !!agentRef.runtime.session;
36
38
  this.sentRum = false; // flag to facilitate calling sendRum() once externally (by the consent API in agent-session.js)
37
39
 
38
40
  this.timeToFirstByte = 0;
@@ -44,6 +46,19 @@ class Aggregate extends _aggregateBase.AggregateBase {
44
46
  this.obfuscator = new _obfuscate.Obfuscator(agentRef, _events.EVENT_TYPES.PVE);
45
47
  agentRef.runtime.timeKeeper = new _timeKeeper.TimeKeeper(agentRef.runtime.session);
46
48
  if (_runtime.isBrowserScope) {
49
+ const cached = this.isSessionTrackingEnabled && agentRef.runtime.session?.state.cachedRumResponse;
50
+ if (cached) {
51
+ const {
52
+ app: cachedApp,
53
+ ...cachedFlags
54
+ } = cached;
55
+
56
+ // set the agent runtime objects that require the rum response or entity guid
57
+ if (!Object.keys(this.agentRef.runtime.appMetadata).length) agentRef.runtime.appMetadata = cachedApp;
58
+ this.drain();
59
+ agentRef.runtime.harvester.startTimer();
60
+ (0, _featureFlags.activateFeatures)(cachedFlags, agentRef);
61
+ }
47
62
  _timeToFirstByte.timeToFirstByte.subscribe(({
48
63
  value,
49
64
  attrs
@@ -88,8 +103,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
88
103
  ua: info.userAttributes,
89
104
  at: info.atts
90
105
  };
91
- if (this.agentRef.runtime.session) queryParameters.fsh = Number(this.agentRef.runtime.session.isNew); // "first session harvest" aka RUM request or PageView event of a session
92
-
106
+ if (this.agentRef.runtime.session) queryParameters.fsh = Number(!this.agentRef.runtime.session.state.cachedRumResponse);
93
107
  let body = this.obfuscator.traverseAndObfuscateEvents({
94
108
  ja: {
95
109
  ...customAttributes,
@@ -143,6 +157,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
143
157
  }) {
144
158
  const rumEndTime = (0, _now.now)();
145
159
  let app, flags;
160
+ const hasCachedRumResponse = !!this.agentRef.runtime.session?.state.cachedRumResponse;
146
161
  try {
147
162
  ({
148
163
  app,
@@ -152,10 +167,20 @@ class Aggregate extends _aggregateBase.AggregateBase {
152
167
  // wont set entity stuff here, if main agent will later abort, if registered agent, nothing will happen
153
168
  (0, _console.warn)(53, error);
154
169
  }
170
+ const shouldCacheResponse = this.isSessionTrackingEnabled && !hasCachedRumResponse && !!app;
171
+ if (hasCachedRumResponse) {
172
+ let {
173
+ app: cachedApp,
174
+ ...cachedFlags
175
+ } = this.agentRef.runtime.session.state.cachedRumResponse;
176
+ app ??= cachedApp;
177
+ flags = cachedFlags;
178
+ }
155
179
  super.postHarvestCleanup({
156
180
  sent,
157
- retry
181
+ retry: retry && !hasCachedRumResponse
158
182
  }); // this will set isRetrying & re-buffer the body if request is to be retried
183
+
159
184
  if (this.isRetrying && this.retries++ < 1) {
160
185
  // Only retry once
161
186
  setTimeout(() => this.agentRef.runtime.harvester.triggerHarvestFor(this, {
@@ -165,7 +190,6 @@ class Aggregate extends _aggregateBase.AggregateBase {
165
190
  }
166
191
  if (status >= 400 || status === 0) {
167
192
  (0, _console.warn)(18, status);
168
- this.blocked = true;
169
193
 
170
194
  // Get estimated payload size of our backlog
171
195
  const textEncoder = new TextEncoder();
@@ -176,15 +200,24 @@ class Aggregate extends _aggregateBase.AggregateBase {
176
200
  }, 0);
177
201
  const BCSError = 'BCS/Error/';
178
202
  // Send SMs about failed RUM request
179
- const body = {
180
- sm: [{
181
- params: {
182
- name: BCSError + status
183
- },
184
- stats: {
185
- c: 1
186
- }
187
- }, {
203
+ const sm = [{
204
+ params: {
205
+ name: BCSError + status
206
+ },
207
+ stats: {
208
+ c: 1
209
+ }
210
+ }, {
211
+ params: {
212
+ name: BCSError + 'Duration/Ms'
213
+ },
214
+ stats: {
215
+ c: 1,
216
+ t: rumEndTime - this.rumStartTime
217
+ }
218
+ }];
219
+ if (!hasCachedRumResponse) {
220
+ sm.push({
188
221
  params: {
189
222
  name: BCSError + 'Dropped/Bytes'
190
223
  },
@@ -192,28 +225,30 @@ class Aggregate extends _aggregateBase.AggregateBase {
192
225
  c: 1,
193
226
  t: payloadSize
194
227
  }
195
- }, {
196
- params: {
197
- name: BCSError + 'Duration/Ms'
198
- },
199
- stats: {
200
- c: 1,
201
- t: rumEndTime - this.rumStartTime
202
- }
203
- }]
204
- };
205
- (0, _harvester.send)(this.agentRef, {
228
+ });
229
+ }
230
+ (0, _send.send)(this.agentRef, {
206
231
  endpoint: _features.FEATURE_TO_ENDPOINT[_features.FEATURE_NAMES.metrics],
207
232
  payload: {
208
- body
233
+ body: {
234
+ sm
235
+ }
209
236
  },
210
237
  submitMethod: (0, _submitData.getSubmitMethod)(),
211
238
  featureName: _features.FEATURE_NAMES.metrics
212
239
  });
213
-
214
- // Adding retry logic for the rum call will be a separate change; this.blocked will need to be changed since that prevents another triggerHarvestFor()
215
- this.ee.abort();
216
- return;
240
+ if (!hasCachedRumResponse) {
241
+ this.blocked = true;
242
+ this.ee.abort();
243
+ return;
244
+ }
245
+ } else if (shouldCacheResponse) {
246
+ this.agentRef.runtime.session.write({
247
+ cachedRumResponse: {
248
+ app,
249
+ ...flags
250
+ }
251
+ });
217
252
  }
218
253
  try {
219
254
  const wasReady = this.agentRef.runtime.timeKeeper.ready;
@@ -30,7 +30,7 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
30
30
  */
31
31
  class Aggregate extends _aggregateBase.AggregateBase {
32
32
  static featureName = _constants.FEATURE_NAME;
33
- mode = _constants2.MODE.OFF;
33
+ mode = null;
34
34
 
35
35
  // pass the recorder into the aggregator
36
36
  constructor(agentRef, args) {
@@ -75,7 +75,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
75
75
  if (!this.recorder) return;
76
76
  // if the mode changed on a different tab, it needs to update this instance to match
77
77
  this.mode = agentRef.runtime.session.state.sessionReplayMode;
78
- if (!this.initialized || this.mode === _constants2.MODE.OFF) return;
78
+ if (!this.initialized || this.mode === _constants2.MODE.OFF || !this.mode) return;
79
79
  this.recorder?.startRecording(_constants.TRIGGERS.RESUME, this.mode);
80
80
  });
81
81
  this.ee.on(_constants2.SESSION_EVENTS.UPDATE, (type, data) => {
@@ -102,6 +102,10 @@ class Aggregate extends _aggregateBase.AggregateBase {
102
102
  this.waitForFlags(['srs', 'sr']).then(([srMode, entitled]) => {
103
103
  this.entitled = !!entitled;
104
104
  if (!this.entitled) {
105
+ this.mode = _constants2.MODE.OFF;
106
+ this.#writeToStorage({
107
+ sessionReplayMode: this.mode
108
+ });
105
109
  this.deregisterDrain();
106
110
  if (this.agentRef.runtime.isRecording) {
107
111
  this.abort(_constants.ABORT_REASONS.ENTITLEMENTS);
@@ -113,7 +117,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
113
117
  this.drain();
114
118
  });
115
119
  }).then(() => {
116
- if (this.mode === _constants2.MODE.OFF) {
120
+ if (!this.mode) {
117
121
  this.recorder?.stopRecording(); // stop any conservative preload recording launched by instrument
118
122
  while (this.recorder?.getEvents().events.length) this.recorder?.clearBuffer?.();
119
123
  }
@@ -146,7 +150,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
146
150
  // if the error was noticed AFTER the recorder was already imported....
147
151
  if (this.recorder && this.initialized) {
148
152
  if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(_constants.TRIGGERS.SWITCH_TO_FULL, this.mode); // off --> full
149
- this.syncWithSessionManager({
153
+ this.#writeToStorage({
150
154
  sessionReplayMode: this.mode
151
155
  });
152
156
  } else {
@@ -178,35 +182,35 @@ class Aggregate extends _aggregateBase.AggregateBase {
178
182
  this.timeKeeper = timeKeeper;
179
183
  if (this.recorder?.trigger === _constants.TRIGGERS.API && this.agentRef.runtime.isRecording) {
180
184
  this.mode = _constants2.MODE.FULL;
181
- } else if (!session.isNew && !ignoreSession) {
185
+ } else if (session.state.sessionReplayMode !== null && !ignoreSession) {
182
186
  // inherit the mode of the existing session
183
187
  this.mode = session.state.sessionReplayMode;
184
188
  } else {
185
189
  // The session is new... determine the mode the new session should start in
186
190
  this.mode = srMode;
187
191
  }
188
- // If off, then don't record (early return)
189
- if (this.mode === _constants2.MODE.OFF) return;
190
- try {
191
- /** will return a recorder instance if already imported, otherwise, will fetch the recorder and initialize it */
192
- this.recorder ??= await this.instrumentClass.importRecorder();
193
- } catch (err) {
194
- /** if the recorder fails to import, abort the feature */
195
- return this.abort(_constants.ABORT_REASONS.IMPORT, err);
196
- }
192
+ if (this.mode !== _constants2.MODE.OFF) {
193
+ try {
194
+ /** will return a recorder instance if already imported, otherwise, will fetch the recorder and initialize it */
195
+ this.recorder ??= await this.instrumentClass.importRecorder();
196
+ } catch (err) {
197
+ /** if the recorder fails to import, abort the feature */
198
+ return this.abort(_constants.ABORT_REASONS.IMPORT, err);
199
+ }
197
200
 
198
- // If an error was noticed before the mode could be set (like in the early lifecycle of the page), immediately set to FULL mode
199
- if (this.mode === _constants2.MODE.ERROR && this.instrumentClass.errorNoticed) {
200
- this.mode = _constants2.MODE.FULL;
201
- }
201
+ // If an error was noticed before the mode could be set (like in the early lifecycle of the page), immediately set to FULL mode
202
+ if (this.mode === _constants2.MODE.ERROR && this.instrumentClass.errorNoticed) {
203
+ this.mode = _constants2.MODE.FULL;
204
+ }
202
205
 
203
- // FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
204
- // ERROR mode will do this until an error is thrown, and then switch into FULL mode.
205
- // The makeHarvestPayload should ensure that no payload is returned if we're not in FULL mode...
206
+ // FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
207
+ // ERROR mode will do this until an error is thrown, and then switch into FULL mode.
208
+ // The makeHarvestPayload should ensure that no payload is returned if we're not in FULL mode...
206
209
 
207
- await this.prepUtils();
208
- if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(trigger, this.mode);
209
- this.syncWithSessionManager({
210
+ await this.prepUtils();
211
+ if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(trigger, this.mode);
212
+ }
213
+ this.#writeToStorage({
210
214
  sessionReplayMode: this.mode
211
215
  });
212
216
  }
@@ -253,7 +257,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
253
257
  this.abort(_constants.ABORT_REASONS.TOO_BIG, len);
254
258
  return;
255
259
  }
256
- if (!this.agentRef.runtime.session.state.sessionReplaySentFirstChunk) this.syncWithSessionManager({
260
+ if (!this.agentRef.runtime.session.state.sessionReplaySentFirstChunk) this.#writeToStorage({
257
261
  sessionReplaySentFirstChunk: true
258
262
  });
259
263
  this.recorder.clearBuffer();
@@ -382,7 +386,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
382
386
  if (forceHarvest) this.agentRef.runtime.harvester.triggerHarvestFor(this);
383
387
  this.mode = _constants2.MODE.OFF;
384
388
  this.recorder?.stopRecording?.();
385
- this.syncWithSessionManager({
389
+ this.#writeToStorage({
386
390
  sessionReplayMode: this.mode
387
391
  });
388
392
  }
@@ -394,13 +398,13 @@ class Aggregate extends _aggregateBase.AggregateBase {
394
398
  this.blocked = true;
395
399
  this.mode = _constants2.MODE.OFF;
396
400
  this.recorder?.stopRecording?.();
397
- this.syncWithSessionManager({
401
+ this.#writeToStorage({
398
402
  sessionReplayMode: this.mode
399
403
  });
400
404
  this.recorder?.clearTimestamps?.();
401
405
  while (this.recorder?.getEvents().events.length) this.recorder?.clearBuffer?.();
402
406
  }
403
- syncWithSessionManager(state = {}) {
407
+ #writeToStorage(state = {}) {
404
408
  if (this.isSessionTrackingEnabled) {
405
409
  this.agentRef.runtime.session.write(state);
406
410
  }