@newrelic/browser-agent 1.320.0 → 1.320.1-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 (121) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/cjs/common/config/runtime.js +1 -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/event-listener/event-listener-opts.js +1 -1
  6. package/dist/cjs/common/harvest/connector.js +200 -0
  7. package/dist/cjs/common/harvest/harvester.js +5 -8
  8. package/dist/cjs/common/harvest/send.js +8 -7
  9. package/dist/cjs/common/payloads/payloads.js +3 -2
  10. package/dist/cjs/common/serialize/bel-serializer.js +17 -4
  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/mfe-vitals.js +34 -11
  16. package/dist/cjs/common/window/page-visibility.js +1 -1
  17. package/dist/cjs/features/ajax/aggregate/index.js +19 -8
  18. package/dist/cjs/features/page_view_event/aggregate/index.js +2 -6
  19. package/dist/cjs/features/page_view_event/aggregate_v2/index.js +109 -0
  20. package/dist/cjs/features/page_view_event/instrument/index.js +6 -1
  21. package/dist/cjs/features/page_view_timing/aggregate/index.js +33 -14
  22. package/dist/cjs/features/session_replay/instrument/index.js +9 -0
  23. package/dist/cjs/features/session_replay/shared/recorder.js +2 -1
  24. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +16 -4
  25. package/dist/cjs/features/soft_navigations/aggregate/interaction.js +10 -3
  26. package/dist/cjs/features/utils/aggregate-base.js +0 -12
  27. package/dist/cjs/features/utils/instrument-base.js +72 -29
  28. package/dist/cjs/loaders/agent.js +2 -1
  29. package/dist/cjs/loaders/api/consent.js +2 -2
  30. package/dist/cjs/loaders/features/features.js +2 -1
  31. package/dist/esm/common/config/runtime.js +1 -0
  32. package/dist/esm/common/constants/env.cdn.js +1 -1
  33. package/dist/esm/common/constants/env.npm.js +1 -1
  34. package/dist/esm/common/event-listener/event-listener-opts.js +1 -1
  35. package/dist/esm/common/harvest/connector.js +193 -0
  36. package/dist/esm/common/harvest/harvester.js +5 -8
  37. package/dist/esm/common/harvest/send.js +10 -9
  38. package/dist/esm/common/payloads/payloads.js +3 -2
  39. package/dist/esm/common/serialize/bel-serializer.js +17 -4
  40. package/dist/esm/common/timer/interaction-timer.js +2 -2
  41. package/dist/esm/common/unload/eol.js +3 -3
  42. package/dist/esm/common/util/console.js +2 -1
  43. package/dist/esm/common/util/feature-flags.js +2 -1
  44. package/dist/esm/common/v2/mfe-vitals.js +34 -11
  45. package/dist/esm/common/window/page-visibility.js +1 -1
  46. package/dist/esm/features/ajax/aggregate/index.js +19 -8
  47. package/dist/esm/features/page_view_event/aggregate/index.js +2 -6
  48. package/dist/esm/features/page_view_event/aggregate_v2/index.js +100 -0
  49. package/dist/esm/features/page_view_event/instrument/index.js +6 -1
  50. package/dist/esm/features/page_view_timing/aggregate/index.js +33 -14
  51. package/dist/esm/features/session_replay/instrument/index.js +9 -0
  52. package/dist/esm/features/session_replay/shared/recorder.js +2 -1
  53. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +16 -4
  54. package/dist/esm/features/soft_navigations/aggregate/interaction.js +10 -3
  55. package/dist/esm/features/utils/aggregate-base.js +0 -12
  56. package/dist/esm/features/utils/instrument-base.js +71 -28
  57. package/dist/esm/loaders/agent.js +2 -1
  58. package/dist/esm/loaders/api/consent.js +2 -2
  59. package/dist/esm/loaders/features/features.js +1 -0
  60. package/dist/tsconfig.tsbuildinfo +1 -1
  61. package/dist/types/common/config/runtime.d.ts.map +1 -1
  62. package/dist/types/common/event-listener/event-listener-opts.d.ts +1 -1
  63. package/dist/types/common/harvest/connector.d.ts +6 -0
  64. package/dist/types/common/harvest/connector.d.ts.map +1 -0
  65. package/dist/types/common/harvest/harvester.d.ts.map +1 -1
  66. package/dist/types/common/harvest/send.d.ts.map +1 -1
  67. package/dist/types/common/payloads/payloads.d.ts +3 -2
  68. package/dist/types/common/payloads/payloads.d.ts.map +1 -1
  69. package/dist/types/common/serialize/bel-serializer.d.ts +9 -1
  70. package/dist/types/common/serialize/bel-serializer.d.ts.map +1 -1
  71. package/dist/types/common/unload/eol.d.ts +1 -1
  72. package/dist/types/common/unload/eol.d.ts.map +1 -1
  73. package/dist/types/common/util/console.d.ts +2 -1
  74. package/dist/types/common/util/console.d.ts.map +1 -1
  75. package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -1
  76. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  77. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  78. package/dist/types/features/page_view_event/aggregate_v2/index.d.ts +20 -0
  79. package/dist/types/features/page_view_event/aggregate_v2/index.d.ts.map +1 -0
  80. package/dist/types/features/page_view_event/instrument/index.d.ts.map +1 -1
  81. package/dist/types/features/page_view_timing/aggregate/index.d.ts +0 -1
  82. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  83. package/dist/types/features/session_replay/instrument/index.d.ts +1 -0
  84. package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
  85. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  86. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  87. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
  88. package/dist/types/features/utils/aggregate-base.d.ts +0 -5
  89. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  90. package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
  91. package/dist/types/loaders/agent.d.ts.map +1 -1
  92. package/dist/types/loaders/features/features.d.ts +1 -0
  93. package/dist/types/loaders/features/features.d.ts.map +1 -1
  94. package/package.json +2 -2
  95. package/src/common/config/runtime.js +1 -0
  96. package/src/common/event-listener/event-listener-opts.js +1 -1
  97. package/src/common/harvest/connector.js +168 -0
  98. package/src/common/harvest/harvester.js +7 -9
  99. package/src/common/harvest/send.js +9 -7
  100. package/src/common/payloads/payloads.js +3 -2
  101. package/src/common/serialize/bel-serializer.js +13 -4
  102. package/src/common/timer/interaction-timer.js +2 -2
  103. package/src/common/unload/eol.js +3 -3
  104. package/src/common/util/console.js +2 -1
  105. package/src/common/util/feature-flags.js +1 -1
  106. package/src/common/v2/mfe-vitals.js +39 -12
  107. package/src/common/window/page-visibility.js +1 -1
  108. package/src/features/ajax/aggregate/index.js +12 -9
  109. package/src/features/page_view_event/aggregate/index.js +2 -6
  110. package/src/features/page_view_event/aggregate_v2/index.js +97 -0
  111. package/src/features/page_view_event/instrument/index.js +6 -1
  112. package/src/features/page_view_timing/aggregate/index.js +24 -15
  113. package/src/features/session_replay/instrument/index.js +9 -0
  114. package/src/features/session_replay/shared/recorder.js +2 -1
  115. package/src/features/soft_navigations/aggregate/ajax-node.js +10 -7
  116. package/src/features/soft_navigations/aggregate/interaction.js +6 -5
  117. package/src/features/utils/aggregate-base.js +0 -12
  118. package/src/features/utils/instrument-base.js +71 -25
  119. package/src/loaders/agent.js +2 -1
  120. package/src/loaders/api/consent.js +2 -2
  121. package/src/loaders/features/features.js +1 -0
@@ -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, isWorkerScope, isBrowserScope } from '../constants/runtime'
@@ -22,9 +22,9 @@ if (isWorkerScope) {
22
22
  * This is used, for example, to submit a final harvest and send all remaining data on best-effort.
23
23
  * @param {function} cb - func to run before or during the last reliable event or time of an env's life span
24
24
  */
25
- export function subscribeToEOL (cb, capturePhase) {
25
+ export function subscribeToEOL (cb) {
26
26
  if (isBrowserScope) {
27
- subscribeToVisibilityChange(cb, true, capturePhase) // when user switches tab or hides window, esp. mobile scenario
27
+ subscribeToVisibilityChange(cb, true) // when user switches tab or hides window, esp. mobile scenario
28
28
  } else if (isWorkerScope) {
29
29
  globalScope.cleanupTasks.push(cb) // close() should run these tasks before quitting thread
30
30
  }
@@ -68,7 +68,7 @@ import { dispatchGlobalEvent } from '../dispatch/global-event'
68
68
  * | 50 | Failed to connect. Cannot allow registered API. |
69
69
  * | 51 | Container agent is not available to register with. Can not connect. |
70
70
  * | 52 | Unexpected problem encountered. There should be at least one app for harvest! |
71
- * | 53 | Did not receive a valid entityGuid from connection response. |
71
+ * | 53 | Failed to parse connect response. |
72
72
  * | 54 | An experimental feature is being used. Support can not be offered for issues. |
73
73
  * | 55 | Register API has been disabled on the container agent. |
74
74
  * | 56 | Could not find a matching entity to store data. |
@@ -94,6 +94,7 @@ import { dispatchGlobalEvent } from '../dispatch/global-event'
94
94
  * | 76 | Agent rejected post message, could not match with existing entity. |
95
95
  * | 77 | Agent rejected post message, could not validate origin. |
96
96
  * | 78 | RegisteredIframeEntity could not determine parent origin and will not register, to avoid trusting messages from any origin. |
97
+ * | 79 | Unable to initialize Connector and/or Harvester. |
97
98
  *
98
99
  * @param {number} code The warning code to emit, which will be used to link to the warning code documentation
99
100
  * @param {*} [secondary] Secondary data to include, usually an extra message, error or object
@@ -19,7 +19,7 @@ export function activateFeatures (flags, agentRef) {
19
19
  // let any window level subscribers know that the agent is running, per install docs
20
20
  dispatchGlobalEvent({
21
21
  loaded: true,
22
- drained: true,
22
+ drained: !agentRef.init.feature_flags.includes('rum_v2'), // v2 RUM always call activateFeatures before any group is drained (in waitForFlags)
23
23
  type: 'lifecycle',
24
24
  name: 'load',
25
25
  feature: undefined,
@@ -124,6 +124,9 @@ const observePerformance = (observers, config, onEntry) => {
124
124
  export function trackMFEVitals (target, timings) {
125
125
  let fcpObservedAt
126
126
  let lcpObservedAt
127
+ // Once disconnect() runs, no further vitals data should be written - even from an observer callback that was
128
+ // already in flight when disconnect() was called.
129
+ let disconnected = false
127
130
 
128
131
  const getTimeRelativeToScriptStart = (capturedAt) => {
129
132
  if (capturedAt == null) return capturedAt
@@ -132,12 +135,21 @@ export function trackMFEVitals (target, timings) {
132
135
 
133
136
  const vitals = {
134
137
  fcp: {
135
- get value () { return getTimeRelativeToScriptStart(fcpObservedAt) },
136
- set value (v) { fcpObservedAt = v }
138
+ // A clock freeze/tab suspend between script registration and first paint can make this value huge once the
139
+ // tab resumes. Rather than report a multi-hour "FCP", drop it so bad data doesn't reach the backend.
140
+ get value () {
141
+ const relative = getTimeRelativeToScriptStart(fcpObservedAt)
142
+ return relative > CORRELATION_STALE_THRESHOLD_MS ? undefined : relative
143
+ },
144
+ set value (v) { if (!disconnected) fcpObservedAt = v }
137
145
  },
138
146
  lcp: {
139
- get value () { return getTimeRelativeToScriptStart(lcpObservedAt) },
140
- set value (v) { lcpObservedAt = v }
147
+ // LCP can never occur before FCP, so if FCP was dropped as invalid, LCP is derived from the same untrustworthy clock and must be dropped too.
148
+ get value () {
149
+ if (vitals.fcp.value === undefined) return undefined
150
+ return getTimeRelativeToScriptStart(lcpObservedAt)
151
+ },
152
+ set value (v) { if (!disconnected) lcpObservedAt = v }
141
153
  },
142
154
  cls: {
143
155
  value: undefined
@@ -159,15 +171,22 @@ export function trackMFEVitals (target, timings) {
159
171
  }, CORRELATION_STALE_THRESHOLD_MS)
160
172
 
161
173
  const populateVitalMinimums = () => {
162
- fcpObservedAt ??= now()
174
+ if (disconnected) return
175
+ if (fcpObservedAt == null) {
176
+ const capturedAt = now()
177
+ // If the very first observed render already exceeds the window, the clock is untrustworthy for this
178
+ // MFE (e.g. tab was frozen/suspended between registration and paint) - shut everything down instead
179
+ // of seeding vitals with data derived from it.
180
+ if (getTimeRelativeToScriptStart(capturedAt) > CORRELATION_STALE_THRESHOLD_MS) {
181
+ vitals.disconnect()
182
+ return
183
+ }
184
+ fcpObservedAt = capturedAt
185
+ }
163
186
  lcpObservedAt ??= now()
164
187
  vitals.cls.value ??= 0
165
188
  }
166
189
 
167
- // if the MFE has already rendered something on the page before we could set up listeners, just populate vital minimums immediately
168
- const existingRoots = globalScope.document?.querySelectorAll(`[data-nr-mfe-id="${escapeSelectorValue(target.id)}"]`)
169
- if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums()
170
-
171
190
  // Track FCP - first contentful paint
172
191
  const fcpObs = observeMutations(target, (_, obs) => {
173
192
  // An observed "FCP" means _something_ rendered, so at minimum we can populate all the baseline values for the vitals
@@ -183,12 +202,13 @@ export function trackMFEVitals (target, timings) {
183
202
 
184
203
  try {
185
204
  resizeObs = new globalScope.ResizeObserver((entries) => {
205
+ if (disconnected) return
186
206
  entries.forEach((entry) => {
187
207
  try {
188
208
  const size = entry.contentRect.width * entry.contentRect.height
189
209
  if (size > largestSize) {
190
210
  largestSize = size
191
- lcpObservedAt = now()
211
+ vitals.lcp.value = now()
192
212
  }
193
213
  resizeObs.unobserve(entry.target)
194
214
  } catch (e) {
@@ -245,7 +265,7 @@ export function trackMFEVitals (target, timings) {
245
265
  // Track CLS - cumulative layout shift
246
266
  // Initialize CLS to 0 if browser supports it
247
267
  observePerformance(observers, { type: 'layout-shift', buffered: true }, (entry) => {
248
- if (entry.hadRecentInput) return
268
+ if (disconnected || entry.hadRecentInput) return
249
269
  ;(entry.sources || []).some(source => {
250
270
  if (isInMFE(source.node, target)) {
251
271
  // an observed "CLS" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
@@ -259,7 +279,7 @@ export function trackMFEVitals (target, timings) {
259
279
 
260
280
  // Track INP - interaction to next paint
261
281
  observePerformance(observers, { type: 'event', buffered: true, durationThreshold: 40 }, (entry) => {
262
- if (!entry.interactionId || !isInMFE(entry.target, target)) return
282
+ if (disconnected || !entry.interactionId || !isInMFE(entry.target, target)) return
263
283
  if (vitals.inp.value === undefined || entry.duration > vitals.inp.value) {
264
284
  // an observed "INP" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
265
285
  populateVitalMinimums()
@@ -276,6 +296,7 @@ export function trackMFEVitals (target, timings) {
276
296
 
277
297
  // Disconnect all observers
278
298
  vitals.disconnect = () => {
299
+ disconnected = true
279
300
  // Disconnect all observers
280
301
  observers.forEach(obs => {
281
302
  try {
@@ -317,5 +338,11 @@ export function trackMFEVitals (target, timings) {
317
338
  globalScope.addEventListener(type, vitals.disconnect, { once: true, passive: true })
318
339
  })
319
340
 
341
+ // If the MFE has already rendered something on the page before we could set up listeners, just populate vital
342
+ // minimums immediately. Done last, once `observers` is fully populated and `vitals.disconnect` has its real
343
+ // implementation, so a stale-clock detection here can actually tear everything down instead of no-op'ing.
344
+ const existingRoots = globalScope.document?.querySelectorAll(`[data-nr-mfe-id="${escapeSelectorValue(target.id)}"]`)
345
+ if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums()
346
+
320
347
  return vitals
321
348
  }
@@ -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
 
@@ -157,7 +157,7 @@ export class Aggregate extends AggregateBase {
157
157
  serializer (eventBuffer) {
158
158
  if (!eventBuffer.length) return
159
159
 
160
- const { addString, addStringWithTruncation } = createStringAdders(getAddStringContext, this.obfuscator)
160
+ const { addString, addStringRaw, addStringWithTruncation } = createStringAdders(getAddStringContext, this.obfuscator)
161
161
 
162
162
  let payload = 'bel.7;'
163
163
 
@@ -174,16 +174,16 @@ export class Aggregate extends AggregateBase {
174
174
  numeric(event.endTime - event.startTime),
175
175
  numeric(0), // callbackEnd
176
176
  numeric(0), // no callbackDuration for non-SPA events
177
- addString(event.method),
177
+ addStringRaw(event.method),
178
178
  numeric(event.status),
179
179
  addString(event.domain),
180
180
  addString(event.path),
181
181
  numeric(event.requestSize),
182
182
  numeric(event.responseSize),
183
183
  event.type === 'fetch' ? 1 : '',
184
- addString(0), // nodeId
185
- nullable(event.spanId, addString, true) + // guid
186
- nullable(event.traceId, addString, true) + // traceId
184
+ addStringRaw(0), // nodeId
185
+ nullable(event.spanId, addStringRaw, true) + // guid
186
+ nullable(event.traceId, addStringRaw, true) + // traceId
187
187
  nullable(event.spanTimestamp, numeric, false) // timestamp
188
188
  ]
189
189
 
@@ -193,12 +193,15 @@ export class Aggregate extends AggregateBase {
193
193
  const jsAttributes = this.agentRef.info.jsAttributes
194
194
 
195
195
  // Regular attributes: obfuscate only
196
- const regularAttrs = addCustomAttributes({
196
+ let regularAttrs = addCustomAttributes({
197
197
  ...(jsAttributes || {}),
198
- [AJAX_ID]: event[AJAX_ID], // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
199
198
  ...(event.targetAttributes || {}), // used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
200
199
  ...(event.gql || {})
201
- }, addString)
200
+ }, { addKey: addStringRaw, addVal: addString })
201
+
202
+ regularAttrs.push(...addCustomAttributes({
203
+ [AJAX_ID]: event[AJAX_ID] // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
204
+ }, { addKey: addStringRaw, addVal: addStringRaw }))
202
205
 
203
206
  // Payload attributes: obfuscate then truncate
204
207
  const payloadAttrs = addCustomAttributes({
@@ -207,7 +210,7 @@ export class Aggregate extends AggregateBase {
207
210
  ...(event.requestQuery ? { requestQuery: event.requestQuery } : {}),
208
211
  ...(event.responseBody ? { responseBody: event.responseBody } : {}),
209
212
  ...(event.responseHeaders ? { responseHeaders: event.responseHeaders } : {})
210
- }, addStringWithTruncation)
213
+ }, { addKey: addStringRaw, addVal: addStringWithTruncation })
211
214
 
212
215
  const attrParts = [...regularAttrs, ...payloadAttrs]
213
216
  fields.unshift(numeric(attrParts.length))
@@ -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 () {
@@ -23,6 +23,7 @@ import { loadTime } from '../../../common/vitals/load-time'
23
23
  import { webdriverDetected } from '../../../common/util/webdriver-detection'
24
24
  import { cleanURL } from '../../../common/url/clean-url'
25
25
  import { EVENT_TYPES } from '../../../common/constants/events'
26
+ import { createStringAdders } from '../../../common/payloads/payloads'
26
27
 
27
28
  export class Aggregate extends AggregateBase {
28
29
  static featureName = FEATURE_NAME
@@ -58,7 +59,11 @@ export class Aggregate extends AggregateBase {
58
59
  const { name, value, attrs } = cumulativeLayoutShift.current
59
60
  if (value === undefined) return
60
61
  this.addTiming(name, value * 1000, attrs)
61
- }, 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. */
62
67
 
63
68
  this.drain()
64
69
  })
@@ -93,6 +98,8 @@ export class Aggregate extends AggregateBase {
93
98
  attrs.cls = cumulativeLayoutShift.current.value
94
99
  }
95
100
 
101
+ attrs.webdriverDetected = webdriverDetected
102
+
96
103
  const timing = {
97
104
  name,
98
105
  value,
@@ -125,17 +132,13 @@ export class Aggregate extends AggregateBase {
125
132
  })
126
133
  }
127
134
 
128
- appendGlobalCustomAttributes (timing) {
129
- var timingAttributes = timing.attrs || {}
135
+ #getGlobalCustomAttributes () {
136
+ const reservedAttributes = ['size', 'eid', 'cls', 'type', 'fid', 'elTag', 'elUrl', 'net-type',
137
+ 'net-etype', 'net-rtt', 'net-dlink', 'webdriverDetected']
130
138
 
131
- var reservedAttributes = ['size', 'eid', 'cls', 'type', 'fid', 'elTag', 'elUrl', 'net-type',
132
- 'net-etype', 'net-rtt', 'net-dlink']
133
- Object.entries(this.agentRef.info.jsAttributes || {}).forEach(([key, val]) => {
134
- if (reservedAttributes.indexOf(key) < 0) {
135
- timingAttributes[key] = val
136
- }
137
- })
138
- timingAttributes.webdriverDetected = webdriverDetected
139
+ return Object.fromEntries(
140
+ Object.entries(this.agentRef.info.jsAttributes || {}).filter(([key]) => !reservedAttributes.includes(key))
141
+ )
139
142
  }
140
143
 
141
144
  preHarvestChecks () {
@@ -146,7 +149,9 @@ export class Aggregate extends AggregateBase {
146
149
  // serialize array of timing data
147
150
  serializer (eventBuffer) {
148
151
  if (!eventBuffer?.length) return ''
149
- var addString = getAddStringContext(this.obfuscator)
152
+ const { addString, addStringRaw } = createStringAdders(getAddStringContext, this.obfuscator)
153
+ const keepOrigValue = { addKey: addStringRaw, addVal: addStringRaw }
154
+ const obfuscateValue = { addKey: addStringRaw, addVal: addString }
150
155
 
151
156
  var payload = 'bel.6;'
152
157
 
@@ -154,12 +159,16 @@ export class Aggregate extends AggregateBase {
154
159
  var timing = eventBuffer[i]
155
160
 
156
161
  payload += 'e,'
157
- payload += addString(timing.name) + ','
162
+ payload += addStringRaw(timing.name) + ','
158
163
  payload += nullable(timing.value, numeric, false) + ','
159
164
 
160
- this.appendGlobalCustomAttributes(timing)
165
+ const userAttrs = this.#getGlobalCustomAttributes()
166
+
167
+ const { pageUrl, ...systemReservedAttrs } = timing.attrs || {}
168
+ var attrParts = addCustomAttributes(systemReservedAttrs, keepOrigValue)
169
+ attrParts.push(...addCustomAttributes(userAttrs, obfuscateValue))
170
+ if (pageUrl) attrParts.push(...addCustomAttributes({ pageUrl }, obfuscateValue))
161
171
 
162
- var attrParts = addCustomAttributes(timing.attrs, addString)
163
172
  if (attrParts && attrParts.length > 0) {
164
173
  payload += numeric(attrParts.length) + ';' + attrParts.join(';')
165
174
  }
@@ -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
  }
@@ -42,7 +42,7 @@ export class AjaxNode extends BelNode {
42
42
  }
43
43
 
44
44
  serialize (parentStartTimestamp, agentRef, ajaxObfuscator) {
45
- const { addString, addStringWithTruncation } = createStringAdders(getAddStringContext, ajaxObfuscator)
45
+ const { addString, addStringRaw, addStringWithTruncation } = createStringAdders(getAddStringContext, ajaxObfuscator)
46
46
 
47
47
  const nodeList = []
48
48
 
@@ -54,22 +54,25 @@ export class AjaxNode extends BelNode {
54
54
  numeric(this.end - this.start), // end is relative to start
55
55
  numeric(this.callbackEnd - this.end), // callbackEnd is relative to end
56
56
  numeric(this.callbackDuration), // not relative
57
- addString(this.method),
57
+ addStringRaw(this.method),
58
58
  numeric(this.status),
59
59
  addString(this.domain),
60
60
  addString(this.path),
61
61
  numeric(this.txSize),
62
62
  numeric(this.rxSize),
63
63
  this.requestedWith,
64
- addString(this.nodeId),
65
- nullable(this.spanId, addString, true) + nullable(this.traceId, addString, true) + nullable(this.spanTimestamp, numeric)
64
+ addStringRaw(this.nodeId),
65
+ nullable(this.spanId, addStringRaw, true) + nullable(this.traceId, addStringRaw, true) + nullable(this.spanTimestamp, numeric)
66
66
  ]
67
67
  // Regular attributes: obfuscate only
68
68
  const regularAttrs = addCustomAttributes({
69
- [AJAX_ID]: this[AJAX_ID],
70
69
  ...(this.targetAttributes || {}),
71
70
  ...(this.gql || {})
72
- }, addString)
71
+ }, { addKey: addStringRaw, addVal: addString })
72
+
73
+ regularAttrs.push(...addCustomAttributes({
74
+ [AJAX_ID]: this[AJAX_ID]
75
+ }, { addKey: addStringRaw, addVal: addStringRaw }))
73
76
 
74
77
  // Payload attributes: obfuscate then truncate
75
78
  const payloadAttrs = addCustomAttributes({
@@ -78,7 +81,7 @@ export class AjaxNode extends BelNode {
78
81
  ...(this.requestQuery ? { requestQuery: this.requestQuery } : {}),
79
82
  ...(this.responseBody ? { responseBody: this.responseBody } : {}),
80
83
  ...(this.responseHeaders ? { responseHeaders: this.responseHeaders } : {})
81
- }, addStringWithTruncation)
84
+ }, { addKey: addStringRaw, addVal: addStringWithTruncation })
82
85
 
83
86
  let allAttachedNodes = [...regularAttrs, ...payloadAttrs]
84
87
  this.children.forEach(node => allAttachedNodes.push(node.serialize())) // no children is expected under ajax nodes at this time
@@ -9,6 +9,7 @@ import { now } from '../../../common/timing/now'
9
9
  import { cleanURL } from '../../../common/url/clean-url'
10
10
  import { NODE_TYPE, INTERACTION_STATUS, INTERACTION_TYPE, API_TRIGGER_NAME, IPL_TRIGGER_NAME, NO_LONG_TASK_WINDOW } from '../constants'
11
11
  import { BelNode } from './bel-node'
12
+ import { createStringAdders } from '../../../common/payloads/payloads'
12
13
 
13
14
  /**
14
15
  * link https://github.com/newrelic/nr-querypack/blob/main/schemas/bel/7.qpschema
@@ -139,7 +140,7 @@ export class Interaction extends BelNode {
139
140
  */
140
141
  serialize (firstStartTimeOfPayload, agentRef, interactionObfuscator, ajaxObfuscator) {
141
142
  const isFirstIxnOfPayload = firstStartTimeOfPayload === undefined
142
- const addString = getAddStringContext(interactionObfuscator)
143
+ const { addString, addStringRaw } = createStringAdders(getAddStringContext, interactionObfuscator)
143
144
  const nodeList = []
144
145
  let ixnType
145
146
  if (this.trigger === IPL_TRIGGER_NAME) ixnType = INTERACTION_TYPE.INITIAL_PAGE_LOAD
@@ -154,7 +155,7 @@ export class Interaction extends BelNode {
154
155
  numeric(this.end - this.start), // end -- relative to start
155
156
  numeric(0), // callbackEnd -- relative to start; not used by BrowserInteraction events so these are always 0
156
157
  numeric(0), // not relative; always 0 for BrowserInteraction
157
- addString(this.trigger),
158
+ addStringRaw(this.trigger),
158
159
  addString(cleanURL(this.initialPageURL, true)),
159
160
  addString(cleanURL(this.oldURL, true)),
160
161
  addString(cleanURL(this.newURL, true)),
@@ -162,12 +163,12 @@ export class Interaction extends BelNode {
162
163
  ixnType,
163
164
  nullable(this.queueTime, numeric, true) + nullable(this.appTime, numeric, true) +
164
165
  nullable(this.oldRoute, addString, true) + nullable(this.newRoute, addString, true) +
165
- addString(this.id),
166
- addString(this.nodeId),
166
+ addStringRaw(this.id),
167
+ addStringRaw(this.nodeId),
167
168
  nullable(this.firstPaint, numeric, true) + nullable(this.firstContentfulPaint, numeric)
168
169
  ]
169
170
  const customAttributes = { ...agentRef.info.jsAttributes, ...this.customAttributes } // attrs specific to this interaction should have precedence over the general custom attrs
170
- const allAttachedNodes = addCustomAttributes(customAttributes || {}, addString) // start with all custom attributes
171
+ const allAttachedNodes = addCustomAttributes(customAttributes || {}, { addKey: addStringRaw, addVal: addString }) // start with all custom attributes
171
172
  if (agentRef.info.atts) allAttachedNodes.push('a,' + addString(agentRef.info.atts)) // add apm provided attributes
172
173
  /* Querypack encoder+decoder quirkiness:
173
174
  - 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.)
@@ -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