@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.
- package/CHANGELOG.md +7 -0
- package/dist/cjs/common/config/init.js +2 -1
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/dom/selector-path.js +20 -3
- package/dist/cjs/features/generic_events/aggregate/index.js +21 -14
- package/dist/cjs/features/generic_events/aggregate/user-actions/aggregated-user-action.js +2 -1
- package/dist/cjs/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +20 -6
- package/dist/esm/common/config/init.js +2 -1
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/dom/selector-path.js +20 -3
- package/dist/esm/features/generic_events/aggregate/index.js +21 -14
- package/dist/esm/features/generic_events/aggregate/user-actions/aggregated-user-action.js +2 -1
- package/dist/esm/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +20 -6
- package/dist/types/common/dom/selector-path.d.ts +1 -1
- package/dist/types/common/dom/selector-path.d.ts.map +1 -1
- package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts +2 -1
- package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts.map +1 -1
- package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts +1 -1
- package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/common/config/init.js +1 -1
- package/src/common/dom/selector-path.js +15 -3
- package/src/features/generic_events/aggregate/index.js +21 -6
- package/src/features/generic_events/aggregate/user-actions/aggregated-user-action.js +2 -1
- 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
|
|
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
...(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
|
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
|
|
76
|
+
return {
|
|
77
|
+
selectorPath,
|
|
78
|
+
nearestTargetFields
|
|
79
|
+
};
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
/**
|
|
@@ -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
|
-
|
|
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
|
-
...(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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"}
|
package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts
CHANGED
|
@@ -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
|
package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts
CHANGED
|
@@ -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';
|
package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts.map
CHANGED
|
@@ -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,
|
|
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.
|
|
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
|
-
|
|
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
|
-
...(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
/**
|