@newrelic/browser-agent 1.280.0 → 1.281.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 (28) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/cjs/common/config/init.js +2 -1
  3. package/dist/cjs/common/constants/env.cdn.js +1 -1
  4. package/dist/cjs/common/constants/env.npm.js +1 -1
  5. package/dist/cjs/common/dom/selector-path.js +20 -3
  6. package/dist/cjs/features/generic_events/aggregate/index.js +21 -14
  7. package/dist/cjs/features/generic_events/aggregate/user-actions/aggregated-user-action.js +2 -1
  8. package/dist/cjs/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +20 -6
  9. package/dist/esm/common/config/init.js +2 -1
  10. package/dist/esm/common/constants/env.cdn.js +1 -1
  11. package/dist/esm/common/constants/env.npm.js +1 -1
  12. package/dist/esm/common/dom/selector-path.js +20 -3
  13. package/dist/esm/features/generic_events/aggregate/index.js +21 -14
  14. package/dist/esm/features/generic_events/aggregate/user-actions/aggregated-user-action.js +2 -1
  15. package/dist/esm/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +20 -6
  16. package/dist/types/common/dom/selector-path.d.ts +1 -1
  17. package/dist/types/common/dom/selector-path.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 +2 -1
  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 +1 -1
  22. package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts.map +1 -1
  23. package/package.json +2 -2
  24. package/src/common/config/init.js +1 -1
  25. package/src/common/dom/selector-path.js +15 -3
  26. package/src/features/generic_events/aggregate/index.js +21 -6
  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 +11 -7
package/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [1.281.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.280.0...v1.281.0) (2025-02-04)
7
+
8
+
9
+ ### Features
10
+
11
+ * Capture Nearest UserAction Fields ([#1267](https://github.com/newrelic/newrelic-browser-agent/issues/1267)) ([d410937](https://github.com/newrelic/newrelic-browser-agent/commit/d410937983545a6a6aa39c52c3762f621acf1110))
12
+
6
13
  ## [1.280.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.279.1...v1.280.0) (2025-01-31)
7
14
 
8
15
 
@@ -211,7 +211,8 @@ const model = () => {
211
211
  },
212
212
  ssl: undefined,
213
213
  user_actions: {
214
- enabled: true
214
+ enabled: true,
215
+ elementAttributes: ['id', 'className', 'tagName', 'type']
215
216
  }
216
217
  };
217
218
  };
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.DIST_METHOD = exports.BUILD_EN
17
17
  /**
18
18
  * Exposes the version of the agent
19
19
  */
20
- const VERSION = exports.VERSION = "1.280.0";
20
+ const VERSION = exports.VERSION = "1.281.0";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.DIST_METHOD = exports.BUILD_EN
17
17
  /**
18
18
  * Exposes the version of the agent
19
19
  */
20
- const VERSION = exports.VERSION = "1.280.0";
20
+ const VERSION = exports.VERSION = "1.281.0";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -16,8 +16,11 @@ exports.generateSelectorPath = void 0;
16
16
  * @param {boolean} includeClass
17
17
  * @returns {string|undefined}
18
18
  */
19
- const generateSelectorPath = elem => {
20
- if (!elem) return;
19
+ const generateSelectorPath = (elem, targetFields = []) => {
20
+ if (!elem) return {
21
+ path: undefined,
22
+ nearestFields: {}
23
+ };
21
24
  const getNthOfTypeIndex = node => {
22
25
  try {
23
26
  let i = 1;
@@ -35,12 +38,16 @@ const generateSelectorPath = elem => {
35
38
  };
36
39
  let pathSelector = '';
37
40
  let index = getNthOfTypeIndex(elem);
41
+ const nearestFields = {};
38
42
  try {
39
43
  while (elem?.tagName) {
40
44
  const {
41
45
  id,
42
46
  localName
43
47
  } = elem;
48
+ targetFields.forEach(field => {
49
+ nearestFields[nearestAttrName(field)] ||= elem[field];
50
+ });
44
51
  const selector = [localName, id ? "#".concat(id) : '', pathSelector ? ">".concat(pathSelector) : ''].join('');
45
52
  pathSelector = selector;
46
53
  elem = elem.parentNode;
@@ -48,6 +55,16 @@ const generateSelectorPath = elem => {
48
55
  } catch (err) {
49
56
  // do nothing for now
50
57
  }
51
- return pathSelector ? index ? "".concat(pathSelector, ":nth-of-type(").concat(index, ")") : pathSelector : undefined;
58
+ const path = pathSelector ? index ? "".concat(pathSelector, ":nth-of-type(").concat(index, ")") : pathSelector : undefined;
59
+ return {
60
+ path,
61
+ nearestFields
62
+ };
63
+ function nearestAttrName(originalFieldName) {
64
+ /** preserve original renaming structure for pre-existing field maps */
65
+ if (originalFieldName === 'tagName') originalFieldName = 'tag';
66
+ if (originalFieldName === 'className') originalFieldName = 'class';
67
+ return "nearest".concat(originalFieldName.charAt(0).toUpperCase() + originalFieldName.slice(1));
68
+ }
52
69
  };
53
70
  exports.generateSelectorPath = generateSelectorPath;
@@ -61,7 +61,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
61
61
  });
62
62
  }, this.featureName, this.ee);
63
63
  }
64
- let addUserAction;
64
+ let addUserAction = () => {/** no-op */};
65
65
  if (_runtime.isBrowserScope && agentRef.init.user_actions.enabled) {
66
66
  this.userActionAggregator = new _userActionsAggregator.UserActionsAggregator();
67
67
  this.harvestOpts.beforeUnload = () => addUserAction?.(this.userActionAggregator.aggregationEvent);
@@ -87,20 +87,27 @@ class Aggregate extends _aggregateBase.AggregateBase {
87
87
  ...((0, _iframe.isIFrameWindow)(window) && {
88
88
  iframe: true
89
89
  }),
90
- ...(canTrustTargetAttribute('id') && {
91
- targetId: target.id
92
- }),
93
- ...(canTrustTargetAttribute('tagName') && {
94
- targetTag: target.tagName
95
- }),
96
- ...(canTrustTargetAttribute('type') && {
97
- targetType: target.type
98
- }),
99
- ...(canTrustTargetAttribute('className') && {
100
- targetClass: target.className
101
- })
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
102
96
  });
103
97
 
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
+ }
110
+
104
111
  /**
105
112
  * Only trust attributes that exist on HTML element targets, which excludes the window and the document targets
106
113
  * @param {string} attribute The attribute to check for on the target element
@@ -116,7 +123,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
116
123
  };
117
124
  (0, _registerHandler.registerHandler)('ua', evt => {
118
125
  /** the processor will return the previously aggregated event if it has been completed by processing the current event */
119
- addUserAction(this.userActionAggregator.process(evt));
126
+ addUserAction(this.userActionAggregator.process(evt, this.agentRef.init.user_actions.elementAttributes));
120
127
  }, this.featureName, this.ee);
121
128
  }
122
129
 
@@ -11,13 +11,14 @@ var _constants = require("../../constants");
11
11
  */
12
12
 
13
13
  class AggregatedUserAction {
14
- constructor(evt, selectorPath) {
14
+ constructor(evt, selectorPath, nearestTargetFields) {
15
15
  this.event = evt;
16
16
  this.count = 1;
17
17
  this.originMs = Math.floor(evt.timeStamp);
18
18
  this.relativeMs = [0];
19
19
  this.selectorPath = selectorPath;
20
20
  this.rageClick = undefined;
21
+ this.nearestTargetFields = nearestTargetFields;
21
22
  }
22
23
 
23
24
  /**
@@ -31,9 +31,12 @@ class UserActionsAggregator {
31
31
  * @param {Event} evt The event supplied by the addEventListener callback
32
32
  * @returns {AggregatedUserAction|undefined} The previous aggregation set if it has been completed by processing the current event
33
33
  */
34
- process(evt) {
34
+ process(evt, targetFields) {
35
35
  if (!evt) return;
36
- const selectorPath = getSelectorPath(evt);
36
+ const {
37
+ selectorPath,
38
+ nearestTargetFields
39
+ } = getSelectorPath(evt, targetFields);
37
40
  const aggregationKey = getAggregationKey(evt, selectorPath);
38
41
  if (!!aggregationKey && aggregationKey === this.#aggregationKey) {
39
42
  // an aggregation exists already, so lets just continue to increment
@@ -43,7 +46,7 @@ class UserActionsAggregator {
43
46
  const finishedEvent = this.#aggregationEvent;
44
47
  // then set as this new event aggregation
45
48
  this.#aggregationKey = aggregationKey;
46
- this.#aggregationEvent = new _aggregatedUserAction.AggregatedUserAction(evt, selectorPath);
49
+ this.#aggregationEvent = new _aggregatedUserAction.AggregatedUserAction(evt, selectorPath, nearestTargetFields);
47
50
  return finishedEvent;
48
51
  }
49
52
  }
@@ -56,13 +59,24 @@ class UserActionsAggregator {
56
59
  * @returns {string}
57
60
  */
58
61
  exports.UserActionsAggregator = UserActionsAggregator;
59
- function getSelectorPath(evt) {
62
+ function getSelectorPath(evt, targetFields) {
60
63
  let selectorPath;
64
+ let nearestTargetFields = {};
61
65
  if (_constants.OBSERVED_WINDOW_EVENTS.includes(evt.type) || evt.target === window) selectorPath = 'window';else if (evt.target === document) selectorPath = 'document';
62
66
  // if still no selectorPath, generate one from target tree that includes elem ids
63
- else selectorPath = (0, _selectorPath.generateSelectorPath)(evt.target);
67
+ else {
68
+ const {
69
+ path,
70
+ nearestFields
71
+ } = (0, _selectorPath.generateSelectorPath)(evt.target, targetFields);
72
+ selectorPath = path;
73
+ nearestTargetFields = nearestFields;
74
+ }
64
75
  // if STILL no selectorPath, it will return undefined which will skip aggregation for this event
65
- return selectorPath;
76
+ return {
77
+ selectorPath,
78
+ nearestTargetFields
79
+ };
66
80
  }
67
81
 
68
82
  /**
@@ -202,7 +202,8 @@ const model = () => {
202
202
  },
203
203
  ssl: undefined,
204
204
  user_actions: {
205
- enabled: true
205
+ enabled: true,
206
+ elementAttributes: ['id', 'className', 'tagName', 'type']
206
207
  }
207
208
  };
208
209
  };
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.280.0";
14
+ export const VERSION = "1.281.0";
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.280.0";
14
+ export const VERSION = "1.281.0";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -10,8 +10,11 @@
10
10
  * @param {boolean} includeClass
11
11
  * @returns {string|undefined}
12
12
  */
13
- export const generateSelectorPath = elem => {
14
- if (!elem) return;
13
+ export const generateSelectorPath = (elem, targetFields = []) => {
14
+ if (!elem) return {
15
+ path: undefined,
16
+ nearestFields: {}
17
+ };
15
18
  const getNthOfTypeIndex = node => {
16
19
  try {
17
20
  let i = 1;
@@ -29,12 +32,16 @@ export const generateSelectorPath = elem => {
29
32
  };
30
33
  let pathSelector = '';
31
34
  let index = getNthOfTypeIndex(elem);
35
+ const nearestFields = {};
32
36
  try {
33
37
  while (elem?.tagName) {
34
38
  const {
35
39
  id,
36
40
  localName
37
41
  } = elem;
42
+ targetFields.forEach(field => {
43
+ nearestFields[nearestAttrName(field)] ||= elem[field];
44
+ });
38
45
  const selector = [localName, id ? "#".concat(id) : '', pathSelector ? ">".concat(pathSelector) : ''].join('');
39
46
  pathSelector = selector;
40
47
  elem = elem.parentNode;
@@ -42,5 +49,15 @@ export const generateSelectorPath = elem => {
42
49
  } catch (err) {
43
50
  // do nothing for now
44
51
  }
45
- return pathSelector ? index ? "".concat(pathSelector, ":nth-of-type(").concat(index, ")") : pathSelector : undefined;
52
+ const path = pathSelector ? index ? "".concat(pathSelector, ":nth-of-type(").concat(index, ")") : pathSelector : undefined;
53
+ return {
54
+ path,
55
+ nearestFields
56
+ };
57
+ function nearestAttrName(originalFieldName) {
58
+ /** preserve original renaming structure for pre-existing field maps */
59
+ if (originalFieldName === 'tagName') originalFieldName = 'tag';
60
+ if (originalFieldName === 'className') originalFieldName = 'class';
61
+ return "nearest".concat(originalFieldName.charAt(0).toUpperCase() + originalFieldName.slice(1));
62
+ }
46
63
  };
@@ -54,7 +54,7 @@ export class Aggregate extends AggregateBase {
54
54
  });
55
55
  }, this.featureName, this.ee);
56
56
  }
57
- let addUserAction;
57
+ let addUserAction = () => {/** no-op */};
58
58
  if (isBrowserScope && agentRef.init.user_actions.enabled) {
59
59
  this.userActionAggregator = new UserActionsAggregator();
60
60
  this.harvestOpts.beforeUnload = () => addUserAction?.(this.userActionAggregator.aggregationEvent);
@@ -80,20 +80,27 @@ export class Aggregate extends AggregateBase {
80
80
  ...(isIFrameWindow(window) && {
81
81
  iframe: true
82
82
  }),
83
- ...(canTrustTargetAttribute('id') && {
84
- targetId: target.id
85
- }),
86
- ...(canTrustTargetAttribute('tagName') && {
87
- targetTag: target.tagName
88
- }),
89
- ...(canTrustTargetAttribute('type') && {
90
- targetType: target.type
91
- }),
92
- ...(canTrustTargetAttribute('className') && {
93
- targetClass: target.className
94
- })
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
95
89
  });
96
90
 
91
+ /**
92
+ * Returns the original target field name with `target` prepended and camelCased
93
+ * @param {string} originalFieldName
94
+ * @returns {string} the target field name
95
+ */
96
+ function targetAttrName(originalFieldName) {
97
+ /** preserve original renaming structure for pre-existing field maps */
98
+ if (originalFieldName === 'tagName') originalFieldName = 'tag';
99
+ if (originalFieldName === 'className') originalFieldName = 'class';
100
+ /** return the original field name, cap'd and prepended with target to match formatting */
101
+ return "target".concat(originalFieldName.charAt(0).toUpperCase() + originalFieldName.slice(1));
102
+ }
103
+
97
104
  /**
98
105
  * Only trust attributes that exist on HTML element targets, which excludes the window and the document targets
99
106
  * @param {string} attribute The attribute to check for on the target element
@@ -109,7 +116,7 @@ export class Aggregate extends AggregateBase {
109
116
  };
110
117
  registerHandler('ua', evt => {
111
118
  /** the processor will return the previously aggregated event if it has been completed by processing the current event */
112
- addUserAction(this.userActionAggregator.process(evt));
119
+ addUserAction(this.userActionAggregator.process(evt, this.agentRef.init.user_actions.elementAttributes));
113
120
  }, this.featureName, this.ee);
114
121
  }
115
122
 
@@ -4,13 +4,14 @@
4
4
  */
5
5
  import { RAGE_CLICK_THRESHOLD_EVENTS, RAGE_CLICK_THRESHOLD_MS } from '../../constants';
6
6
  export class AggregatedUserAction {
7
- constructor(evt, selectorPath) {
7
+ constructor(evt, selectorPath, nearestTargetFields) {
8
8
  this.event = evt;
9
9
  this.count = 1;
10
10
  this.originMs = Math.floor(evt.timeStamp);
11
11
  this.relativeMs = [0];
12
12
  this.selectorPath = selectorPath;
13
13
  this.rageClick = undefined;
14
+ this.nearestTargetFields = nearestTargetFields;
14
15
  }
15
16
 
16
17
  /**
@@ -24,9 +24,12 @@ export class UserActionsAggregator {
24
24
  * @param {Event} evt The event supplied by the addEventListener callback
25
25
  * @returns {AggregatedUserAction|undefined} The previous aggregation set if it has been completed by processing the current event
26
26
  */
27
- process(evt) {
27
+ process(evt, targetFields) {
28
28
  if (!evt) return;
29
- const selectorPath = getSelectorPath(evt);
29
+ const {
30
+ selectorPath,
31
+ nearestTargetFields
32
+ } = getSelectorPath(evt, targetFields);
30
33
  const aggregationKey = getAggregationKey(evt, selectorPath);
31
34
  if (!!aggregationKey && aggregationKey === this.#aggregationKey) {
32
35
  // an aggregation exists already, so lets just continue to increment
@@ -36,7 +39,7 @@ export class UserActionsAggregator {
36
39
  const finishedEvent = this.#aggregationEvent;
37
40
  // then set as this new event aggregation
38
41
  this.#aggregationKey = aggregationKey;
39
- this.#aggregationEvent = new AggregatedUserAction(evt, selectorPath);
42
+ this.#aggregationEvent = new AggregatedUserAction(evt, selectorPath, nearestTargetFields);
40
43
  return finishedEvent;
41
44
  }
42
45
  }
@@ -48,13 +51,24 @@ export class UserActionsAggregator {
48
51
  * @param {Event} evt
49
52
  * @returns {string}
50
53
  */
51
- function getSelectorPath(evt) {
54
+ function getSelectorPath(evt, targetFields) {
52
55
  let selectorPath;
56
+ let nearestTargetFields = {};
53
57
  if (OBSERVED_WINDOW_EVENTS.includes(evt.type) || evt.target === window) selectorPath = 'window';else if (evt.target === document) selectorPath = 'document';
54
58
  // if still no selectorPath, generate one from target tree that includes elem ids
55
- else selectorPath = generateSelectorPath(evt.target);
59
+ else {
60
+ const {
61
+ path,
62
+ nearestFields
63
+ } = generateSelectorPath(evt.target, targetFields);
64
+ selectorPath = path;
65
+ nearestTargetFields = nearestFields;
66
+ }
56
67
  // if STILL no selectorPath, it will return undefined which will skip aggregation for this event
57
- return selectorPath;
68
+ return {
69
+ selectorPath,
70
+ nearestTargetFields
71
+ };
58
72
  }
59
73
 
60
74
  /**
@@ -1,2 +1,2 @@
1
- export function generateSelectorPath(elem: HTMLElement): string | undefined;
1
+ export function generateSelectorPath(elem: HTMLElement, targetFields?: any[]): string | undefined;
2
2
  //# sourceMappingURL=selector-path.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"selector-path.d.ts","sourceRoot":"","sources":["../../../../src/common/dom/selector-path.js"],"names":[],"mappings":"AAYO,2CALI,WAAW,GAGT,MAAM,GAAC,SAAS,CAuC5B"}
1
+ {"version":3,"file":"selector-path.d.ts","sourceRoot":"","sources":["../../../../src/common/dom/selector-path.js"],"names":[],"mappings":"AAYO,2CALI,WAAW,yBAGT,MAAM,GAAC,SAAS,CAmD5B"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/aggregate/index.js"],"names":[],"mappings":"AAoBA;IACE,2BAAiC;IACjC,2BA4LC;IA1LC,yBAA4B;IAC5B,gCAAkG;IAuC9F,4CAAuD;IAqJ7D;;;;;;;;;;;OAWG;IACH,eAHW,MAAM,YAAC,QA0CjB;IAED,qCAEC;IAED;;;MAEC;IAED,gCAEC;IAED,mCASC;CACF;8BAxR6B,4BAA4B;sCAMpB,wCAAwC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/aggregate/index.js"],"names":[],"mappings":"AAoBA;IACE,2BAAiC;IACjC,2BA2MC;IAzMC,yBAA4B;IAC5B,gCAAkG;IAuC9F,4CAAuD;IAoK7D;;;;;;;;;;;OAWG;IACH,eAHW,MAAM,YAAC,QA0CjB;IAED,qCAEC;IAED;;;MAEC;IAED,gCAEC;IAED,mCASC;CACF;8BAvS6B,4BAA4B;sCAMpB,wCAAwC"}
@@ -1,11 +1,12 @@
1
1
  export class AggregatedUserAction {
2
- constructor(evt: any, selectorPath: any);
2
+ constructor(evt: any, selectorPath: any, nearestTargetFields: any);
3
3
  event: any;
4
4
  count: number;
5
5
  originMs: number;
6
6
  relativeMs: number[];
7
7
  selectorPath: any;
8
8
  rageClick: boolean | undefined;
9
+ nearestTargetFields: any;
9
10
  /**
10
11
  * Aggregates the count and maintains the relative MS array for matching events
11
12
  * 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":"AAMA;IACE,yCAOC;IANC,WAAgB;IAChB,cAAc;IACd,iBAAyC;IACzC,qBAAqB;IACrB,kBAAgC;IAChC,+BAA0B;IAG5B;;;;;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":"AAMA;IACE,mEAQC;IAPC,WAAgB;IAChB,cAAc;IACd,iBAAyC;IACzC,qBAAqB;IACrB,kBAAgC;IAChC,+BAA0B;IAC1B,yBAA8C;IAGhD;;;;;OAKG;IACH,eAHW,KAAK,GACH,IAAI,CAMhB;IAED;;;OAGG;IACH,eAFa,OAAO,CAKnB;CACF"}
@@ -5,7 +5,7 @@ export class UserActionsAggregator {
5
5
  * @param {Event} evt The event supplied by the addEventListener callback
6
6
  * @returns {AggregatedUserAction|undefined} The previous aggregation set if it has been completed by processing the current event
7
7
  */
8
- process(evt: Event): AggregatedUserAction | undefined;
8
+ process(evt: Event, targetFields: any): AggregatedUserAction | undefined;
9
9
  #private;
10
10
  }
11
11
  import { AggregatedUserAction } from './aggregated-user-action';
@@ -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":"AAQA;IAKE,yDAQC;IAED;;;;OAIG;IACH,aAHW,KAAK,GACH,oBAAoB,GAAC,SAAS,CAiB1C;;CACF;qCAtCoC,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":"AAQA;IAKE,yDAQC;IAED;;;;OAIG;IACH,aAHW,KAAK,sBACH,oBAAoB,GAAC,SAAS,CAiB1C;;CACF;qCAtCoC,0BAA0B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/browser-agent",
3
- "version": "1.280.0",
3
+ "version": "1.281.0",
4
4
  "private": false,
5
5
  "author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
6
6
  "description": "New Relic Browser Agent",
@@ -172,7 +172,7 @@
172
172
  "start": "npm-run-all --parallel cdn:watch test-server",
173
173
  "lint": "eslint -c .eslintrc.js --ext .js,.cjs,.mjs .",
174
174
  "lint:fix": "npm run lint -- --fix",
175
- "test": "jest",
175
+ "test": " NODE_OPTIONS=--max-old-space-size=8192 jest",
176
176
  "test:unit": "jest --selectProjects unit",
177
177
  "test:component": "jest --selectProjects component",
178
178
  "test:types": "tsd -f ./tests/dts/**/*.ts",
@@ -131,7 +131,7 @@ const model = () => {
131
131
  soft_navigations: { enabled: true, autoStart: true },
132
132
  spa: { enabled: true, autoStart: true },
133
133
  ssl: undefined,
134
- user_actions: { enabled: true }
134
+ user_actions: { enabled: true, elementAttributes: ['id', 'className', 'tagName', 'type'] }
135
135
  }
136
136
  }
137
137
 
@@ -10,8 +10,8 @@
10
10
  * @param {boolean} includeClass
11
11
  * @returns {string|undefined}
12
12
  */
13
- export const generateSelectorPath = (elem) => {
14
- if (!elem) return
13
+ export const generateSelectorPath = (elem, targetFields = []) => {
14
+ if (!elem) return { path: undefined, nearestFields: {} }
15
15
 
16
16
  const getNthOfTypeIndex = (node) => {
17
17
  try {
@@ -30,9 +30,13 @@ export const generateSelectorPath = (elem) => {
30
30
  let pathSelector = ''
31
31
  let index = getNthOfTypeIndex(elem)
32
32
 
33
+ const nearestFields = {}
33
34
  try {
34
35
  while (elem?.tagName) {
35
36
  const { id, localName } = elem
37
+ targetFields.forEach(field => {
38
+ nearestFields[nearestAttrName(field)] ||= elem[field]
39
+ })
36
40
  const selector = [
37
41
  localName,
38
42
  id ? `#${id}` : '',
@@ -46,5 +50,13 @@ export const generateSelectorPath = (elem) => {
46
50
  // do nothing for now
47
51
  }
48
52
 
49
- return pathSelector ? index ? `${pathSelector}:nth-of-type(${index})` : pathSelector : undefined
53
+ const path = pathSelector ? index ? `${pathSelector}:nth-of-type(${index})` : pathSelector : undefined
54
+ return { path, nearestFields }
55
+
56
+ function nearestAttrName (originalFieldName) {
57
+ /** preserve original renaming structure for pre-existing field maps */
58
+ if (originalFieldName === 'tagName') originalFieldName = 'tag'
59
+ if (originalFieldName === 'className') originalFieldName = 'class'
60
+ return `nearest${originalFieldName.charAt(0).toUpperCase() + originalFieldName.slice(1)}`
61
+ }
50
62
  }
@@ -60,7 +60,7 @@ export class Aggregate extends AggregateBase {
60
60
  }, this.featureName, this.ee)
61
61
  }
62
62
 
63
- let addUserAction
63
+ let addUserAction = () => { /** no-op */ }
64
64
  if (isBrowserScope && agentRef.init.user_actions.enabled) {
65
65
  this.userActionAggregator = new UserActionsAggregator()
66
66
  this.harvestOpts.beforeUnload = () => addUserAction?.(this.userActionAggregator.aggregationEvent)
@@ -81,12 +81,27 @@ export class Aggregate extends AggregateBase {
81
81
  rageClick: aggregatedUserAction.rageClick,
82
82
  target: aggregatedUserAction.selectorPath,
83
83
  ...(isIFrameWindow(window) && { iframe: true }),
84
- ...(canTrustTargetAttribute('id') && { targetId: target.id }),
85
- ...(canTrustTargetAttribute('tagName') && { targetTag: target.tagName }),
86
- ...(canTrustTargetAttribute('type') && { targetType: target.type }),
87
- ...(canTrustTargetAttribute('className') && { targetClass: target.className })
84
+ ...(this.agentRef.init.user_actions.elementAttributes.reduce((acc, field) => {
85
+ /** 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
88
90
  })
89
91
 
92
+ /**
93
+ * Returns the original target field name with `target` prepended and camelCased
94
+ * @param {string} originalFieldName
95
+ * @returns {string} the target field name
96
+ */
97
+ function targetAttrName (originalFieldName) {
98
+ /** preserve original renaming structure for pre-existing field maps */
99
+ if (originalFieldName === 'tagName') originalFieldName = 'tag'
100
+ if (originalFieldName === 'className') originalFieldName = 'class'
101
+ /** return the original field name, cap'd and prepended with target to match formatting */
102
+ return `target${originalFieldName.charAt(0).toUpperCase() + originalFieldName.slice(1)}`
103
+ }
104
+
90
105
  /**
91
106
  * Only trust attributes that exist on HTML element targets, which excludes the window and the document targets
92
107
  * @param {string} attribute The attribute to check for on the target element
@@ -103,7 +118,7 @@ export class Aggregate extends AggregateBase {
103
118
 
104
119
  registerHandler('ua', (evt) => {
105
120
  /** the processor will return the previously aggregated event if it has been completed by processing the current event */
106
- addUserAction(this.userActionAggregator.process(evt))
121
+ addUserAction(this.userActionAggregator.process(evt, this.agentRef.init.user_actions.elementAttributes))
107
122
  }, this.featureName, this.ee)
108
123
  }
109
124
 
@@ -5,13 +5,14 @@
5
5
  import { RAGE_CLICK_THRESHOLD_EVENTS, RAGE_CLICK_THRESHOLD_MS } from '../../constants'
6
6
 
7
7
  export class AggregatedUserAction {
8
- constructor (evt, selectorPath) {
8
+ constructor (evt, selectorPath, nearestTargetFields) {
9
9
  this.event = evt
10
10
  this.count = 1
11
11
  this.originMs = Math.floor(evt.timeStamp)
12
12
  this.relativeMs = [0]
13
13
  this.selectorPath = selectorPath
14
14
  this.rageClick = undefined
15
+ this.nearestTargetFields = nearestTargetFields
15
16
  }
16
17
 
17
18
  /**
@@ -26,9 +26,9 @@ export class UserActionsAggregator {
26
26
  * @param {Event} evt The event supplied by the addEventListener callback
27
27
  * @returns {AggregatedUserAction|undefined} The previous aggregation set if it has been completed by processing the current event
28
28
  */
29
- process (evt) {
29
+ process (evt, targetFields) {
30
30
  if (!evt) return
31
- const selectorPath = getSelectorPath(evt)
31
+ const { selectorPath, nearestTargetFields } = getSelectorPath(evt, targetFields)
32
32
  const aggregationKey = getAggregationKey(evt, selectorPath)
33
33
  if (!!aggregationKey && aggregationKey === this.#aggregationKey) {
34
34
  // an aggregation exists already, so lets just continue to increment
@@ -38,7 +38,7 @@ export class UserActionsAggregator {
38
38
  const finishedEvent = this.#aggregationEvent
39
39
  // then set as this new event aggregation
40
40
  this.#aggregationKey = aggregationKey
41
- this.#aggregationEvent = new AggregatedUserAction(evt, selectorPath)
41
+ this.#aggregationEvent = new AggregatedUserAction(evt, selectorPath, nearestTargetFields)
42
42
  return finishedEvent
43
43
  }
44
44
  }
@@ -50,14 +50,18 @@ export class UserActionsAggregator {
50
50
  * @param {Event} evt
51
51
  * @returns {string}
52
52
  */
53
- function getSelectorPath (evt) {
54
- let selectorPath
53
+ function getSelectorPath (evt, targetFields) {
54
+ let selectorPath; let nearestTargetFields = {}
55
55
  if (OBSERVED_WINDOW_EVENTS.includes(evt.type) || evt.target === window) selectorPath = 'window'
56
56
  else if (evt.target === document) selectorPath = 'document'
57
57
  // if still no selectorPath, generate one from target tree that includes elem ids
58
- else selectorPath = generateSelectorPath(evt.target)
58
+ else {
59
+ const { path, nearestFields } = generateSelectorPath(evt.target, targetFields)
60
+ selectorPath = path
61
+ nearestTargetFields = nearestFields
62
+ }
59
63
  // if STILL no selectorPath, it will return undefined which will skip aggregation for this event
60
- return selectorPath
64
+ return { selectorPath, nearestTargetFields }
61
65
  }
62
66
 
63
67
  /**