@newrelic/browser-agent 1.313.1-rc.2 → 1.313.1-rc.4

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 (28) hide show
  1. package/dist/cjs/common/constants/env.cdn.js +1 -1
  2. package/dist/cjs/common/constants/env.npm.js +1 -1
  3. package/dist/cjs/common/dom/selector-path.js +12 -3
  4. package/dist/cjs/common/timing/time-keeper.js +18 -6
  5. package/dist/cjs/features/generic_events/aggregate/index.js +60 -53
  6. package/dist/cjs/features/generic_events/aggregate/user-actions/aggregated-user-action.js +2 -1
  7. package/dist/cjs/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +4 -3
  8. package/dist/esm/common/constants/env.cdn.js +1 -1
  9. package/dist/esm/common/constants/env.npm.js +1 -1
  10. package/dist/esm/common/dom/selector-path.js +13 -3
  11. package/dist/esm/common/timing/time-keeper.js +18 -6
  12. package/dist/esm/features/generic_events/aggregate/index.js +61 -54
  13. package/dist/esm/features/generic_events/aggregate/user-actions/aggregated-user-action.js +2 -1
  14. package/dist/esm/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +4 -3
  15. package/dist/types/common/dom/selector-path.d.ts +2 -1
  16. package/dist/types/common/dom/selector-path.d.ts.map +1 -1
  17. package/dist/types/common/timing/time-keeper.d.ts.map +1 -1
  18. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  19. package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts +1 -0
  20. package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts.map +1 -1
  21. package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts +2 -0
  22. package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts.map +1 -1
  23. package/package.json +1 -1
  24. package/src/common/dom/selector-path.js +13 -4
  25. package/src/common/timing/time-keeper.js +17 -6
  26. package/src/features/generic_events/aggregate/index.js +42 -39
  27. package/src/features/generic_events/aggregate/user-actions/aggregated-user-action.js +2 -1
  28. package/src/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +4 -3
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.RRWEB_PACKAGE_NAME = exports.D
17
17
  /**
18
18
  * Exposes the version of the agent
19
19
  */
20
- const VERSION = exports.VERSION = "1.313.1-rc.2";
20
+ const VERSION = exports.VERSION = "1.313.1-rc.4";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.RRWEB_PACKAGE_NAME = exports.D
17
17
  /**
18
18
  * Exposes the version of the agent
19
19
  */
20
- const VERSION = exports.VERSION = "1.313.1-rc.2";
20
+ const VERSION = exports.VERSION = "1.313.1-rc.4";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -4,8 +4,9 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.analyzeElemPath = void 0;
7
+ var _utils = require("../v2/utils");
7
8
  /**
8
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
9
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
9
10
  * SPDX-License-Identifier: Apache-2.0
10
11
  */
11
12
 
@@ -18,12 +19,16 @@ exports.analyzeElemPath = void 0;
18
19
  *
19
20
  * @param {HTMLElement} elem
20
21
  * @param {Array<string>} [targetFields=[]] specifies which fields to gather from the nearest element in the path
21
- * @returns {{path: (undefined|string), nearestFields: {}, hasButton: boolean, hasLink: boolean}}
22
+ * @returns {{path: (undefined|string), nearestFields: {}, targets: Array, hasButton: boolean, hasLink: boolean}}
22
23
  */
23
- const analyzeElemPath = (elem, targetFields = []) => {
24
+ const analyzeElemPath = (elem, targetFields = [], agentRef) => {
25
+ const targets = [];
24
26
  const result = {
25
27
  path: undefined,
26
28
  nearestFields: {},
29
+ get targets() {
30
+ return targets.length ? targets : [undefined];
31
+ },
27
32
  hasButton: false,
28
33
  hasLink: false
29
34
  };
@@ -46,6 +51,10 @@ const analyzeElemPath = (elem, targetFields = []) => {
46
51
  targetFields.forEach(field => {
47
52
  result.nearestFields[nearestAttrName(field)] ||= elem[field]?.baseVal || elem[field];
48
53
  });
54
+ const dataAttrs = elem?.dataset;
55
+ if (dataAttrs.nrMfeId) {
56
+ targets.push(...(0, _utils.getRegisteredTargetsFromId)(dataAttrs.nrMfeId, agentRef));
57
+ }
49
58
  pathSelector = buildPathSelector(elem, pathSelector);
50
59
  elem = elem.parentNode;
51
60
  }
@@ -45,14 +45,19 @@ class TimeKeeper {
45
45
  * @type {boolean}
46
46
  */
47
47
  #ready = false;
48
- #reportedDrift = false;
48
+
49
+ /**
50
+ * The total measured drift in milliseconds. Represents how much performance.now()
51
+ * has fallen behind Date.now(), which is used to correct timestamp conversions.
52
+ * @type {number}
53
+ */
54
+ #measuredDrift = 0;
49
55
  constructor(sessionObj) {
50
56
  this.#session = sessionObj;
51
57
  this.processStoredDiff();
52
58
  (0, _monkeyPatched.isNative)(performance.now, Date.now); // will warn the user if these are not native functions. We need these to be native for time in the agent to be accurate in general.
53
59
  }
54
60
  #detectDrift() {
55
- if (this.#reportedDrift) return;
56
61
  try {
57
62
  // Drift detection: measures if performance.now() and Date.now() have become desynchronized
58
63
  // This can happen when a machine sleeps and the performance timer freezes while Date continues
@@ -63,8 +68,13 @@ class TimeKeeper {
63
68
  // Note: localTimeDiff (server time offset) is NOT part of drift - that's a legitimate offset
64
69
  const drift = Date.now() - _runtime.originTime - performance.now();
65
70
  if (drift > 1000) {
66
- this.#reportedDrift = true;
67
- (0, _handle.handle)(_constants.SUPPORTABILITY_METRIC_CHANNEL, ['Generic/TimeKeeper/ClockDrift/Detected', drift], undefined, _features.FEATURE_NAMES.metrics, this.#session.agentRef.ee);
71
+ // Check if this is new drift (increase of >1000ms from last measurement)
72
+ const newDrift = drift - this.#measuredDrift;
73
+ if (newDrift > 1000) {
74
+ // Update measured drift and report it
75
+ this.#measuredDrift = drift;
76
+ if (this.#session) (0, _handle.handle)(_constants.SUPPORTABILITY_METRIC_CHANNEL, ['Generic/TimeKeeper/ClockDrift/Detected', drift], undefined, _features.FEATURE_NAMES.metrics, this.#session.agentRef.ee);
77
+ }
68
78
  }
69
79
  } catch (err) {
70
80
  // Silently ignore drift detection errors to avoid breaking normal operation
@@ -115,7 +125,8 @@ class TimeKeeper {
115
125
  */
116
126
  convertRelativeTimestamp(relativeTime) {
117
127
  this.#detectDrift();
118
- return _runtime.originTime + relativeTime;
128
+ // Add measured drift to compensate for performance.now() falling behind
129
+ return _runtime.originTime + relativeTime + this.#measuredDrift;
119
130
  }
120
131
 
121
132
  /**
@@ -126,7 +137,8 @@ class TimeKeeper {
126
137
  */
127
138
  convertAbsoluteTimestamp(timestamp) {
128
139
  this.#detectDrift();
129
- return timestamp - _runtime.originTime;
140
+ // Subtract measured drift since we're converting from absolute to relative
141
+ return timestamp - _runtime.originTime - this.#measuredDrift;
130
142
  }
131
143
 
132
144
  /**
@@ -61,7 +61,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
61
61
  }
62
62
  let addUserAction = () => {/** no-op */};
63
63
  if (_runtime.isBrowserScope && agentRef.init.user_actions.enabled) {
64
- this.#userActionAggregator = new _userActionsAggregator.UserActionsAggregator();
64
+ this.#userActionAggregator = new _userActionsAggregator.UserActionsAggregator(this.agentRef);
65
65
  this.harvestOpts.beforeUnload = () => addUserAction?.(this.#userActionAggregator.aggregationEvent);
66
66
  addUserAction = aggregatedUserAction => {
67
67
  try {
@@ -73,56 +73,58 @@ class Aggregate extends _aggregateBase.AggregateBase {
73
73
  timeStamp,
74
74
  type
75
75
  } = aggregatedUserAction.event;
76
- const userActionEvent = {
77
- eventType: 'UserAction',
78
- timestamp: this.#toEpoch(timeStamp),
79
- action: type,
80
- actionCount: aggregatedUserAction.count,
81
- actionDuration: aggregatedUserAction.relativeMs[aggregatedUserAction.relativeMs.length - 1],
82
- actionMs: aggregatedUserAction.relativeMs,
83
- rageClick: aggregatedUserAction.rageClick,
84
- target: aggregatedUserAction.selectorPath,
85
- currentUrl: aggregatedUserAction.currentUrl,
86
- ...((0, _iframe.isIFrameWindow)(window) && {
87
- iframe: true
88
- }),
89
- ...this.agentRef.init.user_actions.elementAttributes.reduce((acc, field) => {
90
- /** prevent us from capturing an obscenely long value */
91
- if (canTrustTargetAttribute(field)) acc[targetAttrName(field)] = String(target[field]).trim().slice(0, 128);
92
- return acc;
93
- }, {}),
94
- ...aggregatedUserAction.nearestTargetFields,
95
- ...(aggregatedUserAction.deadClick && {
96
- deadClick: true
97
- }),
98
- ...(aggregatedUserAction.errorClick && {
99
- errorClick: true
100
- })
101
- };
102
- this.addEvent(userActionEvent);
103
- this.#trackUserActionSM(userActionEvent);
76
+ aggregatedUserAction.targets.forEach(mfeTarget => {
77
+ const userActionEvent = {
78
+ eventType: 'UserAction',
79
+ timestamp: this.#toEpoch(timeStamp),
80
+ action: type,
81
+ actionCount: aggregatedUserAction.count,
82
+ actionDuration: aggregatedUserAction.relativeMs[aggregatedUserAction.relativeMs.length - 1],
83
+ actionMs: aggregatedUserAction.relativeMs,
84
+ rageClick: aggregatedUserAction.rageClick,
85
+ target: aggregatedUserAction.selectorPath,
86
+ currentUrl: aggregatedUserAction.currentUrl,
87
+ ...((0, _iframe.isIFrameWindow)(window) && {
88
+ iframe: true
89
+ }),
90
+ ...this.agentRef.init.user_actions.elementAttributes.reduce((acc, field) => {
91
+ /** prevent us from capturing an obscenely long value */
92
+ if (canTrustTargetAttribute(field)) acc[targetAttrName(field)] = String(target[field]).trim().slice(0, 128);
93
+ return acc;
94
+ }, {}),
95
+ ...aggregatedUserAction.nearestTargetFields,
96
+ ...(aggregatedUserAction.deadClick && {
97
+ deadClick: true
98
+ }),
99
+ ...(aggregatedUserAction.errorClick && {
100
+ errorClick: true
101
+ })
102
+ };
103
+ this.addEvent(userActionEvent, mfeTarget);
104
+ this.#trackUserActionSM(userActionEvent);
104
105
 
105
- /**
106
- * Returns the original target field name with `target` prepended and camelCased
107
- * @param {string} originalFieldName
108
- * @returns {string} the target field name
109
- */
110
- function targetAttrName(originalFieldName) {
111
- /** preserve original renaming structure for pre-existing field maps */
112
- if (originalFieldName === 'tagName') originalFieldName = 'tag';
113
- if (originalFieldName === 'className') originalFieldName = 'class';
114
- /** return the original field name, cap'd and prepended with target to match formatting */
115
- return "target".concat(originalFieldName.charAt(0).toUpperCase() + originalFieldName.slice(1));
116
- }
106
+ /**
107
+ * Returns the original target field name with `target` prepended and camelCased
108
+ * @param {string} originalFieldName
109
+ * @returns {string} the target field name
110
+ */
111
+ function targetAttrName(originalFieldName) {
112
+ /** preserve original renaming structure for pre-existing field maps */
113
+ if (originalFieldName === 'tagName') originalFieldName = 'tag';
114
+ if (originalFieldName === 'className') originalFieldName = 'class';
115
+ /** return the original field name, cap'd and prepended with target to match formatting */
116
+ return "target".concat(originalFieldName.charAt(0).toUpperCase() + originalFieldName.slice(1));
117
+ }
117
118
 
118
- /**
119
- * Only trust attributes that exist on HTML element targets, which excludes the window and the document targets
120
- * @param {string} attribute The attribute to check for on the target element
121
- * @returns {boolean} Whether the target element has the attribute and can be trusted
122
- */
123
- function canTrustTargetAttribute(attribute) {
124
- return !!(aggregatedUserAction.selectorPath !== 'window' && aggregatedUserAction.selectorPath !== 'document' && target instanceof HTMLElement && target?.[attribute]);
125
- }
119
+ /**
120
+ * Only trust attributes that exist on HTML element targets, which excludes the window and the document targets
121
+ * @param {string} attribute The attribute to check for on the target element
122
+ * @returns {boolean} Whether the target element has the attribute and can be trusted
123
+ */
124
+ function canTrustTargetAttribute(attribute) {
125
+ return !!(aggregatedUserAction.selectorPath !== 'window' && aggregatedUserAction.selectorPath !== 'document' && target instanceof HTMLElement && target?.[attribute]);
126
+ }
127
+ });
126
128
  }
127
129
  } catch (e) {
128
130
  // do nothing for now
@@ -322,9 +324,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
322
324
  timestamp: this.#toEpoch((0, _now.now)()),
323
325
  /** all generic events require pageUrl(s) */
324
326
  pageUrl: (0, _cleanUrl.cleanURL)('' + _runtime.initialLocation),
325
- currentUrl: (0, _cleanUrl.cleanURL)('' + location),
326
- /** Specific attributes only supplied if harvesting to endpoint version 2 */
327
- ...(0, _utils.getVersion2Attributes)(target, this)
327
+ currentUrl: (0, _cleanUrl.cleanURL)('' + location)
328
328
  };
329
329
  const eventAttributes = {
330
330
  /** Agent-level custom attributes */
@@ -334,7 +334,14 @@ class Aggregate extends _aggregateBase.AggregateBase {
334
334
  /** Event-specific attributes take precedence over agent-level custom attributes and fallbacks */
335
335
  ...obj
336
336
  };
337
- this.events.add(eventAttributes);
337
+ this.events.add({
338
+ ...eventAttributes,
339
+ ...(0, _utils.getVersion2Attributes)(target, this)
340
+ });
341
+ if ((0, _utils.shouldDuplicate)(target, this.agentRef)) this.addEvent({
342
+ ...eventAttributes,
343
+ ...(0, _utils.getVersion2DuplicationAttributes)(target, this)
344
+ });
338
345
  }
339
346
  serializer(eventBuffer) {
340
347
  return (0, _traverse.applyFnToProps)({
@@ -7,7 +7,7 @@ exports.AggregatedUserAction = void 0;
7
7
  var _constants = require("../../constants");
8
8
  var _cleanUrl = require("../../../../common/url/clean-url");
9
9
  /**
10
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
10
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
11
11
  * SPDX-License-Identifier: Apache-2.0
12
12
  */
13
13
 
@@ -23,6 +23,7 @@ class AggregatedUserAction {
23
23
  this.currentUrl = (0, _cleanUrl.cleanURL)('' + location);
24
24
  this.deadClick = false;
25
25
  this.errorClick = false;
26
+ this.targets = selectorInfo.targets;
26
27
  }
27
28
 
28
29
  /**
@@ -10,7 +10,7 @@ var _aggregatedUserAction = require("./aggregated-user-action");
10
10
  var _timer = require("../../../../common/timer/timer");
11
11
  var _nreum = require("../../../../common/window/nreum");
12
12
  /**
13
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
13
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
14
14
  * SPDX-License-Identifier: Apache-2.0
15
15
  */
16
16
 
@@ -21,7 +21,8 @@ class UserActionsAggregator {
21
21
  #deadClickTimer = undefined;
22
22
  #domObserver = undefined;
23
23
  #errorClickTimer = undefined;
24
- constructor() {
24
+ constructor(agentRef) {
25
+ this.agentRef = agentRef;
25
26
  if ((0, _nreum.gosNREUMOriginals)().o.MO) {
26
27
  this.#domObserver = new MutationObserver(this.isLiveClick.bind(this));
27
28
  }
@@ -44,7 +45,7 @@ class UserActionsAggregator {
44
45
  process(evt, targetFields) {
45
46
  if (!evt) return;
46
47
  const targetElem = _constants.OBSERVED_WINDOW_EVENTS.includes(evt.type) ? window : evt.target;
47
- const selectorInfo = (0, _selectorPath.analyzeElemPath)(targetElem, targetFields);
48
+ const selectorInfo = (0, _selectorPath.analyzeElemPath)(targetElem, targetFields, this.agentRef);
48
49
 
49
50
  // if selectorInfo.path is undefined, aggregation will be skipped for this event
50
51
  const aggregationKey = getAggregationKey(evt, selectorInfo.path);
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.313.1-rc.2";
14
+ export const VERSION = "1.313.1-rc.4";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.313.1-rc.2";
14
+ export const VERSION = "1.313.1-rc.4";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -1,8 +1,10 @@
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
 
6
+ import { getRegisteredTargetsFromId } from '../v2/utils';
7
+
6
8
  /**
7
9
  * Generates a CSS selector path for the given element, if possible.
8
10
  * Also gather metadata about the element's nearest fields, and whether there are any links or buttons in the path.
@@ -12,12 +14,16 @@
12
14
  *
13
15
  * @param {HTMLElement} elem
14
16
  * @param {Array<string>} [targetFields=[]] specifies which fields to gather from the nearest element in the path
15
- * @returns {{path: (undefined|string), nearestFields: {}, hasButton: boolean, hasLink: boolean}}
17
+ * @returns {{path: (undefined|string), nearestFields: {}, targets: Array, hasButton: boolean, hasLink: boolean}}
16
18
  */
17
- export const analyzeElemPath = (elem, targetFields = []) => {
19
+ export const analyzeElemPath = (elem, targetFields = [], agentRef) => {
20
+ const targets = [];
18
21
  const result = {
19
22
  path: undefined,
20
23
  nearestFields: {},
24
+ get targets() {
25
+ return targets.length ? targets : [undefined];
26
+ },
21
27
  hasButton: false,
22
28
  hasLink: false
23
29
  };
@@ -40,6 +46,10 @@ export const analyzeElemPath = (elem, targetFields = []) => {
40
46
  targetFields.forEach(field => {
41
47
  result.nearestFields[nearestAttrName(field)] ||= elem[field]?.baseVal || elem[field];
42
48
  });
49
+ const dataAttrs = elem?.dataset;
50
+ if (dataAttrs.nrMfeId) {
51
+ targets.push(...getRegisteredTargetsFromId(dataAttrs.nrMfeId, agentRef));
52
+ }
43
53
  pathSelector = buildPathSelector(elem, pathSelector);
44
54
  elem = elem.parentNode;
45
55
  }
@@ -39,14 +39,19 @@ export class TimeKeeper {
39
39
  * @type {boolean}
40
40
  */
41
41
  #ready = false;
42
- #reportedDrift = false;
42
+
43
+ /**
44
+ * The total measured drift in milliseconds. Represents how much performance.now()
45
+ * has fallen behind Date.now(), which is used to correct timestamp conversions.
46
+ * @type {number}
47
+ */
48
+ #measuredDrift = 0;
43
49
  constructor(sessionObj) {
44
50
  this.#session = sessionObj;
45
51
  this.processStoredDiff();
46
52
  isNative(performance.now, Date.now); // will warn the user if these are not native functions. We need these to be native for time in the agent to be accurate in general.
47
53
  }
48
54
  #detectDrift() {
49
- if (this.#reportedDrift) return;
50
55
  try {
51
56
  // Drift detection: measures if performance.now() and Date.now() have become desynchronized
52
57
  // This can happen when a machine sleeps and the performance timer freezes while Date continues
@@ -57,8 +62,13 @@ export class TimeKeeper {
57
62
  // Note: localTimeDiff (server time offset) is NOT part of drift - that's a legitimate offset
58
63
  const drift = Date.now() - originTime - performance.now();
59
64
  if (drift > 1000) {
60
- this.#reportedDrift = true;
61
- handle(SUPPORTABILITY_METRIC_CHANNEL, ['Generic/TimeKeeper/ClockDrift/Detected', drift], undefined, FEATURE_NAMES.metrics, this.#session.agentRef.ee);
65
+ // Check if this is new drift (increase of >1000ms from last measurement)
66
+ const newDrift = drift - this.#measuredDrift;
67
+ if (newDrift > 1000) {
68
+ // Update measured drift and report it
69
+ this.#measuredDrift = drift;
70
+ if (this.#session) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Generic/TimeKeeper/ClockDrift/Detected', drift], undefined, FEATURE_NAMES.metrics, this.#session.agentRef.ee);
71
+ }
62
72
  }
63
73
  } catch (err) {
64
74
  // Silently ignore drift detection errors to avoid breaking normal operation
@@ -109,7 +119,8 @@ export class TimeKeeper {
109
119
  */
110
120
  convertRelativeTimestamp(relativeTime) {
111
121
  this.#detectDrift();
112
- return originTime + relativeTime;
122
+ // Add measured drift to compensate for performance.now() falling behind
123
+ return originTime + relativeTime + this.#measuredDrift;
113
124
  }
114
125
 
115
126
  /**
@@ -120,7 +131,8 @@ export class TimeKeeper {
120
131
  */
121
132
  convertAbsoluteTimestamp(timestamp) {
122
133
  this.#detectDrift();
123
- return timestamp - originTime;
134
+ // Subtract measured drift since we're converting from absolute to relative
135
+ return timestamp - originTime - this.#measuredDrift;
124
136
  }
125
137
 
126
138
  /**
@@ -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
  export class Aggregate extends AggregateBase {
19
19
  static featureName = FEATURE_NAME;
20
20
  #userActionAggregator;
@@ -54,7 +54,7 @@ export class Aggregate extends AggregateBase {
54
54
  }
55
55
  let addUserAction = () => {/** no-op */};
56
56
  if (isBrowserScope && agentRef.init.user_actions.enabled) {
57
- this.#userActionAggregator = new UserActionsAggregator();
57
+ this.#userActionAggregator = new UserActionsAggregator(this.agentRef);
58
58
  this.harvestOpts.beforeUnload = () => addUserAction?.(this.#userActionAggregator.aggregationEvent);
59
59
  addUserAction = aggregatedUserAction => {
60
60
  try {
@@ -66,56 +66,58 @@ export class Aggregate extends AggregateBase {
66
66
  timeStamp,
67
67
  type
68
68
  } = aggregatedUserAction.event;
69
- const userActionEvent = {
70
- eventType: 'UserAction',
71
- timestamp: this.#toEpoch(timeStamp),
72
- action: type,
73
- actionCount: aggregatedUserAction.count,
74
- actionDuration: aggregatedUserAction.relativeMs[aggregatedUserAction.relativeMs.length - 1],
75
- actionMs: aggregatedUserAction.relativeMs,
76
- rageClick: aggregatedUserAction.rageClick,
77
- target: aggregatedUserAction.selectorPath,
78
- currentUrl: aggregatedUserAction.currentUrl,
79
- ...(isIFrameWindow(window) && {
80
- iframe: true
81
- }),
82
- ...this.agentRef.init.user_actions.elementAttributes.reduce((acc, field) => {
83
- /** prevent us from capturing an obscenely long value */
84
- if (canTrustTargetAttribute(field)) acc[targetAttrName(field)] = String(target[field]).trim().slice(0, 128);
85
- return acc;
86
- }, {}),
87
- ...aggregatedUserAction.nearestTargetFields,
88
- ...(aggregatedUserAction.deadClick && {
89
- deadClick: true
90
- }),
91
- ...(aggregatedUserAction.errorClick && {
92
- errorClick: true
93
- })
94
- };
95
- this.addEvent(userActionEvent);
96
- this.#trackUserActionSM(userActionEvent);
69
+ aggregatedUserAction.targets.forEach(mfeTarget => {
70
+ const userActionEvent = {
71
+ eventType: 'UserAction',
72
+ timestamp: this.#toEpoch(timeStamp),
73
+ action: type,
74
+ actionCount: aggregatedUserAction.count,
75
+ actionDuration: aggregatedUserAction.relativeMs[aggregatedUserAction.relativeMs.length - 1],
76
+ actionMs: aggregatedUserAction.relativeMs,
77
+ rageClick: aggregatedUserAction.rageClick,
78
+ target: aggregatedUserAction.selectorPath,
79
+ currentUrl: aggregatedUserAction.currentUrl,
80
+ ...(isIFrameWindow(window) && {
81
+ iframe: true
82
+ }),
83
+ ...this.agentRef.init.user_actions.elementAttributes.reduce((acc, field) => {
84
+ /** prevent us from capturing an obscenely long value */
85
+ if (canTrustTargetAttribute(field)) acc[targetAttrName(field)] = String(target[field]).trim().slice(0, 128);
86
+ return acc;
87
+ }, {}),
88
+ ...aggregatedUserAction.nearestTargetFields,
89
+ ...(aggregatedUserAction.deadClick && {
90
+ deadClick: true
91
+ }),
92
+ ...(aggregatedUserAction.errorClick && {
93
+ errorClick: true
94
+ })
95
+ };
96
+ this.addEvent(userActionEvent, mfeTarget);
97
+ this.#trackUserActionSM(userActionEvent);
97
98
 
98
- /**
99
- * Returns the original target field name with `target` prepended and camelCased
100
- * @param {string} originalFieldName
101
- * @returns {string} the target field name
102
- */
103
- function targetAttrName(originalFieldName) {
104
- /** preserve original renaming structure for pre-existing field maps */
105
- if (originalFieldName === 'tagName') originalFieldName = 'tag';
106
- if (originalFieldName === 'className') originalFieldName = 'class';
107
- /** return the original field name, cap'd and prepended with target to match formatting */
108
- return "target".concat(originalFieldName.charAt(0).toUpperCase() + originalFieldName.slice(1));
109
- }
99
+ /**
100
+ * Returns the original target field name with `target` prepended and camelCased
101
+ * @param {string} originalFieldName
102
+ * @returns {string} the target field name
103
+ */
104
+ function targetAttrName(originalFieldName) {
105
+ /** preserve original renaming structure for pre-existing field maps */
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".concat(originalFieldName.charAt(0).toUpperCase() + originalFieldName.slice(1));
110
+ }
110
111
 
111
- /**
112
- * Only trust attributes that exist on HTML element targets, which excludes the window and the document targets
113
- * @param {string} attribute The attribute to check for on the target element
114
- * @returns {boolean} Whether the target element has the attribute and can be trusted
115
- */
116
- function canTrustTargetAttribute(attribute) {
117
- return !!(aggregatedUserAction.selectorPath !== 'window' && aggregatedUserAction.selectorPath !== 'document' && target instanceof HTMLElement && target?.[attribute]);
118
- }
112
+ /**
113
+ * Only trust attributes that exist on HTML element targets, which excludes the window and the document targets
114
+ * @param {string} attribute The attribute to check for on the target element
115
+ * @returns {boolean} Whether the target element has the attribute and can be trusted
116
+ */
117
+ function canTrustTargetAttribute(attribute) {
118
+ return !!(aggregatedUserAction.selectorPath !== 'window' && aggregatedUserAction.selectorPath !== 'document' && target instanceof HTMLElement && target?.[attribute]);
119
+ }
120
+ });
119
121
  }
120
122
  } catch (e) {
121
123
  // do nothing for now
@@ -315,9 +317,7 @@ export class Aggregate extends AggregateBase {
315
317
  timestamp: this.#toEpoch(now()),
316
318
  /** all generic events require pageUrl(s) */
317
319
  pageUrl: cleanURL('' + initialLocation),
318
- currentUrl: cleanURL('' + location),
319
- /** Specific attributes only supplied if harvesting to endpoint version 2 */
320
- ...getVersion2Attributes(target, this)
320
+ currentUrl: cleanURL('' + location)
321
321
  };
322
322
  const eventAttributes = {
323
323
  /** Agent-level custom attributes */
@@ -327,7 +327,14 @@ export class Aggregate extends AggregateBase {
327
327
  /** Event-specific attributes take precedence over agent-level custom attributes and fallbacks */
328
328
  ...obj
329
329
  };
330
- this.events.add(eventAttributes);
330
+ this.events.add({
331
+ ...eventAttributes,
332
+ ...getVersion2Attributes(target, this)
333
+ });
334
+ if (shouldDuplicate(target, this.agentRef)) this.addEvent({
335
+ ...eventAttributes,
336
+ ...getVersion2DuplicationAttributes(target, this)
337
+ });
331
338
  }
332
339
  serializer(eventBuffer) {
333
340
  return applyFnToProps({
@@ -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';
@@ -16,6 +16,7 @@ export class AggregatedUserAction {
16
16
  this.currentUrl = cleanURL('' + location);
17
17
  this.deadClick = false;
18
18
  this.errorClick = false;
19
+ this.targets = selectorInfo.targets;
19
20
  }
20
21
 
21
22
  /**
@@ -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';
@@ -14,7 +14,8 @@ export class UserActionsAggregator {
14
14
  #deadClickTimer = undefined;
15
15
  #domObserver = undefined;
16
16
  #errorClickTimer = undefined;
17
- constructor() {
17
+ constructor(agentRef) {
18
+ this.agentRef = agentRef;
18
19
  if (gosNREUMOriginals().o.MO) {
19
20
  this.#domObserver = new MutationObserver(this.isLiveClick.bind(this));
20
21
  }
@@ -37,7 +38,7 @@ export class UserActionsAggregator {
37
38
  process(evt, targetFields) {
38
39
  if (!evt) return;
39
40
  const targetElem = OBSERVED_WINDOW_EVENTS.includes(evt.type) ? window : evt.target;
40
- const selectorInfo = analyzeElemPath(targetElem, targetFields);
41
+ const selectorInfo = analyzeElemPath(targetElem, targetFields, this.agentRef);
41
42
 
42
43
  // if selectorInfo.path is undefined, aggregation will be skipped for this event
43
44
  const aggregationKey = getAggregationKey(evt, selectorInfo.path);
@@ -1,6 +1,7 @@
1
- export function analyzeElemPath(elem: HTMLElement, targetFields?: Array<string>): {
1
+ export function analyzeElemPath(elem: HTMLElement, targetFields?: Array<string>, agentRef: any): {
2
2
  path: (undefined | string);
3
3
  nearestFields: {};
4
+ targets: any[];
4
5
  hasButton: boolean;
5
6
  hasLink: boolean;
6
7
  };
@@ -1 +1 @@
1
- {"version":3,"file":"selector-path.d.ts","sourceRoot":"","sources":["../../../../src/common/dom/selector-path.js"],"names":[],"mappings":"AAgBO,sCAJI,WAAW,iBACX,KAAK,CAAC,MAAM,CAAC,GACX;IAAC,IAAI,EAAE,CAAC,SAAS,GAAC,MAAM,CAAC,CAAC;IAAC,aAAa,EAAE,EAAE,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAC,CA2B/F"}
1
+ {"version":3,"file":"selector-path.d.ts","sourceRoot":"","sources":["../../../../src/common/dom/selector-path.js"],"names":[],"mappings":"AAkBO,sCAJI,WAAW,iBACX,KAAK,CAAC,MAAM,CAAC,kBACX;IAAC,IAAI,EAAE,CAAC,SAAS,GAAC,MAAM,CAAC,CAAC;IAAC,aAAa,EAAE,EAAE,CAAC;IAAC,OAAO,QAAQ;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAC,CAkC/G"}
@@ -1 +1 @@
1
- {"version":3,"file":"time-keeper.d.ts","sourceRoot":"","sources":["../../../../src/common/timing/time-keeper.js"],"names":[],"mappings":"AAUA;;;;GAIG;AACH;IA6BE,6BAIC;IAsBD,qBAEC;IAED,kCAEC;IAED,4BAEC;IAED;;;;;;OAMG;IACH,8BALsB,cAAc,aACf,MAAM,WACR,MAAM,gBACD,MAAM,QAqB7B;IAED;;;;;OAKG;IACH,uCAHwB,MAAM,GACjB,MAAM,CAKlB;IAED;;;;;OAKG;IACH,0CAFa,MAAM,CAKlB;IAED;;;;OAIG;IACH,oCAHqB,MAAM,GACf,MAAM,CAKjB;IAED;;;;OAIG;IACH,uCAHW,mBAAmB,GACjB,MAAM,CAKlB;IAED,+FAA+F;IAC/F,0BASC;;CACF"}
1
+ {"version":3,"file":"time-keeper.d.ts","sourceRoot":"","sources":["../../../../src/common/timing/time-keeper.js"],"names":[],"mappings":"AAUA;;;;GAIG;AACH;IAkCE,6BAIC;IA0BD,qBAEC;IAED,kCAEC;IAED,4BAEC;IAED;;;;;;OAMG;IACH,8BALsB,cAAc,aACf,MAAM,WACR,MAAM,gBACD,MAAM,QAqB7B;IAED;;;;;OAKG;IACH,uCAHwB,MAAM,GACjB,MAAM,CAMlB;IAED;;;;;OAKG;IACH,0CAFa,MAAM,CAMlB;IAED;;;;OAIG;IACH,oCAHqB,MAAM,GACf,MAAM,CAKjB;IAED;;;;OAIG;IACH,uCAHW,mBAAmB,GACjB,MAAM,CAKlB;IAED,+FAA+F;IAC/F,0BASC;;CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/aggregate/index.js"],"names":[],"mappings":"AAkBA;IACE,2BAAiC;IAGjC,2BAqQC;IAnQC,gCAAkG;IAsQpG;;;;;;;;;;;;OAYG;IACH,eAJW,MAAM,YAAC,WACP,MAAM,YAAC,QAmCjB;IAED,qCAEC;IAED;;;MAEC;;CAsBF;8BAjW6B,4BAA4B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/aggregate/index.js"],"names":[],"mappings":"AAkBA;IACE,2BAAiC;IAGjC,2BAyQC;IAvQC,gCAAkG;IA0QpG;;;;;;;;;;;;OAYG;IACH,eAJW,MAAM,YAAC,WACP,MAAM,YAAC,QAkCjB;IAED,qCAEC;IAED;;;MAEC;;CAsBF;8BApW6B,4BAA4B"}
@@ -10,6 +10,7 @@ export class AggregatedUserAction {
10
10
  currentUrl: string;
11
11
  deadClick: boolean;
12
12
  errorClick: boolean;
13
+ targets: any;
13
14
  /**
14
15
  * Aggregates the count and maintains the relative MS array for matching events
15
16
  * Will determine if a rage click was observed as part of the aggregation
@@ -1 +1 @@
1
- {"version":3,"file":"aggregated-user-action.d.ts","sourceRoot":"","sources":["../../../../../../src/features/generic_events/aggregate/user-actions/aggregated-user-action.js"],"names":[],"mappings":"AAOA;IACE,yCAWC;IAVC,WAAgB;IAChB,cAAc;IACd,iBAAyC;IACzC,qBAAqB;IACrB,kBAAqC;IACrC,+BAA0B;IAC1B,yBAAqD;IACrD,mBAAyC;IACzC,mBAAsB;IACtB,oBAAuB;IAGzB;;;;;OAKG;IACH,eAHW,KAAK,GACH,IAAI,CAMhB;IAED;;;OAGG;IACH,eAFa,OAAO,CAKnB;CACF"}
1
+ {"version":3,"file":"aggregated-user-action.d.ts","sourceRoot":"","sources":["../../../../../../src/features/generic_events/aggregate/user-actions/aggregated-user-action.js"],"names":[],"mappings":"AAOA;IACE,yCAYC;IAXC,WAAgB;IAChB,cAAc;IACd,iBAAyC;IACzC,qBAAqB;IACrB,kBAAqC;IACrC,+BAA0B;IAC1B,yBAAqD;IACrD,mBAAyC;IACzC,mBAAsB;IACtB,oBAAuB;IACvB,aAAmC;IAGrC;;;;;OAKG;IACH,eAHW,KAAK,GACH,IAAI,CAMhB;IAED;;;OAGG;IACH,eAFa,OAAO,CAKnB;CACF"}
@@ -1,4 +1,6 @@
1
1
  export class UserActionsAggregator {
2
+ constructor(agentRef: any);
3
+ agentRef: any;
2
4
  get aggregationEvent(): AggregatedUserAction | undefined;
3
5
  /**
4
6
  * Process the event and determine if a new aggregation set should be made or if it should increment the current aggregation
@@ -1 +1 @@
1
- {"version":3,"file":"user-actions-aggregator.d.ts","sourceRoot":"","sources":["../../../../../../src/features/generic_events/aggregate/user-actions/user-actions-aggregator.js"],"names":[],"mappings":"AAUA;IAcE,yDAQC;IAED;;;;OAIG;IACH,aAHW,KAAK,sBACH,oBAAoB,GAAC,SAAS,CA2B1C;IAED,yBAKC;IA0CD,oBAEC;;CACF;qCA9GoC,0BAA0B"}
1
+ {"version":3,"file":"user-actions-aggregator.d.ts","sourceRoot":"","sources":["../../../../../../src/features/generic_events/aggregate/user-actions/user-actions-aggregator.js"],"names":[],"mappings":"AAUA;IAQE,2BAKC;IAJC,cAAwB;IAM1B,yDAQC;IAED;;;;OAIG;IACH,aAHW,KAAK,sBACH,oBAAoB,GAAC,SAAS,CA2B1C;IAED,yBAKC;IA0CD,oBAEC;;CACF;qCA/GoC,0BAA0B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/browser-agent",
3
- "version": "1.313.1-rc.2",
3
+ "version": "1.313.1-rc.4",
4
4
  "private": false,
5
5
  "author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
6
6
  "description": "New Relic Browser Agent",
@@ -1,8 +1,10 @@
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
 
6
+ import { getRegisteredTargetsFromId } from '../v2/utils'
7
+
6
8
  /**
7
9
  * Generates a CSS selector path for the given element, if possible.
8
10
  * Also gather metadata about the element's nearest fields, and whether there are any links or buttons in the path.
@@ -12,10 +14,11 @@
12
14
  *
13
15
  * @param {HTMLElement} elem
14
16
  * @param {Array<string>} [targetFields=[]] specifies which fields to gather from the nearest element in the path
15
- * @returns {{path: (undefined|string), nearestFields: {}, hasButton: boolean, hasLink: boolean}}
17
+ * @returns {{path: (undefined|string), nearestFields: {}, targets: Array, hasButton: boolean, hasLink: boolean}}
16
18
  */
17
- export const analyzeElemPath = (elem, targetFields = []) => {
18
- const result = { path: undefined, nearestFields: {}, hasButton: false, hasLink: false }
19
+ export const analyzeElemPath = (elem, targetFields = [], agentRef) => {
20
+ const targets = []
21
+ const result = { path: undefined, nearestFields: {}, get targets () { return targets.length ? targets : [undefined] }, hasButton: false, hasLink: false }
19
22
  if (!elem) return result
20
23
  if (elem === window) { result.path = 'window'; return result }
21
24
  if (elem === document) { result.path = 'document'; return result }
@@ -30,6 +33,12 @@ export const analyzeElemPath = (elem, targetFields = []) => {
30
33
  result.hasButton ||= tagName === 'button' || (tagName === 'input' && elem.type.toLowerCase() === 'button')
31
34
 
32
35
  targetFields.forEach(field => { result.nearestFields[nearestAttrName(field)] ||= (elem[field]?.baseVal || elem[field]) })
36
+
37
+ const dataAttrs = elem?.dataset
38
+ if (dataAttrs.nrMfeId) {
39
+ targets.push(...getRegisteredTargetsFromId(dataAttrs.nrMfeId, agentRef))
40
+ }
41
+
33
42
  pathSelector = buildPathSelector(elem, pathSelector)
34
43
  elem = elem.parentNode
35
44
  }
@@ -40,7 +40,12 @@ export class TimeKeeper {
40
40
  */
41
41
  #ready = false
42
42
 
43
- #reportedDrift = false
43
+ /**
44
+ * The total measured drift in milliseconds. Represents how much performance.now()
45
+ * has fallen behind Date.now(), which is used to correct timestamp conversions.
46
+ * @type {number}
47
+ */
48
+ #measuredDrift = 0
44
49
 
45
50
  constructor (sessionObj) {
46
51
  this.#session = sessionObj
@@ -49,7 +54,6 @@ export class TimeKeeper {
49
54
  }
50
55
 
51
56
  #detectDrift () {
52
- if (this.#reportedDrift) return
53
57
  try {
54
58
  // Drift detection: measures if performance.now() and Date.now() have become desynchronized
55
59
  // This can happen when a machine sleeps and the performance timer freezes while Date continues
@@ -60,8 +64,13 @@ export class TimeKeeper {
60
64
  // Note: localTimeDiff (server time offset) is NOT part of drift - that's a legitimate offset
61
65
  const drift = (Date.now() - originTime) - performance.now()
62
66
  if (drift > 1000) {
63
- this.#reportedDrift = true
64
- handle(SUPPORTABILITY_METRIC_CHANNEL, ['Generic/TimeKeeper/ClockDrift/Detected', drift], undefined, FEATURE_NAMES.metrics, this.#session.agentRef.ee)
67
+ // Check if this is new drift (increase of >1000ms from last measurement)
68
+ const newDrift = drift - this.#measuredDrift
69
+ if (newDrift > 1000) {
70
+ // Update measured drift and report it
71
+ this.#measuredDrift = drift
72
+ if (this.#session) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Generic/TimeKeeper/ClockDrift/Detected', drift], undefined, FEATURE_NAMES.metrics, this.#session.agentRef.ee)
73
+ }
65
74
  }
66
75
  } catch (err) {
67
76
  // Silently ignore drift detection errors to avoid breaking normal operation
@@ -116,7 +125,8 @@ export class TimeKeeper {
116
125
  */
117
126
  convertRelativeTimestamp (relativeTime) {
118
127
  this.#detectDrift()
119
- return originTime + relativeTime
128
+ // Add measured drift to compensate for performance.now() falling behind
129
+ return originTime + relativeTime + this.#measuredDrift
120
130
  }
121
131
 
122
132
  /**
@@ -127,7 +137,8 @@ export class TimeKeeper {
127
137
  */
128
138
  convertAbsoluteTimestamp (timestamp) {
129
139
  this.#detectDrift()
130
- return timestamp - originTime
140
+ // Subtract measured drift since we're converting from absolute to relative
141
+ return timestamp - originTime - this.#measuredDrift
131
142
  }
132
143
 
133
144
  /**
@@ -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.agentRef)) 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)