@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
@@ -15,8 +15,9 @@ import { firstPaint } from '../../../common/vitals/first-paint'
15
15
  import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte'
16
16
  import { now } from '../../../common/timing/now'
17
17
  import { TimeKeeper } from '../../../common/timing/time-keeper'
18
+ import { send } from '../../../common/harvest/send'
19
+ import { canEnableSessionTracking } from '../../utils/feature-gates'
18
20
  import { Obfuscator } from '../../../common/util/obfuscate'
19
- import { send } from '../../../common/harvest/harvester'
20
21
  import { FEATURE_NAMES, FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
21
22
  import { getSubmitMethod } from '../../../common/util/submit-data'
22
23
  import { webdriverDetected } from '../../../common/util/webdriver-detection'
@@ -28,6 +29,7 @@ export class Aggregate extends AggregateBase {
28
29
  constructor (agentRef) {
29
30
  super(agentRef, CONSTANTS.FEATURE_NAME)
30
31
 
32
+ this.isSessionTrackingEnabled = canEnableSessionTracking(agentRef.init) && !!agentRef.runtime.session
31
33
  this.sentRum = false // flag to facilitate calling sendRum() once externally (by the consent API in agent-session.js)
32
34
 
33
35
  this.timeToFirstByte = 0
@@ -41,6 +43,17 @@ export class Aggregate extends AggregateBase {
41
43
  agentRef.runtime.timeKeeper = new TimeKeeper(agentRef.runtime.session)
42
44
 
43
45
  if (isBrowserScope) {
46
+ const cached = this.isSessionTrackingEnabled && agentRef.runtime.session?.state.cachedRumResponse
47
+ if (cached) {
48
+ const { app: cachedApp, ...cachedFlags } = cached
49
+
50
+ // set the agent runtime objects that require the rum response or entity guid
51
+ if (!Object.keys(this.agentRef.runtime.appMetadata).length) agentRef.runtime.appMetadata = cachedApp
52
+ this.drain()
53
+ agentRef.runtime.harvester.startTimer()
54
+ activateFeatures(cachedFlags, agentRef)
55
+ }
56
+
44
57
  timeToFirstByte.subscribe(({ value, attrs }) => {
45
58
  const navEntry = attrs.navigationEntry
46
59
  this.timeToFirstByte = Math.max(value, this.timeToFirstByte)
@@ -85,7 +98,7 @@ export class Aggregate extends AggregateBase {
85
98
  at: info.atts
86
99
  }
87
100
 
88
- 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
101
+ if (this.agentRef.runtime.session) queryParameters.fsh = Number(!this.agentRef.runtime.session.state.cachedRumResponse)
89
102
 
90
103
  let body = this.obfuscator.traverseAndObfuscateEvents({ ja: { ...customAttributes, webdriverDetected } })
91
104
 
@@ -131,6 +144,7 @@ export class Aggregate extends AggregateBase {
131
144
  postHarvestCleanup ({ sent, status, responseText, xhr, retry }) {
132
145
  const rumEndTime = now()
133
146
  let app, flags
147
+ const hasCachedRumResponse = !!this.agentRef.runtime.session?.state.cachedRumResponse
134
148
  try {
135
149
  ({ app, ...flags } = JSON.parse(responseText))
136
150
  } catch (error) {
@@ -138,16 +152,24 @@ export class Aggregate extends AggregateBase {
138
152
  warn(53, error)
139
153
  }
140
154
 
141
- super.postHarvestCleanup({ sent, retry }) // this will set isRetrying & re-buffer the body if request is to be retried
155
+ const shouldCacheResponse = this.isSessionTrackingEnabled && !hasCachedRumResponse && !!app
156
+ if (hasCachedRumResponse) {
157
+ let { app: cachedApp, ...cachedFlags } = this.agentRef.runtime.session.state.cachedRumResponse
158
+ app ??= cachedApp
159
+ flags = cachedFlags
160
+ }
161
+
162
+ super.postHarvestCleanup({ sent, retry: retry && !hasCachedRumResponse }) // this will set isRetrying & re-buffer the body if request is to be retried
163
+
142
164
  if (this.isRetrying && this.retries++ < 1) { // Only retry once
143
165
  setTimeout(() => this.agentRef.runtime.harvester.triggerHarvestFor(this, {
144
166
  sendEmptyBody: true
145
167
  }), 5000) // Retry sending the RUM event after 5 seconds
146
168
  return
147
169
  }
170
+
148
171
  if (status >= 400 || status === 0) {
149
172
  warn(18, status)
150
- this.blocked = true
151
173
 
152
174
  // Get estimated payload size of our backlog
153
175
  const textEncoder = new TextEncoder()
@@ -159,16 +181,24 @@ export class Aggregate extends AggregateBase {
159
181
  }, 0)
160
182
  const BCSError = 'BCS/Error/'
161
183
  // Send SMs about failed RUM request
162
- const body = {
163
- sm: [{
164
- params: {
165
- name: BCSError + status
166
- },
167
- stats: {
168
- c: 1
169
- }
184
+ const sm = [{
185
+ params: {
186
+ name: BCSError + status
170
187
  },
171
- {
188
+ stats: {
189
+ c: 1
190
+ }
191
+ }, {
192
+ params: {
193
+ name: BCSError + 'Duration/Ms'
194
+ },
195
+ stats: {
196
+ c: 1,
197
+ t: rumEndTime - this.rumStartTime
198
+ }
199
+ }]
200
+ if (!hasCachedRumResponse) {
201
+ sm.push({
172
202
  params: {
173
203
  name: BCSError + 'Dropped/Bytes'
174
204
  },
@@ -176,28 +206,23 @@ export class Aggregate extends AggregateBase {
176
206
  c: 1,
177
207
  t: payloadSize
178
208
  }
179
- },
180
- {
181
- params: {
182
- name: BCSError + 'Duration/Ms'
183
- },
184
- stats: {
185
- c: 1,
186
- t: rumEndTime - this.rumStartTime
187
- }
188
- }]
209
+ })
189
210
  }
190
211
 
191
212
  send(this.agentRef, {
192
213
  endpoint: FEATURE_TO_ENDPOINT[FEATURE_NAMES.metrics],
193
- payload: { body },
214
+ payload: { body: { sm } },
194
215
  submitMethod: getSubmitMethod(),
195
216
  featureName: FEATURE_NAMES.metrics
196
217
  })
197
218
 
198
- // Adding retry logic for the rum call will be a separate change; this.blocked will need to be changed since that prevents another triggerHarvestFor()
199
- this.ee.abort()
200
- return
219
+ if (!hasCachedRumResponse) {
220
+ this.blocked = true
221
+ this.ee.abort()
222
+ return
223
+ }
224
+ } else if (shouldCacheResponse) {
225
+ this.agentRef.runtime.session.write({ cachedRumResponse: { app, ...flags } })
201
226
  }
202
227
 
203
228
  try {
@@ -27,7 +27,7 @@ import { Obfuscator } from '../../../common/util/obfuscate'
27
27
 
28
28
  export class Aggregate extends AggregateBase {
29
29
  static featureName = FEATURE_NAME
30
- mode = MODE.OFF
30
+ mode = null
31
31
 
32
32
  // pass the recorder into the aggregator
33
33
  constructor (agentRef, args) {
@@ -74,7 +74,7 @@ export class Aggregate extends AggregateBase {
74
74
  if (!this.recorder) return
75
75
  // if the mode changed on a different tab, it needs to update this instance to match
76
76
  this.mode = agentRef.runtime.session.state.sessionReplayMode
77
- if (!this.initialized || this.mode === MODE.OFF) return
77
+ if (!this.initialized || this.mode === MODE.OFF || !this.mode) return
78
78
  this.recorder?.startRecording(TRIGGERS.RESUME, this.mode)
79
79
  })
80
80
 
@@ -97,6 +97,8 @@ export class Aggregate extends AggregateBase {
97
97
  this.waitForFlags(['srs', 'sr']).then(([srMode, entitled]) => {
98
98
  this.entitled = !!entitled
99
99
  if (!this.entitled) {
100
+ this.mode = MODE.OFF
101
+ this.#writeToStorage({ sessionReplayMode: this.mode })
100
102
  this.deregisterDrain()
101
103
  if (this.agentRef.runtime.isRecording) {
102
104
  this.abort(ABORT_REASONS.ENTITLEMENTS)
@@ -106,7 +108,7 @@ export class Aggregate extends AggregateBase {
106
108
  }
107
109
  this.initializeRecording(srMode).then(() => { this.drain() })
108
110
  }).then(() => {
109
- if (this.mode === MODE.OFF) {
111
+ if (!this.mode) {
110
112
  this.recorder?.stopRecording() // stop any conservative preload recording launched by instrument
111
113
  while (this.recorder?.getEvents().events.length) this.recorder?.clearBuffer?.()
112
114
  }
@@ -143,7 +145,7 @@ export class Aggregate extends AggregateBase {
143
145
  // if the error was noticed AFTER the recorder was already imported....
144
146
  if (this.recorder && this.initialized) {
145
147
  if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(TRIGGERS.SWITCH_TO_FULL, this.mode) // off --> full
146
- this.syncWithSessionManager({ sessionReplayMode: this.mode })
148
+ this.#writeToStorage({ sessionReplayMode: this.mode })
147
149
  } else {
148
150
  this.initializeRecording(MODE.FULL, true, TRIGGERS.SWITCH_TO_FULL)
149
151
  }
@@ -171,35 +173,36 @@ export class Aggregate extends AggregateBase {
171
173
 
172
174
  if (this.recorder?.trigger === TRIGGERS.API && this.agentRef.runtime.isRecording) {
173
175
  this.mode = MODE.FULL
174
- } else if (!session.isNew && !ignoreSession) { // inherit the mode of the existing session
176
+ } else if (session.state.sessionReplayMode !== null && !ignoreSession) { // inherit the mode of the existing session
175
177
  this.mode = session.state.sessionReplayMode
176
178
  } else {
177
179
  // The session is new... determine the mode the new session should start in
178
180
  this.mode = srMode
179
181
  }
180
- // If off, then don't record (early return)
181
- if (this.mode === MODE.OFF) return
182
182
 
183
- try {
184
- /** will return a recorder instance if already imported, otherwise, will fetch the recorder and initialize it */
185
- this.recorder ??= await this.instrumentClass.importRecorder()
186
- } catch (err) {
187
- /** if the recorder fails to import, abort the feature */
188
- return this.abort(ABORT_REASONS.IMPORT, err)
189
- }
190
-
191
- // If an error was noticed before the mode could be set (like in the early lifecycle of the page), immediately set to FULL mode
192
- if (this.mode === MODE.ERROR && this.instrumentClass.errorNoticed) { this.mode = MODE.FULL }
183
+ if (this.mode !== MODE.OFF) {
184
+ try {
185
+ /** will return a recorder instance if already imported, otherwise, will fetch the recorder and initialize it */
186
+ this.recorder ??= await this.instrumentClass.importRecorder()
187
+ } catch (err) {
188
+ /** if the recorder fails to import, abort the feature */
189
+ return this.abort(ABORT_REASONS.IMPORT, err)
190
+ }
193
191
 
194
- // FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
195
- // ERROR mode will do this until an error is thrown, and then switch into FULL mode.
196
- // The makeHarvestPayload should ensure that no payload is returned if we're not in FULL mode...
192
+ // If an error was noticed before the mode could be set (like in the early lifecycle of the page), immediately set to FULL mode
193
+ if (this.mode === MODE.ERROR && this.instrumentClass.errorNoticed) {
194
+ this.mode = MODE.FULL
195
+ }
197
196
 
198
- await this.prepUtils()
197
+ // FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
198
+ // ERROR mode will do this until an error is thrown, and then switch into FULL mode.
199
+ // The makeHarvestPayload should ensure that no payload is returned if we're not in FULL mode...
199
200
 
200
- if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(trigger, this.mode)
201
+ await this.prepUtils()
201
202
 
202
- this.syncWithSessionManager({ sessionReplayMode: this.mode })
203
+ if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(trigger, this.mode)
204
+ }
205
+ this.#writeToStorage({ sessionReplayMode: this.mode })
203
206
  }
204
207
 
205
208
  async prepUtils () {
@@ -246,7 +249,7 @@ export class Aggregate extends AggregateBase {
246
249
  return
247
250
  }
248
251
 
249
- if (!this.agentRef.runtime.session.state.sessionReplaySentFirstChunk) this.syncWithSessionManager({ sessionReplaySentFirstChunk: true })
252
+ if (!this.agentRef.runtime.session.state.sessionReplaySentFirstChunk) this.#writeToStorage({ sessionReplaySentFirstChunk: true })
250
253
  this.recorder.clearBuffer()
251
254
 
252
255
  if (!this.agentRef.runtime.session.state.traceHarvestStarted) {
@@ -370,7 +373,7 @@ export class Aggregate extends AggregateBase {
370
373
  if (forceHarvest) this.agentRef.runtime.harvester.triggerHarvestFor(this)
371
374
  this.mode = MODE.OFF
372
375
  this.recorder?.stopRecording?.()
373
- this.syncWithSessionManager({ sessionReplayMode: this.mode })
376
+ this.#writeToStorage({ sessionReplayMode: this.mode })
374
377
  }
375
378
 
376
379
  /** Abort the feature, once aborted it will not resume */
@@ -380,12 +383,12 @@ export class Aggregate extends AggregateBase {
380
383
  this.blocked = true
381
384
  this.mode = MODE.OFF
382
385
  this.recorder?.stopRecording?.()
383
- this.syncWithSessionManager({ sessionReplayMode: this.mode })
386
+ this.#writeToStorage({ sessionReplayMode: this.mode })
384
387
  this.recorder?.clearTimestamps?.()
385
388
  while (this.recorder?.getEvents().events.length) this.recorder?.clearBuffer?.()
386
389
  }
387
390
 
388
- syncWithSessionManager (state = {}) {
391
+ #writeToStorage (state = {}) {
389
392
  if (this.isSessionTrackingEnabled) {
390
393
  this.agentRef.runtime.session.write(state)
391
394
  }
@@ -3,7 +3,7 @@
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
  import { registerHandler } from '../../../common/event-emitter/register-handler'
6
- import { FEATURE_NAME } from '../constants'
6
+ import { ABORT_REASONS, FEATURE_NAME } from '../constants'
7
7
  import { AggregateBase } from '../../utils/aggregate-base'
8
8
  import { TraceStorage } from './trace/storage'
9
9
  import { obj as encodeObj } from '../../../common/url/encode'
@@ -43,8 +43,10 @@ export class Aggregate extends AggregateBase {
43
43
  /** Sets up event listeners, and initializes this module to run in the correct "mode". Can be triggered from a few places, but makes an effort to only set up listeners once */
44
44
  initialize (stMode, stEntitled, ignoreSession) {
45
45
  this.entitled ??= stEntitled
46
- if (!this.entitled) this.blocked = true
47
- if (this.blocked) return this.deregisterDrain()
46
+ if (!this.entitled) {
47
+ this.abort(ABORT_REASONS.ENTITLEMENTS)
48
+ return this.deregisterDrain()
49
+ }
48
50
  this.timeKeeper ??= this.agentRef.runtime.timeKeeper
49
51
 
50
52
  if (!this.initialized) {
@@ -55,7 +57,7 @@ export class Aggregate extends AggregateBase {
55
57
  // The SessionEntity class can emit a message indicating the session was cleared and reset (expiry, inactivity). This feature must abort and never resume if that occurs.
56
58
  this.ee.on(SESSION_EVENTS.RESET, () => {
57
59
  if (this.blocked) return
58
- this.abort(1)
60
+ this.abort(ABORT_REASONS.RESET)
59
61
  })
60
62
  // The SessionEntity can have updates (locally or across tabs for SR mode changes), (across tabs for ST mode changes).
61
63
  // Those updates should be sync'd here to ensure this page also honors the mode after initialization
@@ -64,7 +66,7 @@ export class Aggregate extends AggregateBase {
64
66
  // this will only have an effect if ST is NOT already in full mode
65
67
  if (this.mode !== MODE.FULL && (sessionState.sessionReplayMode === MODE.FULL || sessionState.sessionTraceMode === MODE.FULL)) this.switchToFull()
66
68
  // if another page's session entity has expired, or another page has transitioned to off and this one hasn't... we can just abort straight away here
67
- if (this.sessionId !== sessionState.value || (eventType === 'cross-tab' && sessionState.sessionTraceMode === MODE.OFF)) this.abort(2)
69
+ if (this.sessionId !== sessionState.value || (eventType === 'cross-tab' && sessionState.sessionTraceMode === MODE.OFF)) this.abort(ABORT_REASONS.CROSS_TAB)
68
70
  })
69
71
 
70
72
  if (typeof PerformanceNavigationTiming !== 'undefined' && globalScope.performance?.getEntriesByType('navigation')?.length > 0) {
@@ -76,30 +78,31 @@ export class Aggregate extends AggregateBase {
76
78
 
77
79
  /** ST/SR sampling flow in BCS - https://drive.google.com/file/d/19hwt2oft-8Hh4RrjpLqEXfpP_9wYBLcq/view?usp=sharing */
78
80
  /** ST will run in the mode provided by BCS if the session IS NEW. If not... it will use the state of the session entity to determine what mode to run in */
79
- if (!this.agentRef.runtime.session.isNew && !ignoreSession) this.mode = this.agentRef.runtime.session.state.sessionTraceMode
81
+ if (this.agentRef.runtime.session.state.sessionTraceMode !== null && !ignoreSession) this.mode = this.agentRef.runtime.session.state.sessionTraceMode
80
82
  else this.mode = stMode
81
83
 
82
- /** If the mode is off, we do not want to hold up draining for other features, so we deregister the feature for now.
83
- * If it drains later (due to a mode change), data and handlers will instantly drain instead of waiting for the registry. */
84
- if (this.mode === MODE.OFF) return this.deregisterDrain()
85
-
86
- /** The handlers set up by the Inst file */
87
- registerHandler('bst', (...args) => this.traceStorage.storeEvent(...args), this.featureName, this.ee)
88
- registerHandler('bstResource', (...args) => this.traceStorage.storeResources(...args), this.featureName, this.ee)
89
- registerHandler('bstHist', (...args) => this.traceStorage.storeHist(...args), this.featureName, this.ee)
90
- registerHandler('bstXhrAgg', (...args) => this.traceStorage.storeXhrAgg(...args), this.featureName, this.ee)
91
- registerHandler('bstApi', (...args) => this.traceStorage.storeNode(...args), this.featureName, this.ee)
92
- registerHandler('trace-jserror', (...args) => this.traceStorage.storeErrorAgg(...args), this.featureName, this.ee)
93
- registerHandler('pvtAdded', (...args) => this.traceStorage.processPVT(...args), this.featureName, this.ee)
94
-
95
- if (this.mode !== MODE.FULL) {
96
- /** A separate handler for noticing errors, and switching to "full" mode if running in "error" mode */
97
- registerHandler('trace-jserror', () => {
98
- if (this.mode === MODE.ERROR) this.switchToFull()
99
- }, this.featureName, this.ee)
84
+ if (this.mode !== MODE.OFF) {
85
+ /** The handlers set up by the Inst file */
86
+ registerHandler('bst', (...args) => this.traceStorage.storeEvent(...args), this.featureName, this.ee)
87
+ registerHandler('bstResource', (...args) => this.traceStorage.storeResources(...args), this.featureName, this.ee)
88
+ registerHandler('bstHist', (...args) => this.traceStorage.storeHist(...args), this.featureName, this.ee)
89
+ registerHandler('bstXhrAgg', (...args) => this.traceStorage.storeXhrAgg(...args), this.featureName, this.ee)
90
+ registerHandler('bstApi', (...args) => this.traceStorage.storeNode(...args), this.featureName, this.ee)
91
+ registerHandler('trace-jserror', (...args) => this.traceStorage.storeErrorAgg(...args), this.featureName, this.ee)
92
+ registerHandler('pvtAdded', (...args) => this.traceStorage.processPVT(...args), this.featureName, this.ee)
93
+
94
+ if (this.mode === MODE.ERROR) {
95
+ /** A separate handler for noticing errors, and switching to "full" mode if running in "error" mode */
96
+ registerHandler('trace-jserror', () => {
97
+ if (this.mode === MODE.ERROR) this.switchToFull()
98
+ }, this.featureName, this.ee)
99
+ }
100
100
  }
101
101
  this.agentRef.runtime.session.write({ sessionTraceMode: this.mode })
102
- this.drain()
102
+
103
+ /** If the mode is off, we do not want to hold up draining for other features, so we deregister the feature for now.
104
+ * If it drains later (due to a mode change), data and handlers will instantly drain instead of waiting for the registry. */
105
+ this.mode === MODE.OFF ? this.deregisterDrain() : this.drain()
103
106
  }
104
107
 
105
108
  preHarvestChecks () {
@@ -108,7 +111,7 @@ export class Aggregate extends AggregateBase {
108
111
  if (!this.agentRef.runtime.session) return // session entity is required for trace to run and continue running
109
112
  if (this.sessionId !== this.agentRef.runtime.session.state.value || this.ptid !== this.agentRef.runtime.ptid) {
110
113
  // If something unexpected happened and we somehow still got to harvesting after a session identifier changed, we should force-exit instead of harvesting:
111
- this.abort(3)
114
+ this.abort(ABORT_REASONS.SESSION_CHANGED)
112
115
  return
113
116
  }
114
117
 
@@ -185,8 +188,8 @@ export class Aggregate extends AggregateBase {
185
188
  }
186
189
 
187
190
  /** Stop running for the remainder of the page lifecycle */
188
- abort (code) {
189
- warn(60, code)
191
+ abort (reason = {}) {
192
+ warn(60, reason.message)
190
193
  this.blocked = true
191
194
  this.mode = MODE.OFF
192
195
  this.agentRef.runtime.session.write({ sessionTraceMode: this.mode })
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
  import { FEATURE_NAMES } from '../../loaders/features/features'
@@ -14,3 +14,9 @@ export const FN_END = 'fn' + END
14
14
  export const PUSH_STATE = 'pushState'
15
15
  export const MAX_NODES_PER_HARVEST = 1000
16
16
  export const ERROR_MODE_SECONDS_WINDOW = 30 * 1000 // sliding window of nodes to track when simply monitoring (but not harvesting) in error mode
17
+ export const ABORT_REASONS = {
18
+ CROSS_TAB: { message: 'Session Entity was set to OFF on another tab' },
19
+ ENTITLEMENTS: { message: 'Session Trace is not allowed and will not be started' },
20
+ RESET: { message: 'Session was reset' },
21
+ SESSION_CHANGED: { message: 'Session identifier changed unexpectedly' }
22
+ }
@@ -11,6 +11,7 @@ import { handle } from '../../common/event-emitter/handle'
11
11
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../metrics/constants'
12
12
  import { EventAggregator } from '../../common/aggregate/event-aggregator'
13
13
  import { MAX_PAYLOAD_SIZE, IDEAL_PAYLOAD_SIZE, SUPPORTS_REGISTERED_ENTITIES } from '../../common/constants/agent-constants'
14
+ import { warn } from '../../common/util/console'
14
15
 
15
16
  export class AggregateBase extends FeatureBase {
16
17
  /**
@@ -114,6 +115,7 @@ export class AggregateBase extends FeatureBase {
114
115
  }
115
116
  function buildOutput (ref) {
116
117
  return flagNames.map(flag => {
118
+ if (ref[flag] === undefined || ref[flag] === null) warn(71, flag)
117
119
  if (!ref[flag]) return 0
118
120
  return ref[flag]
119
121
  })
@@ -18,6 +18,7 @@ import { measure } from './measure'
18
18
  import { recordCustomEvent } from './recordCustomEvent'
19
19
  import { subscribeToPageUnload } from '../../common/window/page-visibility'
20
20
  import { findScriptTimings } from '../../common/v2/script-tracker'
21
+ import { trackMFEVitals } from '../../common/v2/mfe-vitals'
21
22
  import { generateRandomHexString } from '../../common/ids/unique-id'
22
23
 
23
24
  /**
@@ -83,6 +84,9 @@ function register (agentRef, target) {
83
84
 
84
85
  const timings = findScriptTimings()
85
86
 
87
+ // Track MFE vitals for this entity
88
+ const vitals = trackMFEVitals(target.id, timings)
89
+
86
90
  const attrs = {}
87
91
 
88
92
  // Only define attributes getter if it doesn't already exist
@@ -149,7 +153,8 @@ function register (agentRef, target) {
149
153
  metadata: {
150
154
  get customAttributes () { return attrs },
151
155
  target,
152
- timings
156
+ timings,
157
+ vitals
153
158
  }
154
159
  }
155
160
 
@@ -177,9 +182,18 @@ function register (agentRef, target) {
177
182
  // only ever report the timings the first time this is called
178
183
  if (timings.reportedAt) return
179
184
  timings.reportedAt = now()
185
+
186
+ // Disconnect observers and capture current values, store in timings obj for visibility
187
+ vitals.disconnect()
188
+ timings.fcp = vitals.fcp
189
+ timings.lcp = vitals.lcp
190
+ timings.cls = vitals.cls
191
+ timings.inp = vitals.inp
192
+
180
193
  const timeToFetch = timings.fetchEnd - timings.fetchStart // fetchStart to fetchEnd
181
194
  const timeToExecute = timings.scriptEnd - timings.scriptStart // scriptStart to scriptEnd
182
- api.recordCustomEvent('MicroFrontEndTiming', {
195
+
196
+ const eventData = {
183
197
  assetUrl: timings.asset, // the url of the script that was registered, or undefined if it could not be determined (inline or no match)
184
198
  assetType: timings.type, // the type of asset that was associated with the timings, one of 'script', 'link' (if preloaded and found in the resource timing buffer), 'preload' (if preloaded but not found in the resource timing buffer), or "unknown" if it could not be determined
185
199
  timeAlive: timings.reportedAt - timings.registeredAt, // registeredAt to reportedAt
@@ -187,8 +201,15 @@ function register (agentRef, target) {
187
201
  timeToExecute, // scriptStart to scriptEnd
188
202
  timeToFetch, // fetchStart to fetchEnd
189
203
  timeToLoad: timeToFetch + timeToExecute, // fetch time and script time together
190
- timeToRegister: timings.registeredAt // timestamp when register() was called
191
- })
204
+ timeToRegister: timings.registeredAt, // timestamp when register() was called
205
+ // leave room to extend these with more data keys as needed
206
+ 'nr.vitals.fcp.value': vitals.fcp?.value ?? null, // FCP vital object with value and metadata
207
+ 'nr.vitals.lcp.value': vitals.lcp?.value ?? null, // LCP vital object with value and metadata
208
+ 'nr.vitals.cls.value': vitals.cls?.value ?? null, // CLS vital object with value and metadata
209
+ 'nr.vitals.inp.value': vitals.inp?.value ?? null // INP vital object with value and metadata
210
+ }
211
+
212
+ api.recordCustomEvent('MicroFrontEndTiming', eventData)
192
213
  }
193
214
 
194
215
  /**