@newrelic/browser-agent 1.316.0 → 1.317.0-rc.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.
Files changed (170) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cjs/common/config/configurable.js +1 -1
  3. package/dist/cjs/common/config/init-types.js +1 -0
  4. package/dist/cjs/common/config/init.js +6 -4
  5. package/dist/cjs/common/constants/env.cdn.js +2 -2
  6. package/dist/cjs/common/constants/env.npm.js +2 -2
  7. package/dist/cjs/common/constants/events.js +26 -0
  8. package/dist/cjs/common/harvest/harvester.js +9 -217
  9. package/dist/cjs/common/harvest/send.js +232 -0
  10. package/dist/cjs/common/harvest/types.js +9 -5
  11. package/dist/cjs/common/payloads/payloads.js +127 -0
  12. package/dist/cjs/common/serialize/bel-serializer.js +4 -3
  13. package/dist/cjs/common/session/session-entity.js +8 -8
  14. package/dist/cjs/common/util/data-size.js +2 -2
  15. package/dist/cjs/common/util/obfuscate.js +147 -3
  16. package/dist/cjs/common/v2/mfe-vitals.js +286 -0
  17. package/dist/cjs/common/wrap/wrap-websocket.js +2 -1
  18. package/dist/cjs/common/wrap/wrap-xhr.js +1 -1
  19. package/dist/cjs/features/ajax/aggregate/gql.js +40 -1
  20. package/dist/cjs/features/ajax/aggregate/index.js +51 -13
  21. package/dist/cjs/features/ajax/constants.js +6 -1
  22. package/dist/cjs/features/ajax/instrument/index.js +105 -17
  23. package/dist/cjs/features/generic_events/aggregate/index.js +15 -10
  24. package/dist/cjs/features/generic_events/constants.js +3 -2
  25. package/dist/cjs/features/jserrors/aggregate/format-stack-trace.js +2 -2
  26. package/dist/cjs/features/jserrors/aggregate/index.js +12 -3
  27. package/dist/cjs/features/logging/aggregate/index.js +12 -8
  28. package/dist/cjs/features/metrics/aggregate/index.js +5 -1
  29. package/dist/cjs/features/page_view_event/aggregate/index.js +72 -33
  30. package/dist/cjs/features/page_view_timing/aggregate/index.js +6 -1
  31. package/dist/cjs/features/session_replay/aggregate/index.js +39 -29
  32. package/dist/cjs/features/session_trace/aggregate/index.js +35 -29
  33. package/dist/cjs/features/session_trace/constants.js +17 -3
  34. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +36 -5
  35. package/dist/cjs/features/soft_navigations/aggregate/index.js +7 -1
  36. package/dist/cjs/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
  37. package/dist/cjs/features/soft_navigations/aggregate/interaction.js +6 -4
  38. package/dist/cjs/features/utils/aggregate-base.js +4 -3
  39. package/dist/cjs/loaders/api/register.js +26 -4
  40. package/dist/esm/common/config/configurable.js +1 -1
  41. package/dist/esm/common/config/init-types.js +1 -0
  42. package/dist/esm/common/config/init.js +3 -1
  43. package/dist/esm/common/constants/env.cdn.js +2 -2
  44. package/dist/esm/common/constants/env.npm.js +2 -2
  45. package/dist/esm/common/constants/events.js +20 -0
  46. package/dist/esm/common/harvest/harvester.js +9 -217
  47. package/dist/esm/common/harvest/send.js +226 -0
  48. package/dist/esm/common/harvest/types.js +9 -5
  49. package/dist/esm/common/payloads/payloads.js +118 -0
  50. package/dist/esm/common/serialize/bel-serializer.js +4 -3
  51. package/dist/esm/common/session/session-entity.js +9 -9
  52. package/dist/esm/common/util/data-size.js +2 -2
  53. package/dist/esm/common/util/obfuscate.js +147 -3
  54. package/dist/esm/common/v2/mfe-vitals.js +281 -0
  55. package/dist/esm/common/wrap/wrap-websocket.js +2 -1
  56. package/dist/esm/common/wrap/wrap-xhr.js +1 -1
  57. package/dist/esm/features/ajax/aggregate/gql.js +39 -1
  58. package/dist/esm/features/ajax/aggregate/index.js +52 -14
  59. package/dist/esm/features/ajax/constants.js +5 -0
  60. package/dist/esm/features/ajax/instrument/index.js +106 -18
  61. package/dist/esm/features/generic_events/aggregate/index.js +15 -10
  62. package/dist/esm/features/generic_events/constants.js +3 -2
  63. package/dist/esm/features/jserrors/aggregate/format-stack-trace.js +2 -2
  64. package/dist/esm/features/jserrors/aggregate/index.js +12 -3
  65. package/dist/esm/features/logging/aggregate/index.js +12 -8
  66. package/dist/esm/features/metrics/aggregate/index.js +5 -1
  67. package/dist/esm/features/page_view_event/aggregate/index.js +71 -32
  68. package/dist/esm/features/page_view_timing/aggregate/index.js +6 -1
  69. package/dist/esm/features/session_replay/aggregate/index.js +39 -29
  70. package/dist/esm/features/session_trace/aggregate/index.js +36 -30
  71. package/dist/esm/features/session_trace/constants.js +16 -2
  72. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +36 -5
  73. package/dist/esm/features/soft_navigations/aggregate/index.js +7 -1
  74. package/dist/esm/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
  75. package/dist/esm/features/soft_navigations/aggregate/interaction.js +6 -4
  76. package/dist/esm/features/utils/aggregate-base.js +4 -3
  77. package/dist/esm/loaders/api/register.js +26 -4
  78. package/dist/tsconfig.tsbuildinfo +1 -1
  79. package/dist/types/common/config/init-types.d.ts +4 -0
  80. package/dist/types/common/config/init-types.d.ts.map +1 -1
  81. package/dist/types/common/config/init.d.ts.map +1 -1
  82. package/dist/types/common/constants/events.d.ts +16 -0
  83. package/dist/types/common/constants/events.d.ts.map +1 -0
  84. package/dist/types/common/harvest/harvester.d.ts +2 -16
  85. package/dist/types/common/harvest/harvester.d.ts.map +1 -1
  86. package/dist/types/common/harvest/send.d.ts +9 -0
  87. package/dist/types/common/harvest/send.d.ts.map +1 -0
  88. package/dist/types/common/harvest/types.d.ts +39 -10
  89. package/dist/types/common/harvest/types.d.ts.map +1 -1
  90. package/dist/types/common/payloads/payloads.d.ts +42 -0
  91. package/dist/types/common/payloads/payloads.d.ts.map +1 -0
  92. package/dist/types/common/serialize/bel-serializer.d.ts +1 -1
  93. package/dist/types/common/serialize/bel-serializer.d.ts.map +1 -1
  94. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  95. package/dist/types/common/util/obfuscate.d.ts +23 -2
  96. package/dist/types/common/util/obfuscate.d.ts.map +1 -1
  97. package/dist/types/common/v2/mfe-vitals.d.ts +14 -0
  98. package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -0
  99. package/dist/types/common/wrap/wrap-websocket.d.ts.map +1 -1
  100. package/dist/types/features/ajax/aggregate/gql.d.ts +8 -0
  101. package/dist/types/features/ajax/aggregate/gql.d.ts.map +1 -1
  102. package/dist/types/features/ajax/aggregate/index.d.ts +2 -0
  103. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  104. package/dist/types/features/ajax/constants.d.ts +5 -0
  105. package/dist/types/features/ajax/constants.d.ts.map +1 -1
  106. package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
  107. package/dist/types/features/generic_events/aggregate/index.d.ts +3 -1
  108. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  109. package/dist/types/features/generic_events/constants.d.ts.map +1 -1
  110. package/dist/types/features/jserrors/aggregate/index.d.ts +8 -1
  111. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  112. package/dist/types/features/logging/aggregate/index.d.ts +3 -1
  113. package/dist/types/features/logging/aggregate/index.d.ts.map +1 -1
  114. package/dist/types/features/metrics/aggregate/index.d.ts +2 -0
  115. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  116. package/dist/types/features/page_view_event/aggregate/index.d.ts +3 -0
  117. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  118. package/dist/types/features/page_view_timing/aggregate/index.d.ts +2 -0
  119. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  120. package/dist/types/features/session_replay/aggregate/index.d.ts +4 -2
  121. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  122. package/dist/types/features/session_trace/aggregate/index.d.ts +3 -1
  123. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  124. package/dist/types/features/session_trace/constants.d.ts +17 -0
  125. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +6 -1
  126. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  127. package/dist/types/features/soft_navigations/aggregate/index.d.ts +3 -0
  128. package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
  129. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +3 -1
  130. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
  131. package/dist/types/features/utils/aggregate-base.d.ts +0 -1
  132. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  133. package/dist/types/loaders/api/register.d.ts.map +1 -1
  134. package/package.json +3 -3
  135. package/src/common/config/configurable.js +1 -1
  136. package/src/common/config/init-types.js +1 -0
  137. package/src/common/config/init.js +2 -1
  138. package/src/common/constants/events.js +20 -0
  139. package/src/common/harvest/harvester.js +9 -201
  140. package/src/common/harvest/send.js +209 -0
  141. package/src/common/harvest/types.js +9 -5
  142. package/src/common/payloads/payloads.js +135 -0
  143. package/src/common/serialize/bel-serializer.js +4 -3
  144. package/src/common/session/session-entity.js +8 -9
  145. package/src/common/util/data-size.js +2 -2
  146. package/src/common/util/obfuscate.js +154 -3
  147. package/src/common/v2/mfe-vitals.js +260 -0
  148. package/src/common/wrap/wrap-websocket.js +2 -1
  149. package/src/common/wrap/wrap-xhr.js +1 -1
  150. package/src/features/ajax/aggregate/gql.js +42 -1
  151. package/src/features/ajax/aggregate/index.js +43 -13
  152. package/src/features/ajax/constants.js +5 -1
  153. package/src/features/ajax/instrument/index.js +114 -18
  154. package/src/features/generic_events/aggregate/index.js +14 -9
  155. package/src/features/generic_events/constants.js +3 -2
  156. package/src/features/jserrors/aggregate/format-stack-trace.js +3 -3
  157. package/src/features/jserrors/aggregate/index.js +12 -3
  158. package/src/features/logging/aggregate/index.js +12 -11
  159. package/src/features/metrics/aggregate/index.js +5 -1
  160. package/src/features/page_view_event/aggregate/index.js +58 -29
  161. package/src/features/page_view_timing/aggregate/index.js +6 -1
  162. package/src/features/session_replay/aggregate/index.js +37 -28
  163. package/src/features/session_trace/aggregate/index.js +37 -30
  164. package/src/features/session_trace/constants.js +7 -1
  165. package/src/features/soft_navigations/aggregate/ajax-node.js +26 -5
  166. package/src/features/soft_navigations/aggregate/index.js +7 -1
  167. package/src/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
  168. package/src/features/soft_navigations/aggregate/interaction.js +6 -4
  169. package/src/features/utils/aggregate-base.js +3 -3
  170. package/src/loaders/api/register.js +24 -4
@@ -9,6 +9,7 @@ import { handle } from '../../../common/event-emitter/handle';
9
9
  import { FEATURE_NAME } from '../constants';
10
10
  import { FEATURE_NAMES } from '../../../loaders/features/features';
11
11
  import { AggregateBase } from '../../utils/aggregate-base';
12
+ import { Obfuscator } from '../../../common/util/obfuscate';
12
13
  import { cumulativeLayoutShift } from '../../../common/vitals/cumulative-layout-shift';
13
14
  import { firstContentfulPaint } from '../../../common/vitals/first-contentful-paint';
14
15
  import { firstPaint } from '../../../common/vitals/first-paint';
@@ -21,6 +22,7 @@ import { eventOrigin } from '../../../common/util/event-origin';
21
22
  import { loadTime } from '../../../common/vitals/load-time';
22
23
  import { webdriverDetected } from '../../../common/util/webdriver-detection';
23
24
  import { cleanURL } from '../../../common/url/clean-url';
25
+ import { EVENT_TYPES } from '../../../common/constants/events';
24
26
  export class Aggregate extends AggregateBase {
25
27
  static featureName = FEATURE_NAME;
26
28
  #handleVitalMetric = ({
@@ -35,6 +37,9 @@ export class Aggregate extends AggregateBase {
35
37
  this.curSessEndRecorded = false;
36
38
  this.firstIxnRecorded = false;
37
39
  super.customAttributesAreSeparate = true;
40
+
41
+ // Create obfuscator for page view timing events
42
+ this.obfuscator = new Obfuscator(agentRef, EVENT_TYPES.PVT);
38
43
  registerHandler('docHidden', msTimestamp => this.endCurrentSession(msTimestamp), this.featureName, this.ee);
39
44
  // Add the time of _window pagehide event_ firing to the next PVT harvest == NRDB windowUnload attr:
40
45
  registerHandler('winPagehide', msTimestamp => this.addTiming('unload', msTimestamp, null), this.featureName, this.ee);
@@ -140,7 +145,7 @@ export class Aggregate extends AggregateBase {
140
145
  // serialize array of timing data
141
146
  serializer(eventBuffer) {
142
147
  if (!eventBuffer?.length) return '';
143
- var addString = getAddStringContext(this.agentRef.runtime.obfuscator);
148
+ var addString = getAddStringContext(this.obfuscator);
144
149
  var payload = 'bel.6;';
145
150
  for (var i = 0; i < eventBuffer.length; i++) {
146
151
  var timing = eventBuffer[i];
@@ -19,16 +19,22 @@ import { stringify } from '../../../common/util/stringify';
19
19
  import { stylesheetEvaluator } from '../shared/stylesheet-evaluator';
20
20
  import { now } from '../../../common/timing/now';
21
21
  import { MAX_PAYLOAD_SIZE } from '../../../common/constants/agent-constants';
22
+ import { EVENT_TYPES } from '../../../common/constants/events';
22
23
  import { cleanURL } from '../../../common/url/clean-url';
23
24
  import { canEnableSessionTracking } from '../../utils/feature-gates';
24
25
  import { PAUSE_REPLAY } from '../../../loaders/api/constants';
26
+ import { Obfuscator } from '../../../common/util/obfuscate';
25
27
  export class Aggregate extends AggregateBase {
26
28
  static featureName = FEATURE_NAME;
27
- mode = MODE.OFF;
29
+ mode = null;
28
30
 
29
31
  // pass the recorder into the aggregator
30
32
  constructor(agentRef, args) {
31
33
  super(agentRef, FEATURE_NAME);
34
+
35
+ // Create obfuscator for session replay query params
36
+ this.obfuscator = new Obfuscator(agentRef, EVENT_TYPES.SR);
37
+
32
38
  /** Set once the recorder has fully initialized after flag checks and sampling */
33
39
  this.initialized = false;
34
40
  /** Set once the feature has been "aborted" to prevent other side-effects from continuing */
@@ -65,7 +71,7 @@ export class Aggregate extends AggregateBase {
65
71
  if (!this.recorder) return;
66
72
  // if the mode changed on a different tab, it needs to update this instance to match
67
73
  this.mode = agentRef.runtime.session.state.sessionReplayMode;
68
- if (!this.initialized || this.mode === MODE.OFF) return;
74
+ if (!this.initialized || this.mode === MODE.OFF || !this.mode) return;
69
75
  this.recorder?.startRecording(TRIGGERS.RESUME, this.mode);
70
76
  });
71
77
  this.ee.on(SESSION_EVENTS.UPDATE, (type, data) => {
@@ -92,6 +98,10 @@ export class Aggregate extends AggregateBase {
92
98
  this.waitForFlags(['srs', 'sr']).then(([srMode, entitled]) => {
93
99
  this.entitled = !!entitled;
94
100
  if (!this.entitled) {
101
+ this.mode = MODE.OFF;
102
+ this.#writeToStorage({
103
+ sessionReplayMode: this.mode
104
+ });
95
105
  this.deregisterDrain();
96
106
  if (this.agentRef.runtime.isRecording) {
97
107
  this.abort(ABORT_REASONS.ENTITLEMENTS);
@@ -103,7 +113,7 @@ export class Aggregate extends AggregateBase {
103
113
  this.drain();
104
114
  });
105
115
  }).then(() => {
106
- if (this.mode === MODE.OFF) {
116
+ if (!this.mode) {
107
117
  this.recorder?.stopRecording(); // stop any conservative preload recording launched by instrument
108
118
  while (this.recorder?.getEvents().events.length) this.recorder?.clearBuffer?.();
109
119
  }
@@ -136,7 +146,7 @@ export class Aggregate extends AggregateBase {
136
146
  // if the error was noticed AFTER the recorder was already imported....
137
147
  if (this.recorder && this.initialized) {
138
148
  if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(TRIGGERS.SWITCH_TO_FULL, this.mode); // off --> full
139
- this.syncWithSessionManager({
149
+ this.#writeToStorage({
140
150
  sessionReplayMode: this.mode
141
151
  });
142
152
  } else {
@@ -168,35 +178,35 @@ export class Aggregate extends AggregateBase {
168
178
  this.timeKeeper = timeKeeper;
169
179
  if (this.recorder?.trigger === TRIGGERS.API && this.agentRef.runtime.isRecording) {
170
180
  this.mode = MODE.FULL;
171
- } else if (!session.isNew && !ignoreSession) {
181
+ } else if (session.state.sessionReplayMode !== null && !ignoreSession) {
172
182
  // inherit the mode of the existing session
173
183
  this.mode = session.state.sessionReplayMode;
174
184
  } else {
175
185
  // The session is new... determine the mode the new session should start in
176
186
  this.mode = srMode;
177
187
  }
178
- // If off, then don't record (early return)
179
- if (this.mode === MODE.OFF) return;
180
- try {
181
- /** will return a recorder instance if already imported, otherwise, will fetch the recorder and initialize it */
182
- this.recorder ??= await this.instrumentClass.importRecorder();
183
- } catch (err) {
184
- /** if the recorder fails to import, abort the feature */
185
- return this.abort(ABORT_REASONS.IMPORT, err);
186
- }
188
+ if (this.mode !== MODE.OFF) {
189
+ try {
190
+ /** will return a recorder instance if already imported, otherwise, will fetch the recorder and initialize it */
191
+ this.recorder ??= await this.instrumentClass.importRecorder();
192
+ } catch (err) {
193
+ /** if the recorder fails to import, abort the feature */
194
+ return this.abort(ABORT_REASONS.IMPORT, err);
195
+ }
187
196
 
188
- // If an error was noticed before the mode could be set (like in the early lifecycle of the page), immediately set to FULL mode
189
- if (this.mode === MODE.ERROR && this.instrumentClass.errorNoticed) {
190
- this.mode = MODE.FULL;
191
- }
197
+ // If an error was noticed before the mode could be set (like in the early lifecycle of the page), immediately set to FULL mode
198
+ if (this.mode === MODE.ERROR && this.instrumentClass.errorNoticed) {
199
+ this.mode = MODE.FULL;
200
+ }
192
201
 
193
- // FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
194
- // ERROR mode will do this until an error is thrown, and then switch into FULL mode.
195
- // The makeHarvestPayload should ensure that no payload is returned if we're not in FULL mode...
202
+ // FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
203
+ // ERROR mode will do this until an error is thrown, and then switch into FULL mode.
204
+ // The makeHarvestPayload should ensure that no payload is returned if we're not in FULL mode...
196
205
 
197
- await this.prepUtils();
198
- if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(trigger, this.mode);
199
- this.syncWithSessionManager({
206
+ await this.prepUtils();
207
+ if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(trigger, this.mode);
208
+ }
209
+ this.#writeToStorage({
200
210
  sessionReplayMode: this.mode
201
211
  });
202
212
  }
@@ -243,7 +253,7 @@ export class Aggregate extends AggregateBase {
243
253
  this.abort(ABORT_REASONS.TOO_BIG, len);
244
254
  return;
245
255
  }
246
- if (!this.agentRef.runtime.session.state.sessionReplaySentFirstChunk) this.syncWithSessionManager({
256
+ if (!this.agentRef.runtime.session.state.sessionReplaySentFirstChunk) this.#writeToStorage({
247
257
  sessionReplaySentFirstChunk: true
248
258
  });
249
259
  this.recorder.clearBuffer();
@@ -306,7 +316,7 @@ export class Aggregate extends AggregateBase {
306
316
  return {
307
317
  qs: {
308
318
  browser_monitoring_key: this.agentRef.info.licenseKey,
309
- type: 'SessionReplay',
319
+ type: EVENT_TYPES.SR,
310
320
  app_id: this.agentRef.info.applicationID,
311
321
  protocol_version: '0',
312
322
  timestamp: firstTimestamp,
@@ -372,7 +382,7 @@ export class Aggregate extends AggregateBase {
372
382
  if (forceHarvest) this.agentRef.runtime.harvester.triggerHarvestFor(this);
373
383
  this.mode = MODE.OFF;
374
384
  this.recorder?.stopRecording?.();
375
- this.syncWithSessionManager({
385
+ this.#writeToStorage({
376
386
  sessionReplayMode: this.mode
377
387
  });
378
388
  }
@@ -384,13 +394,13 @@ export class Aggregate extends AggregateBase {
384
394
  this.blocked = true;
385
395
  this.mode = MODE.OFF;
386
396
  this.recorder?.stopRecording?.();
387
- this.syncWithSessionManager({
397
+ this.#writeToStorage({
388
398
  sessionReplayMode: this.mode
389
399
  });
390
400
  this.recorder?.clearTimestamps?.();
391
401
  while (this.recorder?.getEvents().events.length) this.recorder?.clearBuffer?.();
392
402
  }
393
- syncWithSessionManager(state = {}) {
403
+ #writeToStorage(state = {}) {
394
404
  if (this.isSessionTrackingEnabled) {
395
405
  this.agentRef.runtime.session.write(state);
396
406
  }
@@ -3,15 +3,16 @@
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';
10
10
  import { globalScope } from '../../../common/constants/runtime';
11
11
  import { MODE, SESSION_EVENTS } from '../../../common/session/constants';
12
- import { applyFnToProps } from '../../../common/util/traverse';
12
+ import { Obfuscator } from '../../../common/util/obfuscate';
13
13
  import { cleanURL } from '../../../common/url/clean-url';
14
14
  import { warn } from '../../../common/util/console';
15
+ import { EVENT_TYPES } from '../../../common/constants/events';
15
16
 
16
17
  /** Reserved room for query param attrs */
17
18
  const QUERY_PARAM_PADDING = 5000;
@@ -21,6 +22,9 @@ export class Aggregate extends AggregateBase {
21
22
  super(agentRef, FEATURE_NAME);
22
23
  this.harvestOpts.raw = true;
23
24
 
25
+ // Create obfuscator for session trace nodes
26
+ this.obfuscator = new Obfuscator(agentRef, EVENT_TYPES.ST);
27
+
24
28
  /** Tied to the entitlement flag response from BCS. Will short circuit operations of the agg if false */
25
29
  this.entitled = undefined;
26
30
  /** A flag used to decide if the 30 node threshold should be ignored on the first harvest to ensure sending on the first payload */
@@ -37,8 +41,10 @@ export class Aggregate extends AggregateBase {
37
41
  /** 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 */
38
42
  initialize(stMode, stEntitled, ignoreSession) {
39
43
  this.entitled ??= stEntitled;
40
- if (!this.entitled) this.blocked = true;
41
- if (this.blocked) return this.deregisterDrain();
44
+ if (!this.entitled) {
45
+ this.abort(ABORT_REASONS.ENTITLEMENTS);
46
+ return this.deregisterDrain();
47
+ }
42
48
  this.timeKeeper ??= this.agentRef.runtime.timeKeeper;
43
49
  if (!this.initialized) {
44
50
  this.initialized = true;
@@ -48,7 +54,7 @@ export class Aggregate extends AggregateBase {
48
54
  // 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.
49
55
  this.ee.on(SESSION_EVENTS.RESET, () => {
50
56
  if (this.blocked) return;
51
- this.abort(1);
57
+ this.abort(ABORT_REASONS.RESET);
52
58
  });
53
59
  // The SessionEntity can have updates (locally or across tabs for SR mode changes), (across tabs for ST mode changes).
54
60
  // Those updates should be sync'd here to ensure this page also honors the mode after initialization
@@ -57,7 +63,7 @@ export class Aggregate extends AggregateBase {
57
63
  // this will only have an effect if ST is NOT already in full mode
58
64
  if (this.mode !== MODE.FULL && (sessionState.sessionReplayMode === MODE.FULL || sessionState.sessionTraceMode === MODE.FULL)) this.switchToFull();
59
65
  // 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
60
- if (this.sessionId !== sessionState.value || eventType === 'cross-tab' && sessionState.sessionTraceMode === MODE.OFF) this.abort(2);
66
+ if (this.sessionId !== sessionState.value || eventType === 'cross-tab' && sessionState.sessionTraceMode === MODE.OFF) this.abort(ABORT_REASONS.CROSS_TAB);
61
67
  });
62
68
  if (typeof PerformanceNavigationTiming !== 'undefined' && globalScope.performance?.getEntriesByType('navigation')?.length > 0) {
63
69
  this.traceStorage.storeTiming(globalScope.performance.getEntriesByType('navigation')[0]);
@@ -68,30 +74,30 @@ export class Aggregate extends AggregateBase {
68
74
 
69
75
  /** ST/SR sampling flow in BCS - https://drive.google.com/file/d/19hwt2oft-8Hh4RrjpLqEXfpP_9wYBLcq/view?usp=sharing */
70
76
  /** 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 */
71
- if (!this.agentRef.runtime.session.isNew && !ignoreSession) this.mode = this.agentRef.runtime.session.state.sessionTraceMode;else this.mode = stMode;
72
-
73
- /** If the mode is off, we do not want to hold up draining for other features, so we deregister the feature for now.
74
- * If it drains later (due to a mode change), data and handlers will instantly drain instead of waiting for the registry. */
75
- if (this.mode === MODE.OFF) return this.deregisterDrain();
76
-
77
- /** The handlers set up by the Inst file */
78
- registerHandler('bst', (...args) => this.traceStorage.storeEvent(...args), this.featureName, this.ee);
79
- registerHandler('bstResource', (...args) => this.traceStorage.storeResources(...args), this.featureName, this.ee);
80
- registerHandler('bstHist', (...args) => this.traceStorage.storeHist(...args), this.featureName, this.ee);
81
- registerHandler('bstXhrAgg', (...args) => this.traceStorage.storeXhrAgg(...args), this.featureName, this.ee);
82
- registerHandler('bstApi', (...args) => this.traceStorage.storeNode(...args), this.featureName, this.ee);
83
- registerHandler('trace-jserror', (...args) => this.traceStorage.storeErrorAgg(...args), this.featureName, this.ee);
84
- registerHandler('pvtAdded', (...args) => this.traceStorage.processPVT(...args), this.featureName, this.ee);
85
- if (this.mode !== MODE.FULL) {
86
- /** A separate handler for noticing errors, and switching to "full" mode if running in "error" mode */
87
- registerHandler('trace-jserror', () => {
88
- if (this.mode === MODE.ERROR) this.switchToFull();
89
- }, this.featureName, this.ee);
77
+ if (this.agentRef.runtime.session.state.sessionTraceMode !== null && !ignoreSession) this.mode = this.agentRef.runtime.session.state.sessionTraceMode;else this.mode = stMode;
78
+ if (this.mode !== MODE.OFF) {
79
+ /** The handlers set up by the Inst file */
80
+ registerHandler('bst', (...args) => this.traceStorage.storeEvent(...args), this.featureName, this.ee);
81
+ registerHandler('bstResource', (...args) => this.traceStorage.storeResources(...args), this.featureName, this.ee);
82
+ registerHandler('bstHist', (...args) => this.traceStorage.storeHist(...args), this.featureName, this.ee);
83
+ registerHandler('bstXhrAgg', (...args) => this.traceStorage.storeXhrAgg(...args), this.featureName, this.ee);
84
+ registerHandler('bstApi', (...args) => this.traceStorage.storeNode(...args), this.featureName, this.ee);
85
+ registerHandler('trace-jserror', (...args) => this.traceStorage.storeErrorAgg(...args), this.featureName, this.ee);
86
+ registerHandler('pvtAdded', (...args) => this.traceStorage.processPVT(...args), this.featureName, this.ee);
87
+ if (this.mode === MODE.ERROR) {
88
+ /** A separate handler for noticing errors, and switching to "full" mode if running in "error" mode */
89
+ registerHandler('trace-jserror', () => {
90
+ if (this.mode === MODE.ERROR) this.switchToFull();
91
+ }, this.featureName, this.ee);
92
+ }
90
93
  }
91
94
  this.agentRef.runtime.session.write({
92
95
  sessionTraceMode: this.mode
93
96
  });
94
- this.drain();
97
+
98
+ /** If the mode is off, we do not want to hold up draining for other features, so we deregister the feature for now.
99
+ * If it drains later (due to a mode change), data and handlers will instantly drain instead of waiting for the registry. */
100
+ this.mode === MODE.OFF ? this.deregisterDrain() : this.drain();
95
101
  }
96
102
  preHarvestChecks() {
97
103
  if (this.blocked || this.mode !== MODE.FULL) return; // only allow harvest if running in full mode
@@ -99,7 +105,7 @@ export class Aggregate extends AggregateBase {
99
105
  if (!this.agentRef.runtime.session) return; // session entity is required for trace to run and continue running
100
106
  if (this.sessionId !== this.agentRef.runtime.session.state.value || this.ptid !== this.agentRef.runtime.ptid) {
101
107
  // If something unexpected happened and we somehow still got to harvesting after a session identifier changed, we should force-exit instead of harvesting:
102
- this.abort(3);
108
+ this.abort(ABORT_REASONS.SESSION_CHANGED);
103
109
  return;
104
110
  }
105
111
  return true;
@@ -107,7 +113,7 @@ export class Aggregate extends AggregateBase {
107
113
  serializer(stns) {
108
114
  if (!stns.length) return; // there are no processed nodes
109
115
  this.everHarvested = true;
110
- return applyFnToProps(stns, this.obfuscator.obfuscateString.bind(this.obfuscator), 'string');
116
+ return this.obfuscator.traverseAndObfuscateEvents(stns);
111
117
  }
112
118
  queryStringsBuilder(stns) {
113
119
  const firstSessionHarvest = !this.agentRef.runtime.session.state.traceHarvestStarted;
@@ -184,8 +190,8 @@ export class Aggregate extends AggregateBase {
184
190
  }
185
191
 
186
192
  /** Stop running for the remainder of the page lifecycle */
187
- abort(code) {
188
- warn(60, code);
193
+ abort(reason = {}) {
194
+ warn(60, reason.message);
189
195
  this.blocked = true;
190
196
  this.mode = MODE.OFF;
191
197
  this.agentRef.runtime.session.write({
@@ -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';
@@ -12,4 +12,18 @@ export const FN_START = 'fn' + START;
12
12
  export const FN_END = 'fn' + END;
13
13
  export const PUSH_STATE = 'pushState';
14
14
  export const MAX_NODES_PER_HARVEST = 1000;
15
- export const ERROR_MODE_SECONDS_WINDOW = 30 * 1000; // sliding window of nodes to track when simply monitoring (but not harvesting) in error mode
15
+ export const ERROR_MODE_SECONDS_WINDOW = 30 * 1000; // sliding window of nodes to track when simply monitoring (but not harvesting) in error mode
16
+ export const ABORT_REASONS = {
17
+ CROSS_TAB: {
18
+ message: 'Session Entity was set to OFF on another tab'
19
+ },
20
+ ENTITLEMENTS: {
21
+ message: 'Session Trace is not allowed and will not be started'
22
+ },
23
+ RESET: {
24
+ message: 'Session was reset'
25
+ },
26
+ SESSION_CHANGED: {
27
+ message: 'Session identifier changed unexpectedly'
28
+ }
29
+ };
@@ -3,6 +3,7 @@
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
  import { addCustomAttributes, getAddStringContext, nullable, numeric } from '../../../common/serialize/bel-serializer';
6
+ import { createStringAdders } from '../../../common/payloads/payloads';
6
7
  import { AJAX_ID } from '../../ajax/constants';
7
8
  import { NODE_TYPE } from '../constants';
8
9
  import { BelNode } from './bel-node';
@@ -24,6 +25,12 @@ export class AjaxNode extends BelNode {
24
25
  this.targetAttributes = ajaxEvent.targetAttributes;
25
26
  this[AJAX_ID] = ajaxEvent[AJAX_ID]; // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
26
27
 
28
+ // optional payload metadata attributes
29
+ this.requestBody = ajaxEvent.requestBody;
30
+ this.requestHeaders = ajaxEvent.requestHeaders;
31
+ this.requestQuery = ajaxEvent.requestQuery;
32
+ this.responseBody = ajaxEvent.responseBody;
33
+ this.responseHeaders = ajaxEvent.responseHeaders;
27
34
  this.start = ajaxEvent.startTime;
28
35
  this.end = ajaxEvent.endTime;
29
36
  if (ajaxContext?.latestLongtaskEnd) {
@@ -31,8 +38,11 @@ export class AjaxNode extends BelNode {
31
38
  this.callbackDuration = this.callbackEnd - this.end; // callbackDuration is the time from ajax loaded to last long task observed from it
32
39
  } else this.callbackEnd = this.end; // if no long task was observed, callbackEnd is the same as end
33
40
  }
34
- serialize(parentStartTimestamp, agentRef) {
35
- const addString = getAddStringContext(agentRef.runtime.obfuscator);
41
+ serialize(parentStartTimestamp, agentRef, ajaxObfuscator) {
42
+ const {
43
+ addString,
44
+ addStringWithTruncation
45
+ } = createStringAdders(getAddStringContext, ajaxObfuscator);
36
46
  const nodeList = [];
37
47
 
38
48
  // IMPORTANT: The order in which addString is called matters and correlates to the order in which string shows up in the harvest payload. Do not re-order the following code.
@@ -47,11 +57,32 @@ export class AjaxNode extends BelNode {
47
57
  numeric(this.callbackDuration),
48
58
  // not relative
49
59
  addString(this.method), numeric(this.status), addString(this.domain), addString(this.path), numeric(this.txSize), numeric(this.rxSize), this.requestedWith, addString(this.nodeId), nullable(this.spanId, addString, true) + nullable(this.traceId, addString, true) + nullable(this.spanTimestamp, numeric)];
50
- let allAttachedNodes = addCustomAttributes({
51
- ...(this.gql || {}),
60
+ // Regular attributes: obfuscate only
61
+ const regularAttrs = addCustomAttributes({
62
+ [AJAX_ID]: this[AJAX_ID],
52
63
  ...(this.targetAttributes || {}),
53
- [AJAX_ID]: this[AJAX_ID]
64
+ ...(this.gql || {})
54
65
  }, addString);
66
+
67
+ // Payload attributes: obfuscate then truncate
68
+ const payloadAttrs = addCustomAttributes({
69
+ ...(this.requestBody ? {
70
+ requestBody: this.requestBody
71
+ } : {}),
72
+ ...(this.requestHeaders ? {
73
+ requestHeaders: this.requestHeaders
74
+ } : {}),
75
+ ...(this.requestQuery ? {
76
+ requestQuery: this.requestQuery
77
+ } : {}),
78
+ ...(this.responseBody ? {
79
+ responseBody: this.responseBody
80
+ } : {}),
81
+ ...(this.responseHeaders ? {
82
+ responseHeaders: this.responseHeaders
83
+ } : {})
84
+ }, addStringWithTruncation);
85
+ let allAttachedNodes = [...regularAttrs, ...payloadAttrs];
55
86
  this.children.forEach(node => allAttachedNodes.push(node.serialize())); // no children is expected under ajax nodes at this time
56
87
 
57
88
  fields[1] = numeric(allAttachedNodes.length);
@@ -8,10 +8,12 @@ import { webdriverDetected } from '../../../common/util/webdriver-detection';
8
8
  import { loadTime } from '../../../common/vitals/load-time';
9
9
  import { FEATURE_NAMES } from '../../../loaders/features/features';
10
10
  import { AggregateBase } from '../../utils/aggregate-base';
11
+ import { Obfuscator } from '../../../common/util/obfuscate';
11
12
  import { API_TRIGGER_NAME, FEATURE_NAME, INTERACTION_STATUS, INTERACTION_TRIGGERS, IPL_TRIGGER_NAME, NO_LONG_TASK_WINDOW, POPSTATE_MERGE_WINDOW, POPSTATE_TRIGGER } from '../constants';
12
13
  import { AjaxNode } from './ajax-node';
13
14
  import { InitialPageLoadInteraction } from './initial-page-load-interaction';
14
15
  import { Interaction } from './interaction';
16
+ import { EVENT_TYPES } from '../../../common/constants/events';
15
17
  export class Aggregate extends AggregateBase {
16
18
  static featureName = FEATURE_NAME;
17
19
  constructor(agentRef, {
@@ -19,6 +21,10 @@ export class Aggregate extends AggregateBase {
19
21
  }) {
20
22
  super(agentRef, FEATURE_NAME);
21
23
  super.customAttributesAreSeparate = true;
24
+
25
+ // Create obfuscators for browser interactions and nested AJAX requests
26
+ this.interactionObfuscator = new Obfuscator(agentRef, EVENT_TYPES.BI);
27
+ this.ajaxObfuscator = new Obfuscator(agentRef, EVENT_TYPES.AJAX);
22
28
  this.interactionsToHarvest = this.events;
23
29
  this.domObserver = domObserver;
24
30
  this.initialPageLoadInteraction = new InitialPageLoadInteraction(agentRef);
@@ -82,7 +88,7 @@ export class Aggregate extends AggregateBase {
82
88
  let firstIxnStartTime;
83
89
  const serializedIxnList = [];
84
90
  for (const interaction of eventBuffer) {
85
- serializedIxnList.push(interaction.serialize(firstIxnStartTime, this.agentRef));
91
+ serializedIxnList.push(interaction.serialize(firstIxnStartTime, this.agentRef, this.interactionObfuscator, this.ajaxObfuscator));
86
92
  if (firstIxnStartTime === undefined) firstIxnStartTime = Math.floor(interaction.start); // careful not to match or overwrite on 0 value!
87
93
  }
88
94
  return "bel.7;".concat(serializedIxnList.join(';'));
@@ -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 { navTimingValues } from '../../../common/timing/nav-timing';
@@ -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 { globalScope, initialLocation } from '../../../common/constants/runtime';
@@ -121,11 +121,13 @@ export class Interaction extends BelNode {
121
121
  * Serializes (BEL) the interaction data for transmission.
122
122
  * @param {Number} firstStartTimeOfPayload timestamp
123
123
  * @param {Agent} agentRef Pass in the agent reference directly so that the event itself doesnt need to store the pointers and ruin the evaluation of the event size by including unused object references.
124
+ * @param {Obfuscator} interactionObfuscator Obfuscator for BrowserInteraction event type
125
+ * @param {Obfuscator} ajaxObfuscator Obfuscator for AjaxRequest event type (passed to child AjaxNodes)
124
126
  * @returns {String} A string that is the serialized representation of this interaction.
125
127
  */
126
- serialize(firstStartTimeOfPayload, agentRef) {
128
+ serialize(firstStartTimeOfPayload, agentRef, interactionObfuscator, ajaxObfuscator) {
127
129
  const isFirstIxnOfPayload = firstStartTimeOfPayload === undefined;
128
- const addString = getAddStringContext(agentRef.runtime.obfuscator);
130
+ const addString = getAddStringContext(interactionObfuscator);
129
131
  const nodeList = [];
130
132
  let ixnType;
131
133
  if (this.trigger === IPL_TRIGGER_NAME) ixnType = INTERACTION_TYPE.INITIAL_PAGE_LOAD;else if (this.newURL !== this.oldURL) ixnType = INTERACTION_TYPE.ROUTE_CHANGE;else ixnType = INTERACTION_TYPE.UNSPECIFIED;
@@ -151,7 +153,7 @@ export class Interaction extends BelNode {
151
153
  /* Querypack encoder+decoder quirkiness:
152
154
  - If first ixn node of payload is being processed, its children's start time must be offset by this node's start. (firstStartTime should be undefined.)
153
155
  - Else for subsequent ixns in the same payload, we go back to using that first ixn node's start to offset their children's start. */
154
- this.children.forEach(node => allAttachedNodes.push(node.serialize(isFirstIxnOfPayload ? this.start : firstStartTimeOfPayload, agentRef))); // recursively add the serialized string of every child of this (ixn) bel node
156
+ this.children.forEach(node => allAttachedNodes.push(node.serialize(isFirstIxnOfPayload ? this.start : firstStartTimeOfPayload, agentRef, ajaxObfuscator))); // recursively add the serialized string of every child of this (ixn) bel node
155
157
 
156
158
  fields[1] = numeric(allAttachedNodes.length);
157
159
  nodeList.push(fields);
@@ -4,7 +4,6 @@
4
4
  */
5
5
  import { FeatureBase } from './feature-base';
6
6
  import { drain } from '../../common/drain/drain';
7
- import { Obfuscator } from '../../common/util/obfuscate';
8
7
  import { FEATURE_NAMES } from '../../loaders/features/features';
9
8
  import { Harvester } from '../../common/harvest/harvester';
10
9
  import { EventBuffer } from './event-buffer';
@@ -12,6 +11,7 @@ import { handle } from '../../common/event-emitter/handle';
12
11
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../metrics/constants';
13
12
  import { EventAggregator } from '../../common/aggregate/event-aggregator';
14
13
  import { MAX_PAYLOAD_SIZE, IDEAL_PAYLOAD_SIZE, SUPPORTS_REGISTERED_ENTITIES } from '../../common/constants/agent-constants';
14
+ import { warn } from '../../common/util/console';
15
15
  export class AggregateBase extends FeatureBase {
16
16
  /**
17
17
  * Create an AggregateBase instance.
@@ -111,6 +111,7 @@ export class AggregateBase extends FeatureBase {
111
111
  }
112
112
  function buildOutput(ref) {
113
113
  return flagNames.map(flag => {
114
+ if (ref[flag] === undefined || ref[flag] === null) warn(71, flag);
114
115
  if (!ref[flag]) return 0;
115
116
  return ref[flag];
116
117
  });
@@ -176,8 +177,8 @@ export class AggregateBase extends FeatureBase {
176
177
  * This method should run after checkConfiguration, which may reset the agent's info/runtime object that is used here.
177
178
  */
178
179
  doOnceForAllAggregate(agentRef) {
179
- if (!agentRef.runtime.obfuscator) agentRef.runtime.obfuscator = new Obfuscator(agentRef);
180
- this.obfuscator = agentRef.runtime.obfuscator;
180
+ // Note: obfuscator is now created per-feature for their specific event types
181
+
181
182
  if (!agentRef.runtime.harvester) agentRef.runtime.harvester = new Harvester(agentRef);
182
183
  }
183
184
 
@@ -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
  /**
@@ -77,6 +78,9 @@ function register(agentRef, target) {
77
78
  type: V2_TYPES.BA
78
79
  };
79
80
  const timings = findScriptTimings();
81
+
82
+ // Track MFE vitals for this entity
83
+ const vitals = trackMFEVitals(target.id);
80
84
  const attrs = {};
81
85
 
82
86
  // Only define attributes getter if it doesn't already exist
@@ -165,7 +169,8 @@ function register(agentRef, target) {
165
169
  return attrs;
166
170
  },
167
171
  target,
168
- timings
172
+ timings,
173
+ vitals
169
174
  }
170
175
  };
171
176
 
@@ -193,9 +198,17 @@ function register(agentRef, target) {
193
198
  // only ever report the timings the first time this is called
194
199
  if (timings.reportedAt) return;
195
200
  timings.reportedAt = now();
201
+
202
+ // Disconnect observers and capture current values, store in timings obj for visibility
203
+ vitals.disconnect();
204
+ timings.fcp = vitals.fcp;
205
+ timings.lcp = vitals.lcp;
206
+ timings.cls = vitals.cls;
207
+ timings.inp = vitals.inp;
196
208
  const timeToFetch = timings.fetchEnd - timings.fetchStart; // fetchStart to fetchEnd
197
209
  const timeToExecute = timings.scriptEnd - timings.scriptStart; // scriptStart to scriptEnd
198
- api.recordCustomEvent('MicroFrontEndTiming', {
210
+
211
+ const eventData = {
199
212
  assetUrl: timings.asset,
200
213
  // the url of the script that was registered, or undefined if it could not be determined (inline or no match)
201
214
  assetType: timings.type,
@@ -210,8 +223,17 @@ function register(agentRef, target) {
210
223
  // fetchStart to fetchEnd
211
224
  timeToLoad: timeToFetch + timeToExecute,
212
225
  // fetch time and script time together
213
- timeToRegister: timings.registeredAt // timestamp when register() was called
214
- });
226
+ timeToRegister: timings.registeredAt,
227
+ // timestamp when register() was called
228
+ 'nr.vitals.fcp': vitals.fcp || null,
229
+ // FCP vital object with value and metadata
230
+ 'nr.vitals.lcp': vitals.lcp || null,
231
+ // LCP vital object with value and metadata
232
+ 'nr.vitals.cls': vitals.cls || null,
233
+ // CLS vital object with value and metadata
234
+ 'nr.vitals.inp': vitals.inp || null // INP vital object with value and metadata
235
+ };
236
+ api.recordCustomEvent('MicroFrontEndTiming', eventData);
215
237
  }
216
238
 
217
239
  /**