@newrelic/browser-agent 1.320.1 → 1.321.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 (169) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cjs/common/config/runtime.js +3 -0
  3. package/dist/cjs/common/constants/env.cdn.js +1 -1
  4. package/dist/cjs/common/constants/env.npm.js +1 -1
  5. package/dist/cjs/common/dom/selector-path.js +1 -1
  6. package/dist/cjs/common/event-listener/event-listener-opts.js +1 -1
  7. package/dist/cjs/common/harvest/connector.js +202 -0
  8. package/dist/cjs/common/harvest/harvester.js +5 -8
  9. package/dist/cjs/common/harvest/send.js +8 -7
  10. package/dist/cjs/common/session/session-entity.js +1 -1
  11. package/dist/cjs/common/timer/interaction-timer.js +2 -2
  12. package/dist/cjs/common/unload/eol.js +3 -3
  13. package/dist/cjs/common/util/console.js +2 -1
  14. package/dist/cjs/common/util/feature-flags.js +2 -1
  15. package/dist/cjs/common/v2/constants.js +21 -0
  16. package/dist/cjs/common/v2/utils.js +25 -23
  17. package/dist/cjs/common/window/page-visibility.js +1 -1
  18. package/dist/cjs/common/wrap/wrap-events.js +4 -3
  19. package/dist/cjs/common/wrap/wrap-fetch.js +2 -2
  20. package/dist/cjs/common/wrap/wrap-function.js +1 -1
  21. package/dist/cjs/common/wrap/wrap-history.js +3 -2
  22. package/dist/cjs/common/wrap/wrap-xhr.js +2 -2
  23. package/dist/cjs/features/ajax/aggregate/index.js +1 -1
  24. package/dist/cjs/features/generic_events/aggregate/index.js +1 -1
  25. package/dist/cjs/features/generic_events/instrument/index.js +1 -1
  26. package/dist/cjs/features/jserrors/aggregate/index.js +5 -5
  27. package/dist/cjs/features/logging/instrument/index.js +1 -1
  28. package/dist/cjs/features/page_view_event/aggregate/index.js +2 -6
  29. package/dist/cjs/features/page_view_event/aggregate_v2/index.js +109 -0
  30. package/dist/cjs/features/page_view_event/instrument/index.js +6 -1
  31. package/dist/cjs/features/page_view_timing/aggregate/index.js +5 -1
  32. package/dist/cjs/features/session_replay/instrument/index.js +9 -0
  33. package/dist/cjs/features/session_replay/shared/recorder.js +2 -1
  34. package/dist/cjs/features/session_replay/shared/utils.js +3 -5
  35. package/dist/cjs/features/session_trace/instrument/index.js +3 -3
  36. package/dist/cjs/features/soft_navigations/instrument/index.js +2 -2
  37. package/dist/cjs/features/utils/aggregate-base.js +0 -12
  38. package/dist/cjs/features/utils/instrument-base.js +72 -29
  39. package/dist/cjs/loaders/agent.js +2 -1
  40. package/dist/cjs/loaders/api/consent.js +2 -2
  41. package/dist/cjs/loaders/api/register.js +26 -13
  42. package/dist/cjs/loaders/configure/configure.js +14 -0
  43. package/dist/cjs/loaders/features/features.js +2 -1
  44. package/dist/esm/common/config/runtime.js +3 -0
  45. package/dist/esm/common/constants/env.cdn.js +1 -1
  46. package/dist/esm/common/constants/env.npm.js +1 -1
  47. package/dist/esm/common/dom/selector-path.js +1 -1
  48. package/dist/esm/common/event-listener/event-listener-opts.js +1 -1
  49. package/dist/esm/common/harvest/connector.js +195 -0
  50. package/dist/esm/common/harvest/harvester.js +5 -8
  51. package/dist/esm/common/harvest/send.js +10 -9
  52. package/dist/esm/common/session/session-entity.js +1 -1
  53. package/dist/esm/common/timer/interaction-timer.js +2 -2
  54. package/dist/esm/common/unload/eol.js +3 -3
  55. package/dist/esm/common/util/console.js +2 -1
  56. package/dist/esm/common/util/feature-flags.js +2 -1
  57. package/dist/esm/common/v2/constants.js +15 -0
  58. package/dist/esm/common/v2/utils.js +24 -22
  59. package/dist/esm/common/window/page-visibility.js +1 -1
  60. package/dist/esm/common/wrap/wrap-events.js +4 -3
  61. package/dist/esm/common/wrap/wrap-fetch.js +2 -2
  62. package/dist/esm/common/wrap/wrap-function.js +1 -1
  63. package/dist/esm/common/wrap/wrap-history.js +3 -2
  64. package/dist/esm/common/wrap/wrap-xhr.js +2 -2
  65. package/dist/esm/features/ajax/aggregate/index.js +2 -2
  66. package/dist/esm/features/generic_events/aggregate/index.js +1 -1
  67. package/dist/esm/features/generic_events/instrument/index.js +1 -1
  68. package/dist/esm/features/jserrors/aggregate/index.js +6 -6
  69. package/dist/esm/features/logging/instrument/index.js +1 -1
  70. package/dist/esm/features/page_view_event/aggregate/index.js +2 -6
  71. package/dist/esm/features/page_view_event/aggregate_v2/index.js +100 -0
  72. package/dist/esm/features/page_view_event/instrument/index.js +6 -1
  73. package/dist/esm/features/page_view_timing/aggregate/index.js +5 -1
  74. package/dist/esm/features/session_replay/instrument/index.js +9 -0
  75. package/dist/esm/features/session_replay/shared/recorder.js +2 -1
  76. package/dist/esm/features/session_replay/shared/utils.js +3 -5
  77. package/dist/esm/features/session_trace/instrument/index.js +3 -3
  78. package/dist/esm/features/soft_navigations/instrument/index.js +2 -2
  79. package/dist/esm/features/utils/aggregate-base.js +0 -12
  80. package/dist/esm/features/utils/instrument-base.js +71 -28
  81. package/dist/esm/loaders/agent.js +2 -1
  82. package/dist/esm/loaders/api/consent.js +2 -2
  83. package/dist/esm/loaders/api/register.js +19 -6
  84. package/dist/esm/loaders/configure/configure.js +14 -0
  85. package/dist/esm/loaders/features/features.js +1 -0
  86. package/dist/tsconfig.tsbuildinfo +1 -1
  87. package/dist/types/common/config/runtime.d.ts.map +1 -1
  88. package/dist/types/common/event-listener/event-listener-opts.d.ts +1 -1
  89. package/dist/types/common/harvest/connector.d.ts +6 -0
  90. package/dist/types/common/harvest/connector.d.ts.map +1 -0
  91. package/dist/types/common/harvest/harvester.d.ts.map +1 -1
  92. package/dist/types/common/harvest/send.d.ts.map +1 -1
  93. package/dist/types/common/unload/eol.d.ts +1 -1
  94. package/dist/types/common/unload/eol.d.ts.map +1 -1
  95. package/dist/types/common/util/console.d.ts +2 -1
  96. package/dist/types/common/util/console.d.ts.map +1 -1
  97. package/dist/types/common/v2/constants.d.ts +9 -0
  98. package/dist/types/common/v2/constants.d.ts.map +1 -0
  99. package/dist/types/common/v2/utils.d.ts +36 -11
  100. package/dist/types/common/v2/utils.d.ts.map +1 -1
  101. package/dist/types/common/wrap/wrap-events.d.ts +2 -1
  102. package/dist/types/common/wrap/wrap-events.d.ts.map +1 -1
  103. package/dist/types/common/wrap/wrap-history.d.ts +2 -1
  104. package/dist/types/common/wrap/wrap-history.d.ts.map +1 -1
  105. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  106. package/dist/types/features/page_view_event/aggregate_v2/index.d.ts +20 -0
  107. package/dist/types/features/page_view_event/aggregate_v2/index.d.ts.map +1 -0
  108. package/dist/types/features/page_view_event/instrument/index.d.ts.map +1 -1
  109. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  110. package/dist/types/features/session_replay/instrument/index.d.ts +1 -0
  111. package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
  112. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  113. package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
  114. package/dist/types/features/session_trace/instrument/index.d.ts.map +1 -1
  115. package/dist/types/features/utils/aggregate-base.d.ts +0 -5
  116. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  117. package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
  118. package/dist/types/loaders/agent.d.ts.map +1 -1
  119. package/dist/types/loaders/api/register.d.ts.map +1 -1
  120. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  121. package/dist/types/loaders/features/features.d.ts +1 -0
  122. package/dist/types/loaders/features/features.d.ts.map +1 -1
  123. package/package.json +1 -1
  124. package/src/common/config/runtime.js +3 -0
  125. package/src/common/dom/selector-path.js +1 -1
  126. package/src/common/event-listener/event-listener-opts.js +1 -1
  127. package/src/common/harvest/connector.js +170 -0
  128. package/src/common/harvest/harvester.js +7 -9
  129. package/src/common/harvest/send.js +9 -7
  130. package/src/common/session/session-entity.js +1 -1
  131. package/src/common/timer/interaction-timer.js +2 -2
  132. package/src/common/unload/eol.js +3 -3
  133. package/src/common/util/console.js +2 -1
  134. package/src/common/util/feature-flags.js +1 -1
  135. package/src/common/v2/constants.js +15 -0
  136. package/src/common/v2/utils.js +23 -21
  137. package/src/common/window/page-visibility.js +1 -1
  138. package/src/common/wrap/wrap-events.js +4 -3
  139. package/src/common/wrap/wrap-fetch.js +2 -2
  140. package/src/common/wrap/wrap-function.js +1 -1
  141. package/src/common/wrap/wrap-history.js +3 -2
  142. package/src/common/wrap/wrap-xhr.js +2 -2
  143. package/src/features/ajax/aggregate/index.js +2 -2
  144. package/src/features/generic_events/aggregate/index.js +1 -1
  145. package/src/features/generic_events/instrument/index.js +1 -1
  146. package/src/features/jserrors/aggregate/index.js +6 -6
  147. package/src/features/logging/instrument/index.js +1 -1
  148. package/src/features/page_view_event/aggregate/index.js +2 -6
  149. package/src/features/page_view_event/aggregate_v2/index.js +97 -0
  150. package/src/features/page_view_event/instrument/index.js +6 -1
  151. package/src/features/page_view_timing/aggregate/index.js +5 -1
  152. package/src/features/session_replay/instrument/index.js +9 -0
  153. package/src/features/session_replay/shared/recorder.js +2 -1
  154. package/src/features/session_replay/shared/utils.js +3 -5
  155. package/src/features/session_trace/instrument/index.js +3 -3
  156. package/src/features/soft_navigations/instrument/index.js +2 -2
  157. package/src/features/utils/aggregate-base.js +0 -12
  158. package/src/features/utils/instrument-base.js +71 -25
  159. package/src/loaders/agent.js +2 -1
  160. package/src/loaders/api/consent.js +2 -2
  161. package/src/loaders/api/register.js +16 -6
  162. package/src/loaders/configure/configure.js +12 -0
  163. package/src/loaders/features/features.js +1 -0
  164. package/dist/cjs/common/url/location.js +0 -18
  165. package/dist/esm/common/url/location.js +0 -12
  166. package/dist/types/common/url/location.d.ts +0 -10
  167. package/dist/types/common/url/location.d.ts.map +0 -1
  168. package/src/common/url/__mocks__/location.js +0 -5
  169. package/src/common/url/location.js +0 -12
@@ -18,9 +18,10 @@ const HISTORY_FNS = ['pushState', 'replaceState']
18
18
  * Wraps the `pushState` and `replaceState` methods of `window.history` and returns a corresponding event emitter
19
19
  * scoped to the history object.
20
20
  * @param {Object} sharedEE - The shared event emitter on which a new scoped event emitter will be based.
21
+ * @param {*} [agentRef] - The agent reference, used to attribute wrapped calls to the correct v2 target.
21
22
  * @returns {Object} Scoped event emitter with a debug ID of `history`.
22
23
  */
23
- export function wrapHistory (sharedEE) {
24
+ export function wrapHistory (sharedEE, agentRef) {
24
25
  const ee = scopedEE(sharedEE)
25
26
 
26
27
  // Notice if our wrapping never ran yet, the falsy NaN will not early return; but if it has,
@@ -29,7 +30,7 @@ export function wrapHistory (sharedEE) {
29
30
  if (!isBrowserScope || wrapped[ee.debugId]++) return ee
30
31
  wrapped[ee.debugId] = 1 // otherwise, first feature to wrap history
31
32
 
32
- var wrapFn = wfn(ee)
33
+ var wrapFn = wfn(ee, undefined, agentRef)
33
34
  /*
34
35
  * For objects that will be instantiated more than once, we wrap the object's prototype methods. The history object
35
36
  * is instantiated only once, so we can wrap its methods directly--and we must wrap the history methods directly as
@@ -37,8 +37,8 @@ export function wrapXhr (sharedEE, agentRef) {
37
37
  if (wrapped[ee.debugId]++) return ee
38
38
  wrapped[ee.debugId] = 1 // otherwise, first feature to wrap XHR
39
39
 
40
- wrapEvents(baseEE) // wrap-events patches XMLHttpRequest.prototype.addEventListener for us
41
- var wrapFn = wfn(ee)
40
+ wrapEvents(baseEE, agentRef) // wrap-events patches XMLHttpRequest.prototype.addEventListener for us
41
+ var wrapFn = wfn(ee, undefined, agentRef)
42
42
 
43
43
  var OrigXHR = globalScope.XMLHttpRequest
44
44
  var MutationObserver = globalScope.MutationObserver
@@ -14,7 +14,7 @@ import { gosNREUMOriginals } from '../../../common/window/nreum'
14
14
  import { hasGQLErrors, parseGQL } from './gql'
15
15
  import { canCapturePayload, isLikelyHumanReadable, parseQueryString, createStringAdders } from '../../../common/payloads/payloads'
16
16
  import { Obfuscator } from '../../../common/util/obfuscate'
17
- import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/v2/utils'
17
+ import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate, isMfeTarget } from '../../../common/v2/utils'
18
18
  import { EVENT_TYPES } from '../../../common/constants/events'
19
19
  import { generateUuid } from '../../../common/ids/unique-id'
20
20
 
@@ -137,7 +137,7 @@ export class Aggregate extends AggregateBase {
137
137
  if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length)
138
138
 
139
139
  /** make a copy of the event for the MFE target if it exists */
140
- if (target) {
140
+ if (isMfeTarget(target)) {
141
141
  this.events.add({ ...event, targetAttributes: getVersion2Attributes(target, this) })
142
142
  if (shouldDuplicate(target, this)) this.reportContainerEvent({ ...event, targetAttributes: getVersion2DuplicationAttributes(target, this) }, ctx)
143
143
  } else {
@@ -252,7 +252,7 @@ export class Aggregate extends AggregateBase {
252
252
  }, this.featureName, this.ee)
253
253
 
254
254
  if (agentRef.init.feature_flags.includes('websockets') || agentRef.init.web_sockets?.enabled) {
255
- registerHandler('ws-complete', (nrData, targets = [undefined]) => {
255
+ registerHandler('ws-complete', (nrData, targets = [agentRef.runtime.v2Target]) => {
256
256
  targets.forEach(target => {
257
257
  const event = {
258
258
  ...nrData,
@@ -68,7 +68,7 @@ export class Instrument extends InstrumentBase {
68
68
  if (isBrowserScope) {
69
69
  wrapFetch(this.ee, agentRef)
70
70
  wrapXhr(this.ee, agentRef)
71
- historyEE = wrapHistory(this.ee)
71
+ historyEE = wrapHistory(this.ee, agentRef)
72
72
 
73
73
  if (agentRef.init.user_actions.enabled) {
74
74
  OBSERVED_EVENTS.forEach(eventType =>
@@ -19,7 +19,7 @@ import { AggregateBase } from '../../utils/aggregate-base'
19
19
  import { now } from '../../../common/timing/now'
20
20
  import { Obfuscator } from '../../../common/util/obfuscate'
21
21
  import { evaluateInternalError } from './internal-errors'
22
- import { getRegisteredTargetsFromFilename, getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/v2/utils'
22
+ import { getRegisteredTargetsFromFilename, getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate, isMfeTarget } from '../../../common/v2/utils'
23
23
  import { buildCauseString } from './cause-string'
24
24
  import { ShortCircuit } from '../../../common/util/short-circuit'
25
25
  import { EVENT_TYPES } from '../../../common/constants/events'
@@ -209,7 +209,7 @@ export class Aggregate extends AggregateBase {
209
209
  if (targets.length) break
210
210
  }
211
211
  }
212
- if (!targets.length) targets.push(undefined)
212
+ if (!targets.length) targets.push(this.agentRef.runtime.v2Target)
213
213
  }
214
214
  return targets
215
215
  }
@@ -253,7 +253,7 @@ export class Aggregate extends AggregateBase {
253
253
  if (filterOutput?.group) params.errorGroup = filterOutput.group
254
254
 
255
255
  // Should only decorate "hasReplay" for the container agent, so check if the target matches the config
256
- if (hasReplay && !target) params.hasReplay = hasReplay
256
+ if (hasReplay && !isMfeTarget(target)) params.hasReplay = hasReplay
257
257
  /**
258
258
  * The bucketHash is different from the params.stackHash because the params.stackHash is based on the canonicalized
259
259
  * stack trace and is used downstream in NR1 to attempt to group the same errors across different browsers. However,
@@ -289,7 +289,7 @@ export class Aggregate extends AggregateBase {
289
289
 
290
290
  // Trace sends the error in its payload, and both trace & replay simply listens for any error to occur.
291
291
  const jsErrorEvent = [type, bucketHash, params, newMetrics, customAttributes]
292
- if (!target) handle('trace-jserror', jsErrorEvent, undefined, FEATURE_NAMES.sessionTrace, this.ee)
292
+ if (!isMfeTarget(target)) handle('trace-jserror', jsErrorEvent, undefined, FEATURE_NAMES.sessionTrace, this.ee)
293
293
  // still send EE events for other features such as above, but stop this one from aggregating internal data
294
294
  if (this.blocked) return
295
295
 
@@ -297,7 +297,7 @@ export class Aggregate extends AggregateBase {
297
297
  customAttributes.socketId = err.__newrelic.socketId
298
298
  }
299
299
 
300
- if (!target) {
300
+ if (!isMfeTarget(target)) {
301
301
  const softNavInUse = Boolean(this.agentRef.features?.[FEATURE_NAMES.softNav])
302
302
  if (softNavInUse) { // pass the error to soft nav for evaluation - it will return it via 'returnJserror' when interaction is resolved
303
303
  handle('jserror', [jsErrorEvent], undefined, FEATURE_NAMES.softNav, this.ee)
@@ -307,7 +307,7 @@ export class Aggregate extends AggregateBase {
307
307
  }
308
308
 
309
309
  // always add directly if scoped to a sub-entity, the other pathways above will be deterministic if the main agent should procede
310
- if (target) this.#storeJserrorForHarvest(jsErrorEvent, {}, target)
310
+ if (isMfeTarget(target)) this.#storeJserrorForHarvest(jsErrorEvent, {}, target)
311
311
  }
312
312
 
313
313
  /**
@@ -31,7 +31,7 @@ export class Instrument extends InstrumentBase {
31
31
  })
32
32
 
33
33
  /** emitted by wrap-logger function */
34
- this.ee.on('wrap-logger-end', function handleLog ([message], _, __, targets = []) {
34
+ this.ee.on('wrap-logger-end', function handleLog ([message], _, __, targets = [agentRef.runtime.v2Target]) {
35
35
  const { level, customAttributes, autoCaptured } = this
36
36
  targets.forEach(target => {
37
37
  bufferLog(instanceEE, message, customAttributes, level, autoCaptured, target)
@@ -132,9 +132,7 @@ export class Aggregate extends AggregateBase {
132
132
  }
133
133
  this.events.add(body)
134
134
 
135
- if (this.agentRef.runtime.harvester.triggerHarvestFor(this, {
136
- sendEmptyBody: true
137
- }).ranSend) this.sentRum = true
135
+ if (this.agentRef.runtime.harvester.triggerHarvestFor(this).ranSend) this.sentRum = true
138
136
  }
139
137
 
140
138
  serializer (eventBuffer) { // this is necessary because PVE sends a single item rather than an array; in the case of undefined, this prevents sending [null] as body
@@ -162,9 +160,7 @@ export class Aggregate extends AggregateBase {
162
160
  super.postHarvestCleanup({ sent, retry: retry && !hasCachedRumResponse }) // this will set isRetrying & re-buffer the body if request is to be retried
163
161
 
164
162
  if (this.isRetrying && this.retries++ < 1) { // Only retry once
165
- setTimeout(() => this.agentRef.runtime.harvester.triggerHarvestFor(this, {
166
- sendEmptyBody: true
167
- }), 5000) // Retry sending the RUM event after 5 seconds
163
+ setTimeout(() => this.agentRef.runtime.harvester.triggerHarvestFor(this), 5000) // Retry sending the RUM event after 5 seconds
168
164
  return
169
165
  }
170
166
 
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import { globalScope, isBrowserScope, originTime, getNavigationEntry } from '../../../common/constants/runtime'
6
+ import { addPT, addPN } from '../../../common/timing/nav-timing'
7
+ import { stringify } from '../../../common/util/stringify'
8
+ import * as CONSTANTS from '../constants'
9
+ import { getActivatedFeaturesFlags } from '../aggregate/initialized-features'
10
+ import { AggregateBase } from '../../utils/aggregate-base'
11
+ import { firstContentfulPaint } from '../../../common/vitals/first-contentful-paint'
12
+ import { firstPaint } from '../../../common/vitals/first-paint'
13
+ import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte'
14
+ import { Obfuscator } from '../../../common/util/obfuscate'
15
+ import { webdriverDetected } from '../../../common/util/webdriver-detection'
16
+ import { EVENT_TYPES } from '../../../common/constants/events'
17
+
18
+ export class Aggregate extends AggregateBase {
19
+ static featureName = CONSTANTS.FEATURE_NAME
20
+
21
+ #timeToFirstByte = 0
22
+ #firstByteToWindowLoad = 0 // our "frontend" duration
23
+ #firstByteToDomContent = 0 // our "dom processing" duration
24
+ #obfuscator
25
+
26
+ constructor (agentRef) {
27
+ super(agentRef, CONSTANTS.FEATURE_NAME)
28
+ this.#obfuscator = new Obfuscator(agentRef, EVENT_TYPES.PVE)
29
+
30
+ if (isBrowserScope) {
31
+ timeToFirstByte.subscribe(({ value, attrs }) => {
32
+ const navEntry = attrs.navigationEntry
33
+ this.#timeToFirstByte = Math.max(value, this.#timeToFirstByte)
34
+ this.#firstByteToWindowLoad = Math.max(Math.round(navEntry.loadEventEnd - this.#timeToFirstByte), this.#firstByteToWindowLoad) // our "frontend" duration
35
+ this.#firstByteToDomContent = Math.max(Math.round(navEntry.domContentLoadedEventEnd - this.#timeToFirstByte), this.#firstByteToDomContent) // our "dom processing" duration
36
+ })
37
+ }
38
+
39
+ this.waitForFlags(([])).then(() => {
40
+ const body = this.#obfuscator.traverseAndObfuscateEvents({ ja: { ...agentRef.info.jsAttributes, webdriverDetected } })
41
+ this.events.add(body)
42
+ this.drain()
43
+ })
44
+ }
45
+
46
+ queryStringsBuilder () {
47
+ const info = this.agentRef.info
48
+ const measures = {}
49
+
50
+ if (info.queueTime) measures.qt = info.queueTime
51
+ if (info.applicationTime) measures.ap = info.applicationTime
52
+
53
+ // These 3 values should've been recorded after load and before this func runs. They are part of the minimum required for PageView events to be created.
54
+ // Following PR #428, which demands that the BA must drive entity indexing, these need to be sent even outside of the main window context where PerformanceTiming
55
+ // or PerformanceNavigationTiming do not exists. Hence, they'll be filled in by 0s instead in, for example, worker threads that still init the PVE module.
56
+ measures.be = this.#timeToFirstByte
57
+ measures.fe = this.#firstByteToWindowLoad
58
+ measures.dc = this.#firstByteToDomContent
59
+
60
+ const queryParameters = {
61
+ at: info.atts,
62
+ af: getActivatedFeaturesFlags(this.agentRef).join(','),
63
+ igp: this.agentRef.runtime.appMetadata.igp,
64
+ ...measures,
65
+ fp: firstPaint.current.value,
66
+ fcp: firstContentfulPaint.current.value
67
+ }
68
+ if (globalScope.performance) {
69
+ const navTimingEntry = getNavigationEntry()
70
+ if (navTimingEntry) { // Navigation Timing level 2 API that replaced PerformanceTiming & PerformanceNavigation
71
+ const perf = ({
72
+ timing: addPT(originTime, navTimingEntry, {}),
73
+ navigation: addPN(navTimingEntry, {})
74
+ })
75
+ queryParameters.perf = stringify(perf)
76
+ } else if (typeof PerformanceTiming !== 'undefined') { // Modern Safari iFrames and Safari pre-15 do not support level 2 timing.
77
+ const perf = ({
78
+ timing: addPT(originTime, globalScope.performance.timing, {}, true),
79
+ navigation: addPN(globalScope.performance.navigation, {})
80
+ })
81
+ queryParameters.perf = stringify(perf)
82
+ }
83
+ }
84
+ const { correctedOriginTime } = this.agentRef.runtime.timeKeeper
85
+ if (correctedOriginTime) queryParameters.timestamp = correctedOriginTime // a "PageView" is set to represent the page's (server adjusted) origin time rather than when event is recorded/harvested
86
+
87
+ return queryParameters
88
+ }
89
+
90
+ get harvestEndpointVersion () {
91
+ return 2
92
+ }
93
+
94
+ serializer (eventBuffer) { // this is necessary because PVE sends a single item rather than an array; in the case of undefined, this prevents sending [null] as body
95
+ return eventBuffer[0]
96
+ }
97
+ }
@@ -20,7 +20,12 @@ export class Instrument extends InstrumentBase {
20
20
  /** feature specific APIs */
21
21
  setupSetPageViewNameAPI(agentRef)
22
22
 
23
- this.importAggregator(agentRef, () => import(/* webpackChunkName: "page_view_event-aggregate" */ '../aggregate'))
23
+ this.importAggregator(agentRef, () => {
24
+ if (agentRef.init.feature_flags.includes('rum_v2')) {
25
+ return import(/* webpackChunkName: "page_view_event-aggregate" */ '../aggregate_v2')
26
+ }
27
+ return import(/* webpackChunkName: "page_view_event-aggregate" */ '../aggregate')
28
+ })
24
29
  }
25
30
 
26
31
  setupInspectionEvents () {
@@ -59,7 +59,11 @@ export class Aggregate extends AggregateBase {
59
59
  const { name, value, attrs } = cumulativeLayoutShift.current
60
60
  if (value === undefined) return
61
61
  this.addTiming(name, value * 1000, attrs)
62
- }, true, true) // CLS node should only reports on vis change rather than on every change
62
+ }, true, true) // CLS node should only report on vis change rather than on every change.
63
+ /* NOTE: the capture=true is required -- empirically verified against a real browser (Chrome) for CLS
64
+ timing node to reliably be in the same final harvest as the rest, alongside Harvester#startTimer's
65
+ queueMicrotask deferral of the EOL harvest itself. Dropping this flag caused CLS to be split into its own,
66
+ separate, later harvest instead of going out together with pageHide/INP. */
63
67
 
64
68
  this.drain()
65
69
  })
@@ -46,6 +46,10 @@ export class Instrument extends InstrumentBase {
46
46
  }) // could handle specific fail-state behaviors with a .catch block here
47
47
  }
48
48
 
49
+ // If the aggregate never successfully loads (missing session prerequisite, or the aggregate module itself
50
+ // fails to import/construct), any preload recording that already started needs to be stopped.
51
+ this.abortHandler = this.#abort
52
+
49
53
  this.importAggregator(this.agentRef, () => import(/* webpackChunkName: "session_replay-aggregate" */ '../aggregate'), this)
50
54
 
51
55
  /** If the recorder is running, we can pass error events on to the agg to help it switch to full mode later */
@@ -109,6 +113,11 @@ export class Instrument extends InstrumentBase {
109
113
  }) // could handle specific fail-state behaviors with a .catch block here
110
114
  }
111
115
  }
116
+
117
+ /** Stops any preload recording that already started if the aggregate never ends up successfully loading. */
118
+ #abort () {
119
+ this.recorder?.stopRecording()
120
+ }
112
121
  }
113
122
 
114
123
  export const SessionReplay = Instrument
@@ -204,7 +204,8 @@ export class Recorder {
204
204
 
205
205
  // We are making an effort to try to keep payloads manageable for unloading. If they reach the unload limit before their interval,
206
206
  // it will send immediately. This often happens on the first snapshot, which can be significantly larger than the other payloads.
207
- if (((this.events.hasSnapshot && this.events.hasMeta) || payloadSize > IDEAL_PAYLOAD_SIZE) && !this.isErrorMode) {
207
+ // (the aggregate may not exist yet/at all if its own bootstrap hasn't finished or failed -- nothing to harvest for in that case.)
208
+ if (((this.events.hasSnapshot && this.events.hasMeta) || payloadSize > IDEAL_PAYLOAD_SIZE) && !this.isErrorMode && this.srInstrument.featAggregate) {
208
209
  // if we've made it to the ideal size of ~16kb before the interval timer, we should send early.
209
210
  this.agentRef.runtime.harvester.triggerHarvestFor(this.srInstrument.featAggregate)
210
211
  }
@@ -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 { gosNREUMOriginals } from '../../../common/window/nreum'
@@ -17,10 +17,8 @@ export function isPreloadAllowed (agentInit) {
17
17
 
18
18
  export function customMasker (text, element) {
19
19
  try {
20
- if (typeof element?.type === 'string') {
21
- if (element.type.toLowerCase() === 'password') return '*'.repeat(text?.length || 0)
22
- if (element?.dataset?.nrUnmask !== undefined || element?.classList?.contains('nr-unmask')) return text
23
- }
20
+ if (typeof element?.type === 'string' && element.type.toLowerCase() === 'password') return '*'.repeat(text?.length || 0)
21
+ if (element?.dataset?.nrUnmask !== undefined || element?.classList?.contains('nr-unmask')) return text
24
22
  } catch (err) {
25
23
  // likely an element was passed to this handler that was invalid and was missing attributes or methods
26
24
  }
@@ -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 { handle } from '../../../common/event-emitter/handle'
@@ -33,8 +33,8 @@ export class Instrument extends InstrumentBase {
33
33
  }
34
34
 
35
35
  const thisInstrumentEE = this.ee
36
- wrapHistory(thisInstrumentEE)
37
- this.eventsEE = wrapEvents(thisInstrumentEE)
36
+ wrapHistory(thisInstrumentEE, agentRef)
37
+ this.eventsEE = wrapEvents(thisInstrumentEE, agentRef)
38
38
 
39
39
  this.eventsEE.on(FN_START, function (args, target) {
40
40
  this.bstStart = now()
@@ -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 { gosNREUMOriginals } from '../../../common/window/nreum'
@@ -30,7 +30,7 @@ export class Instrument extends InstrumentBase {
30
30
 
31
31
  if (!isBrowserScope || !gosNREUMOriginals().o.MO) return // soft navigations is not supported outside web env or browsers without the mutation observer API
32
32
 
33
- const historyEE = wrapHistory(this.ee)
33
+ const historyEE = wrapHistory(this.ee, agentRef)
34
34
  try {
35
35
  this.removeOnAbort = new AbortController()
36
36
  } catch (e) {}
@@ -5,7 +5,6 @@
5
5
  import { FeatureBase } from './feature-base'
6
6
  import { drain } from '../../common/drain/drain'
7
7
  import { FEATURE_NAMES } from '../../loaders/features/features'
8
- import { Harvester } from '../../common/harvest/harvester'
9
8
  import { EventBuffer } from './event-buffer'
10
9
  import { handle } from '../../common/event-emitter/handle'
11
10
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../metrics/constants'
@@ -21,7 +20,6 @@ export class AggregateBase extends FeatureBase {
21
20
  */
22
21
  constructor (agentRef, featureName) {
23
22
  super(agentRef, featureName)
24
- this.doOnceForAllAggregate(agentRef)
25
23
 
26
24
  /** @type {Boolean} indicates if custom attributes are combined in each event payload for size estimation purposes. this is set to true in derived classes that need to evaluate custom attributes separately from the event payload */
27
25
  this.customAttributesAreSeparate = false
@@ -178,16 +176,6 @@ export class AggregateBase extends FeatureBase {
178
176
  this.events.clearSave(this.harvestOpts)
179
177
  }
180
178
 
181
- /**
182
- * These are actions related to shared resources that should be initialized once by whichever feature Aggregate subclass loads first.
183
- * This method should run after checkConfiguration, which may reset the agent's info/runtime object that is used here.
184
- */
185
- doOnceForAllAggregate (agentRef) {
186
- // Note: obfuscator is now created per-feature for their specific event types
187
-
188
- if (!agentRef.runtime.harvester) agentRef.runtime.harvester = new Harvester(agentRef)
189
- }
190
-
191
179
  /**
192
180
  * Report a supportability metric
193
181
  * @param {*} metricName The tag of the name matching the Angler aggregation tag
@@ -25,6 +25,7 @@ import { SESSION_ERROR } from '../../common/constants/agent-constants'
25
25
  import { handle } from '../../common/event-emitter/handle'
26
26
 
27
27
  const checkedAgents = new WeakSet()
28
+ const runtimeBootstrapPromises = new WeakMap()
28
29
 
29
30
  /**
30
31
  * Base class for instrumenting a feature.
@@ -103,28 +104,13 @@ export class InstrumentBase extends FeatureBase {
103
104
  return
104
105
  }
105
106
 
106
- let session
107
- try {
108
- if (canEnableSessionTracking(agentRef.init)) { // would require some setup before certain features start
109
- const { setupAgentSession } = await import(/* webpackChunkName: "session-manager" */ './agent-session')
110
- session = setupAgentSession(agentRef)
111
- }
107
+ try { // in the interest of keeping loader file size small, some modules are lazy-loaded as part of the larger async chunk
108
+ await ensureRuntimeBootstrap(agentRef, this.ee, this.featureName)
112
109
  } catch (e) {
113
- warn(20, e)
114
- this.ee.emit('internal-error', [e])
115
- handle(SESSION_ERROR, [e], undefined, this.featureName, this.ee)
116
- }
117
-
118
- if (agentRef.init.api.register.allow_iframe_bridge) {
119
- try {
120
- // This chunk doesn't exist in the lite build (see webpack IgnorePlugin config) since none
121
- // of lite's features wire up agent.register -- guard against that rather than letting an
122
- // unhandled rejection surface if this flag is ever set on a lite page.
123
- const { setupIframeMFEMessageListener } = await import(/* webpackChunkName: "iframe-message-handler" */ '../../loaders/configure/iframe-message-handler')
124
- setupIframeMFEMessageListener(agentRef)
125
- } catch (e) {
126
- warn(23, e)
127
- }
110
+ warn(79, e)
111
+ this.ee.abort() // failed Connector or Harvester will cause entire agent to shutdown
112
+ this.loadedSuccessfully(false)
113
+ return
128
114
  }
129
115
 
130
116
  /**
@@ -132,23 +118,24 @@ export class InstrumentBase extends FeatureBase {
132
118
  * it's only responsible for aborting its one specific feature, rather than all.
133
119
  */
134
120
  try {
135
- if (!this.#shouldImportAgg(this.featureName, session, agentRef.init)) {
121
+ if (!this.#shouldImportAgg(this.featureName, agentRef.runtime.session, agentRef.init)) {
122
+ this.abortHandler?.()
136
123
  drain(this.agentRef, this.featureName)
137
124
  this.loadedSuccessfully(false) // aggregate module isn't loaded at all
138
125
  return
139
126
  }
140
127
  const { Aggregate } = await fetchAggregator()
128
+
141
129
  this.featAggregate = new Aggregate(agentRef, argsObjFromInstrument)
142
130
 
143
- agentRef.runtime.harvester.initializedAggregates.push(this.featAggregate) // "subscribe" the feature to future harvest intervals (PVE will start the timer)
131
+ agentRef.runtime.harvester.initializedAggregates.push(this.featAggregate) // "subscribe" the feature to future harvest intervals
144
132
  this.loadedSuccessfully(true)
145
133
  } catch (e) {
146
134
  warn(34, e)
147
- this.abortHandler?.() // undo any important alterations made to the page
135
+ this.abortHandler?.()
148
136
  // not supported yet but nice to do: "abort" this agent's EE for this feature specifically
149
137
  drain(this.agentRef, this.featureName, true)
150
138
  this.loadedSuccessfully(false)
151
- if (this.ee) this.ee.abort()
152
139
  }
153
140
  }
154
141
 
@@ -206,3 +193,62 @@ export class InstrumentBase extends FeatureBase {
206
193
  }
207
194
  }
208
195
  }
196
+
197
+ /**
198
+ * Lazily initializes the shared runtime bootstrap for a given agent exactly once. This loads session setup, Connector, and
199
+ * Harvester through shared async chunks and returns the same in-flight Promise to any concurrent callers.
200
+ *
201
+ * Session setup failures are reported but do not stop bootstrap. Connector or Harvester failures are allowed to bubble so the caller can abort the agent.
202
+ *
203
+ * @param {Object} agentRef - The agent reference object.
204
+ * @param {Object} ee - The feature event emitter used for error reporting.
205
+ * @param {string} featureName - The feature name used when reporting session setup errors.
206
+ * @returns {Promise<void>} Resolves when the shared bootstrap completes.
207
+ */
208
+ async function ensureRuntimeBootstrap (agentRef, ee, featureName) {
209
+ if (runtimeBootstrapPromises.has(agentRef)) return runtimeBootstrapPromises.get(agentRef)
210
+
211
+ const bootstrapPromise = (async () => {
212
+ if (canEnableSessionTracking(agentRef.init)) {
213
+ try {
214
+ const { setupAgentSession } = await import(/* webpackChunkName: "session-manager" */ './agent-session')
215
+ setupAgentSession(agentRef) // sets agentRef.runtime.session, if successful
216
+ } catch (e) {
217
+ warn(20, e)
218
+ ee.emit('internal-error', [e])
219
+ handle(SESSION_ERROR, [e], undefined, featureName, ee)
220
+ }
221
+ }
222
+ if (agentRef.init.api.register.allow_iframe_bridge) {
223
+ try {
224
+ // This chunk doesn't exist in the lite build (see webpack IgnorePlugin config) since none
225
+ // of lite's features wire up agent.register -- guard against that rather than letting an
226
+ // unhandled rejection surface if this flag is ever set on a lite page.
227
+ const { setupIframeMFEMessageListener } = await import(/* webpackChunkName: "iframe-message-handler" */ '../../loaders/configure/iframe-message-handler')
228
+ setupIframeMFEMessageListener(agentRef)
229
+ } catch (e) {
230
+ warn(23, e)
231
+ }
232
+ }
233
+
234
+ const [{ Connector }, { Harvester }] = await Promise.all([
235
+ import(/* webpackChunkName: "connector" */ '../../common/harvest/connector'),
236
+ import(/* webpackChunkName: "harvester" */ '../../common/harvest/harvester')
237
+ ])
238
+ agentRef.runtime.connector = new Connector(agentRef)
239
+ agentRef.runtime.harvester = new Harvester(agentRef)
240
+
241
+ /* Wait for auto-started features' aggregates to load before starting the harvest timer, so Harvester's EOL
242
+ listener registers after aggregate-level listeners like web-vitals' CWV APIs. Not awaited here to avoid
243
+ deadlock, since every feature awaits this same bootstrap promise before its own aggregate load.
244
+ Only auto-started features are waited on: an autoStart:false feature only registers in drainRegistry once
245
+ `start` is called, so filtering on that registry excludes any feature that's never started instead of
246
+ blocking startTimer() forever. */
247
+ const autoStartedFeatures = Object.values(agentRef.features).filter(feature => agentRef.runtime.drainRegistry.has(feature.featureName))
248
+ Promise.all(autoStartedFeatures.map(feature => feature.onAggregateImported))
249
+ .then(() => agentRef.runtime.harvester.startTimer())
250
+ })()
251
+
252
+ runtimeBootstrapPromises.set(agentRef, bootstrapPromise)
253
+ await bootstrapPromise
254
+ }
@@ -93,10 +93,11 @@ export class Agent extends AgentBase {
93
93
  try {
94
94
  const enabledFeatures = getEnabledFeatures(this.init)
95
95
  const featuresToStart = [...this.desiredFeatures]
96
+ const isRumV2Enabled = this.init.feature_flags.includes('rum_v2')
96
97
 
97
98
  featuresToStart.sort((a, b) => featurePriority[a.featureName] - featurePriority[b.featureName])
98
99
  featuresToStart.forEach(InstrumentCtor => {
99
- if (!enabledFeatures[InstrumentCtor.featureName] && InstrumentCtor.featureName !== FEATURE_NAMES.pageViewEvent) return // PVE is required to run even if it's marked disabled
100
+ if (!enabledFeatures[InstrumentCtor.featureName] && (isRumV2Enabled || InstrumentCtor.featureName !== FEATURE_NAMES.pageViewEvent)) return // PVE is required to run even if it's marked disabled unless rum_v2 is enabled
100
101
 
101
102
  const dependencies = getFeatureDependencyNames(InstrumentCtor.featureName)
102
103
  const missingDependencies = dependencies.filter(featName => !(featName in this.features)) // any other feature(s) this depends on should've been initialized on prior iterations by priority order
@@ -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 { prefix, CONSENT } from './constants'
@@ -23,7 +23,7 @@ export function setupConsentAPI (agent) {
23
23
  pveInst.onAggregateImported.then((loaded) => {
24
24
  const pveAgg = pveInst.featAggregate
25
25
  if (loaded && !pveAgg.sentRum) {
26
- pveAgg.sendRum()
26
+ pveAgg.sendRum?.() // only the old v1 aggregate has a sendRum method that acts as a blocker for the rest of the agent; v2 aggregate will auto report when harvester becomes unblocked
27
27
  }
28
28
  })
29
29
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { handle } from '../../common/event-emitter/handle'
6
6
  import { warn } from '../../common/util/console'
7
- import { V2_TYPES } from '../../common/v2/utils'
7
+ import { V2_TYPES } from '../../common/v2/constants'
8
8
  import { FEATURE_NAMES } from '../features/features'
9
9
  import { now } from '../../common/timing/now'
10
10
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../../features/metrics/constants'
@@ -20,6 +20,7 @@ import { subscribeToPageUnload } from '../../common/window/page-visibility'
20
20
  import { findScriptTimings } from '../../common/v2/script-tracker'
21
21
  import { trackMFEVitals } from '../../common/v2/mfe-vitals'
22
22
  import { generateRandomHexString } from '../../common/ids/unique-id'
23
+ import { cleanURL } from '../../common/url/clean-url'
23
24
 
24
25
  /**
25
26
  * @typedef {import('./register-api-types').RegisterAPI} RegisterAPI
@@ -82,11 +83,15 @@ function register (agentRef, target) {
82
83
  type: V2_TYPES.BA
83
84
  }
84
85
 
86
+ // The script timings for this entity, which will be used to populate part of the MicroFrontEndTiming custom event.
85
87
  const timings = findScriptTimings(target)
86
88
 
87
- // Track MFE vitals for this entity
89
+ // Track MFE vitals for this entity, which will be used to populate part of the MicroFrontEndTiming custom event.
88
90
  const vitals = trackMFEVitals(target, timings)
89
91
 
92
+ // the URL of the page at the time this entity was registered, which will be used to populate part of the MicroFrontEndTiming custom event.
93
+ const registerUrl = cleanURL('' + location)
94
+
90
95
  const attrs = {}
91
96
 
92
97
  // Only define attributes getter if it doesn't already exist
@@ -199,6 +204,11 @@ function register (agentRef, target) {
199
204
  const eventData = {
200
205
  assetUrl: timings.asset, // the url of the script that was registered, or undefined if it could not be determined (inline or no match)
201
206
  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
207
+ registerUrl, // the url of the page at the time this entity was registered
208
+ deregisterUrl: cleanURL('' + location), // the url of the page at the time this entity was deregistered (or unloaded)
209
+ // generic_events' addEvent() injects pageUrl/currentUrl on every custom event by default; override them away here in favor of registerUrl/deregisterUrl above
210
+ pageUrl: undefined,
211
+ currentUrl: undefined,
202
212
  timeAlive: timings.reportedAt - timings.registeredAt, // registeredAt to reportedAt
203
213
  timeToBeRequested: timings.fetchStart, // origin to fetchStart
204
214
  timeToExecute, // scriptStart to scriptEnd
@@ -206,10 +216,10 @@ function register (agentRef, target) {
206
216
  timeToLoad: timeToFetch + timeToExecute, // fetch time and script time together
207
217
  timeToRegister: timings.registeredAt, // timestamp when register() was called
208
218
  // leave room to extend these with more data keys as needed
209
- ...(vitals.fcp.value >= 0 && { 'nr.vitals.fcp.value': vitals.fcp.value }), // FCP vital object with value and metadata
210
- ...(vitals.lcp.value >= 0 && { 'nr.vitals.lcp.value': vitals.lcp.value }), // LCP vital object with value and metadata
211
- ...(vitals.cls.value >= 0 && { 'nr.vitals.cls.value': vitals.cls.value }), // CLS vital object with value and metadata
212
- ...(vitals.inp.value >= 0 && { 'nr.vitals.inp.value': vitals.inp.value }) // INP vital object with value and metadata
219
+ ...(vitals.fcp.value >= 0 && { 'vitals.fcp.value': vitals.fcp.value }), // FCP vital object with value and metadata
220
+ ...(vitals.lcp.value >= 0 && { 'vitals.lcp.value': vitals.lcp.value }), // LCP vital object with value and metadata
221
+ ...(vitals.cls.value >= 0 && { 'vitals.cls.value': vitals.cls.value }), // CLS vital object with value and metadata
222
+ ...(vitals.inp.value >= 0 && { 'vitals.inp.value': vitals.inp.value }) // INP vital object with value and metadata
213
223
  }
214
224
 
215
225
  api.recordCustomEvent('MicroFrontEndTiming', eventData)