@newrelic/browser-agent 1.313.1 → 1.314.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 (59) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/cjs/common/constants/env.cdn.js +1 -1
  3. package/dist/cjs/common/constants/env.npm.js +1 -1
  4. package/dist/cjs/common/dom/selector-path.js +12 -3
  5. package/dist/cjs/common/timing/time-keeper.js +18 -6
  6. package/dist/cjs/common/vitals/load-time.js +5 -2
  7. package/dist/cjs/features/ajax/aggregate/index.js +6 -2
  8. package/dist/cjs/features/ajax/constants.js +4 -3
  9. package/dist/cjs/features/generic_events/aggregate/index.js +60 -53
  10. package/dist/cjs/features/generic_events/aggregate/user-actions/aggregated-user-action.js +2 -1
  11. package/dist/cjs/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +4 -3
  12. package/dist/cjs/features/session_replay/aggregate/index.js +15 -6
  13. package/dist/cjs/features/session_replay/constants.js +1 -1
  14. package/dist/cjs/features/session_replay/shared/recorder.js +3 -1
  15. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +7 -3
  16. package/dist/esm/common/constants/env.cdn.js +1 -1
  17. package/dist/esm/common/constants/env.npm.js +1 -1
  18. package/dist/esm/common/dom/selector-path.js +13 -3
  19. package/dist/esm/common/timing/time-keeper.js +18 -6
  20. package/dist/esm/common/vitals/load-time.js +5 -2
  21. package/dist/esm/features/ajax/aggregate/index.js +7 -3
  22. package/dist/esm/features/ajax/constants.js +3 -2
  23. package/dist/esm/features/generic_events/aggregate/index.js +61 -54
  24. package/dist/esm/features/generic_events/aggregate/user-actions/aggregated-user-action.js +2 -1
  25. package/dist/esm/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +4 -3
  26. package/dist/esm/features/session_replay/aggregate/index.js +15 -6
  27. package/dist/esm/features/session_replay/constants.js +1 -1
  28. package/dist/esm/features/session_replay/shared/recorder.js +3 -1
  29. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +5 -1
  30. package/dist/types/common/dom/selector-path.d.ts +2 -1
  31. package/dist/types/common/dom/selector-path.d.ts.map +1 -1
  32. package/dist/types/common/timing/time-keeper.d.ts.map +1 -1
  33. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  34. package/dist/types/features/ajax/constants.d.ts +1 -0
  35. package/dist/types/features/ajax/constants.d.ts.map +1 -1
  36. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  37. package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts +1 -0
  38. package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts.map +1 -1
  39. package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts +2 -0
  40. package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts.map +1 -1
  41. package/dist/types/features/session_replay/aggregate/index.d.ts +1 -11
  42. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  43. package/dist/types/features/session_replay/shared/recorder.d.ts +2 -0
  44. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  45. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +1 -0
  46. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  47. package/package.json +1 -1
  48. package/src/common/dom/selector-path.js +13 -4
  49. package/src/common/timing/time-keeper.js +17 -6
  50. package/src/common/vitals/load-time.js +5 -2
  51. package/src/features/ajax/aggregate/index.js +6 -3
  52. package/src/features/ajax/constants.js +3 -1
  53. package/src/features/generic_events/aggregate/index.js +42 -39
  54. package/src/features/generic_events/aggregate/user-actions/aggregated-user-action.js +2 -1
  55. package/src/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +4 -3
  56. package/src/features/session_replay/aggregate/index.js +16 -5
  57. package/src/features/session_replay/constants.js +1 -1
  58. package/src/features/session_replay/shared/recorder.js +3 -1
  59. package/src/features/soft_navigations/aggregate/ajax-node.js +4 -1
@@ -6,13 +6,14 @@ import { registerHandler } from '../../../common/event-emitter/register-handler'
6
6
  import { stringify } from '../../../common/util/stringify'
7
7
  import { handle } from '../../../common/event-emitter/handle'
8
8
  import { setDenyList, shouldCollectEvent } from '../../../common/deny-list/deny-list'
9
- import { FEATURE_NAME } from '../constants'
9
+ import { AJAX_ID, FEATURE_NAME } from '../constants'
10
10
  import { FEATURE_NAMES } from '../../../loaders/features/features'
11
11
  import { AggregateBase } from '../../utils/aggregate-base'
12
12
  import { parseGQL } from './gql'
13
13
  import { nullable, numeric, getAddStringContext, addCustomAttributes } from '../../../common/serialize/bel-serializer'
14
14
  import { gosNREUMOriginals } from '../../../common/window/nreum'
15
15
  import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/v2/utils'
16
+ import { generateUuid } from '../../../common/ids/unique-id'
16
17
 
17
18
  export class Aggregate extends AggregateBase {
18
19
  static featureName = FEATURE_NAME
@@ -91,7 +92,8 @@ export class Aggregate extends AggregateBase {
91
92
  type,
92
93
  startTime,
93
94
  endTime,
94
- callbackDuration: metrics.cbTime
95
+ callbackDuration: metrics.cbTime,
96
+ [AJAX_ID]: generateUuid() // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
95
97
  }
96
98
 
97
99
  if (ctx.dt) {
@@ -168,7 +170,8 @@ export class Aggregate extends AggregateBase {
168
170
  const attrParts = addCustomAttributes({
169
171
  ...(jsAttributes || {}),
170
172
  ...(event.gql || {}),
171
- ...(event.targetAttributes || {}) // used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
173
+ ...(event.targetAttributes || {}), // used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
174
+ [AJAX_ID]: event[AJAX_ID] // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
172
175
  }, addString)
173
176
 
174
177
  fields.unshift(numeric(attrParts.length))
@@ -1,7 +1,9 @@
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'
6
6
 
7
7
  export const FEATURE_NAME = FEATURE_NAMES.ajax
8
+
9
+ export const AJAX_ID = 'ajaxRequest.id'
@@ -14,7 +14,7 @@ import { applyFnToProps } from '../../../common/util/traverse'
14
14
  import { UserActionsAggregator } from './user-actions/user-actions-aggregator'
15
15
  import { isIFrameWindow } from '../../../common/dom/iframe'
16
16
  import { isPureObject } from '../../../common/util/type-check'
17
- import { getVersion2Attributes } from '../../../common/v2/utils'
17
+ import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/v2/utils'
18
18
 
19
19
  export class Aggregate extends AggregateBase {
20
20
  static featureName = FEATURE_NAME
@@ -61,7 +61,7 @@ export class Aggregate extends AggregateBase {
61
61
 
62
62
  let addUserAction = () => { /** no-op */ }
63
63
  if (isBrowserScope && agentRef.init.user_actions.enabled) {
64
- this.#userActionAggregator = new UserActionsAggregator()
64
+ this.#userActionAggregator = new UserActionsAggregator(this.agentRef)
65
65
  this.harvestOpts.beforeUnload = () => addUserAction?.(this.#userActionAggregator.aggregationEvent)
66
66
 
67
67
  addUserAction = (aggregatedUserAction) => {
@@ -70,50 +70,54 @@ export class Aggregate extends AggregateBase {
70
70
  * so we still need to validate that an event was given to this method before we try to add */
71
71
  if (aggregatedUserAction?.event) {
72
72
  const { target, timeStamp, type } = aggregatedUserAction.event
73
- const userActionEvent = {
74
- eventType: 'UserAction',
75
- timestamp: this.#toEpoch(timeStamp),
76
- action: type,
77
- actionCount: aggregatedUserAction.count,
78
- actionDuration: aggregatedUserAction.relativeMs[aggregatedUserAction.relativeMs.length - 1],
79
- actionMs: aggregatedUserAction.relativeMs,
80
- rageClick: aggregatedUserAction.rageClick,
81
- target: aggregatedUserAction.selectorPath,
82
- currentUrl: aggregatedUserAction.currentUrl,
83
- ...(isIFrameWindow(window) && { iframe: true }),
84
- ...(this.agentRef.init.user_actions.elementAttributes.reduce((acc, field) => {
73
+
74
+ aggregatedUserAction.targets.forEach(mfeTarget => {
75
+ const userActionEvent = {
76
+ eventType: 'UserAction',
77
+ timestamp: this.#toEpoch(timeStamp),
78
+ action: type,
79
+ actionCount: aggregatedUserAction.count,
80
+ actionDuration: aggregatedUserAction.relativeMs[aggregatedUserAction.relativeMs.length - 1],
81
+ actionMs: aggregatedUserAction.relativeMs,
82
+ rageClick: aggregatedUserAction.rageClick,
83
+ target: aggregatedUserAction.selectorPath,
84
+ currentUrl: aggregatedUserAction.currentUrl,
85
+ ...(isIFrameWindow(window) && { iframe: true }),
86
+ ...(this.agentRef.init.user_actions.elementAttributes.reduce((acc, field) => {
85
87
  /** prevent us from capturing an obscenely long value */
86
- if (canTrustTargetAttribute(field)) acc[targetAttrName(field)] = String(target[field]).trim().slice(0, 128)
87
- return acc
88
- }, {})),
89
- ...aggregatedUserAction.nearestTargetFields,
90
- ...(aggregatedUserAction.deadClick && { deadClick: true }),
91
- ...(aggregatedUserAction.errorClick && { errorClick: true })
92
- }
93
- this.addEvent(userActionEvent)
94
- this.#trackUserActionSM(userActionEvent)
95
-
96
- /**
88
+ if (canTrustTargetAttribute(field)) acc[targetAttrName(field)] = String(target[field]).trim().slice(0, 128)
89
+ return acc
90
+ }, {})),
91
+ ...aggregatedUserAction.nearestTargetFields,
92
+ ...(aggregatedUserAction.deadClick && { deadClick: true }),
93
+ ...(aggregatedUserAction.errorClick && { errorClick: true })
94
+ }
95
+ this.addEvent(userActionEvent, mfeTarget)
96
+
97
+ this.#trackUserActionSM(userActionEvent)
98
+
99
+ /**
97
100
  * Returns the original target field name with `target` prepended and camelCased
98
101
  * @param {string} originalFieldName
99
102
  * @returns {string} the target field name
100
103
  */
101
- function targetAttrName (originalFieldName) {
104
+ function targetAttrName (originalFieldName) {
102
105
  /** preserve original renaming structure for pre-existing field maps */
103
- if (originalFieldName === 'tagName') originalFieldName = 'tag'
104
- if (originalFieldName === 'className') originalFieldName = 'class'
105
- /** return the original field name, cap'd and prepended with target to match formatting */
106
- return `target${originalFieldName.charAt(0).toUpperCase() + originalFieldName.slice(1)}`
107
- }
106
+ if (originalFieldName === 'tagName') originalFieldName = 'tag'
107
+ if (originalFieldName === 'className') originalFieldName = 'class'
108
+ /** return the original field name, cap'd and prepended with target to match formatting */
109
+ return `target${originalFieldName.charAt(0).toUpperCase() + originalFieldName.slice(1)}`
110
+ }
108
111
 
109
- /**
112
+ /**
110
113
  * Only trust attributes that exist on HTML element targets, which excludes the window and the document targets
111
114
  * @param {string} attribute The attribute to check for on the target element
112
115
  * @returns {boolean} Whether the target element has the attribute and can be trusted
113
116
  */
114
- function canTrustTargetAttribute (attribute) {
115
- return !!(aggregatedUserAction.selectorPath !== 'window' && aggregatedUserAction.selectorPath !== 'document' && target instanceof HTMLElement && target?.[attribute])
116
- }
117
+ function canTrustTargetAttribute (attribute) {
118
+ return !!(aggregatedUserAction.selectorPath !== 'window' && aggregatedUserAction.selectorPath !== 'document' && target instanceof HTMLElement && target?.[attribute])
119
+ }
120
+ })
117
121
  }
118
122
  } catch (e) {
119
123
  // do nothing for now
@@ -314,9 +318,7 @@ export class Aggregate extends AggregateBase {
314
318
  timestamp: this.#toEpoch(now()),
315
319
  /** all generic events require pageUrl(s) */
316
320
  pageUrl: cleanURL('' + initialLocation),
317
- currentUrl: cleanURL('' + location),
318
- /** Specific attributes only supplied if harvesting to endpoint version 2 */
319
- ...(getVersion2Attributes(target, this))
321
+ currentUrl: cleanURL('' + location)
320
322
  }
321
323
 
322
324
  const eventAttributes = {
@@ -328,7 +330,8 @@ export class Aggregate extends AggregateBase {
328
330
  ...obj
329
331
  }
330
332
 
331
- this.events.add(eventAttributes)
333
+ this.events.add({ ...eventAttributes, ...getVersion2Attributes(target, this) })
334
+ if (shouldDuplicate(target, this)) this.addEvent({ ...eventAttributes, ...getVersion2DuplicationAttributes(target, this) })
332
335
  }
333
336
 
334
337
  serializer (eventBuffer) {
@@ -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 { RAGE_CLICK_THRESHOLD_EVENTS, RAGE_CLICK_THRESHOLD_MS } from '../../constants'
@@ -17,6 +17,7 @@ export class AggregatedUserAction {
17
17
  this.currentUrl = cleanURL('' + location)
18
18
  this.deadClick = false
19
19
  this.errorClick = false
20
+ this.targets = selectorInfo.targets
20
21
  }
21
22
 
22
23
  /**
@@ -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 { analyzeElemPath } from '../../../../common/dom/selector-path'
@@ -16,7 +16,8 @@ export class UserActionsAggregator {
16
16
  #domObserver = undefined
17
17
  #errorClickTimer = undefined
18
18
 
19
- constructor () {
19
+ constructor (agentRef) {
20
+ this.agentRef = agentRef
20
21
  if (gosNREUMOriginals().o.MO) {
21
22
  this.#domObserver = new MutationObserver(this.isLiveClick.bind(this))
22
23
  }
@@ -40,7 +41,7 @@ export class UserActionsAggregator {
40
41
  process (evt, targetFields) {
41
42
  if (!evt) return
42
43
  const targetElem = OBSERVED_WINDOW_EVENTS.includes(evt.type) ? window : evt.target
43
- const selectorInfo = analyzeElemPath(targetElem, targetFields)
44
+ const selectorInfo = analyzeElemPath(targetElem, targetFields, this.agentRef)
44
45
 
45
46
  // if selectorInfo.path is undefined, aggregation will be skipped for this event
46
47
  const aggregationKey = getAggregationKey(evt, selectorInfo.path)
@@ -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
  /**
@@ -209,6 +209,7 @@ export class Aggregate extends AggregateBase {
209
209
  }
210
210
 
211
211
  makeHarvestPayload () {
212
+ if (this.isRetrying) return this.recorder.retryPayload
212
213
  if (this.mode !== MODE.FULL || this.blocked) return // harvests should only be made in FULL mode, and not if the feature is blocked
213
214
  if (this.shouldCompress && !this.gzipper) return // if compression is enabled, but the libraries have not loaded, wait for them to load
214
215
  if (!this.recorder || !this.timeKeeper?.ready || !(this.recorder.hasSeenSnapshot && this.recorder.hasSeenMeta)) return // if the recorder or the timekeeper is not ready, or the recorder has not yet seen a snapshot, do not harvest
@@ -239,7 +240,6 @@ export class Aggregate extends AggregateBase {
239
240
  return
240
241
  }
241
242
 
242
- // TODO -- Gracefully handle the buffer for retries.
243
243
  if (!this.agentRef.runtime.session.state.sessionReplaySentFirstChunk) this.syncWithSessionManager({ sessionReplaySentFirstChunk: true })
244
244
  this.recorder.clearBuffer()
245
245
 
@@ -247,6 +247,8 @@ export class Aggregate extends AggregateBase {
247
247
  warn(59, JSON.stringify(this.agentRef.runtime.session.state))
248
248
  }
249
249
 
250
+ this.recorder.retryPayload = payload
251
+
250
252
  return payload
251
253
  }
252
254
 
@@ -338,9 +340,18 @@ export class Aggregate extends AggregateBase {
338
340
  }
339
341
 
340
342
  postHarvestCleanup (result) {
341
- // The mutual decision for now is to stop recording and clear buffers if ingest is experiencing 429 rate limiting
342
- if (result.status === 429) {
343
- this.abort(ABORT_REASONS.TOO_MANY)
343
+ if (result.sent) {
344
+ if (result.retry) {
345
+ warn(70)
346
+ this.isRetrying = true
347
+ this.forceStop()
348
+ } else {
349
+ this.recorder.retryPayload = undefined
350
+ if (this.isRetrying) {
351
+ this.isRetrying = false
352
+ this.switchToFull()
353
+ }
354
+ }
344
355
  }
345
356
  }
346
357
 
@@ -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 { MODE } from '../../common/session/constants'
@@ -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 { record as recorder } from '@newrelic/rrweb'
@@ -45,6 +45,8 @@ export class Recorder {
45
45
  this.events = new RecorderEvents(this.shouldFix)
46
46
  /** Backlog used for a 2-part sliding window to guarantee a 15-30s buffer window */
47
47
  this.backloggedEvents = new RecorderEvents(this.shouldFix)
48
+ /** Used to hold the harvest contents to facilitate retrying */
49
+ this.retryPayload = undefined
48
50
  /** Only set to true once a snapshot node has been processed. Used to block harvests from sending before we know we have a snapshot */
49
51
  this.hasSeenSnapshot = false
50
52
  this.hasSeenMeta = false
@@ -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 { AJAX_ID } from '../../ajax/constants'
6
7
  import { NODE_TYPE } from '../constants'
7
8
  import { BelNode } from './bel-node'
8
9
 
@@ -22,6 +23,7 @@ export class AjaxNode extends BelNode {
22
23
  this.spanTimestamp = ajaxEvent.spanTimestamp
23
24
  this.gql = ajaxEvent.gql
24
25
  this.targetAttributes = ajaxEvent.targetAttributes
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
25
27
 
26
28
  this.start = ajaxEvent.startTime
27
29
  this.end = ajaxEvent.endTime
@@ -55,7 +57,8 @@ export class AjaxNode extends BelNode {
55
57
  ]
56
58
  let allAttachedNodes = addCustomAttributes({
57
59
  ...(this.gql || {}),
58
- ...(this.targetAttributes || {})
60
+ ...(this.targetAttributes || {}),
61
+ [AJAX_ID]: this[AJAX_ID]
59
62
  }, addString)
60
63
  this.children.forEach(node => allAttachedNodes.push(node.serialize())) // no children is expected under ajax nodes at this time
61
64