@newrelic/browser-agent 1.298.0 → 1.299.0-rc.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 +14 -0
- package/README.md +12 -2
- 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 +60 -44
- package/dist/cjs/common/harvest/harvester.js +37 -7
- package/dist/cjs/common/url/extract-url.js +21 -0
- package/dist/cjs/features/ajax/instrument/index.js +2 -9
- package/dist/cjs/features/generic_events/aggregate/index.js +29 -8
- package/dist/cjs/features/generic_events/aggregate/user-actions/aggregated-user-action.js +5 -3
- package/dist/cjs/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +71 -33
- package/dist/cjs/features/generic_events/constants.js +2 -1
- package/dist/cjs/features/generic_events/instrument/index.js +45 -0
- package/dist/cjs/features/metrics/aggregate/framework-detection.js +2 -0
- package/dist/cjs/features/metrics/aggregate/harvest-metadata.js +45 -0
- package/dist/cjs/features/metrics/aggregate/index.js +21 -0
- package/dist/cjs/features/session_replay/aggregate/index.js +1 -1
- package/dist/cjs/features/session_replay/shared/recorder.js +6 -2
- package/dist/cjs/loaders/api/noticeError.js +1 -0
- package/dist/cjs/loaders/configure/configure.js +1 -0
- 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 +58 -42
- package/dist/esm/common/harvest/harvester.js +37 -7
- package/dist/esm/common/url/extract-url.js +15 -0
- package/dist/esm/features/ajax/instrument/index.js +2 -9
- package/dist/esm/features/generic_events/aggregate/index.js +29 -8
- package/dist/esm/features/generic_events/aggregate/user-actions/aggregated-user-action.js +5 -3
- package/dist/esm/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +72 -34
- package/dist/esm/features/generic_events/constants.js +1 -0
- package/dist/esm/features/generic_events/instrument/index.js +46 -1
- package/dist/esm/features/metrics/aggregate/framework-detection.js +2 -0
- package/dist/esm/features/metrics/aggregate/harvest-metadata.js +39 -0
- package/dist/esm/features/metrics/aggregate/index.js +21 -0
- package/dist/esm/features/session_replay/aggregate/index.js +1 -1
- package/dist/esm/features/session_replay/shared/recorder.js +6 -2
- package/dist/esm/loaders/api/noticeError.js +1 -0
- package/dist/esm/loaders/configure/configure.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/common/dom/selector-path.d.ts +6 -1
- package/dist/types/common/dom/selector-path.d.ts.map +1 -1
- package/dist/types/common/harvest/harvester.d.ts.map +1 -1
- package/dist/types/common/url/extract-url.d.ts +7 -0
- package/dist/types/common/url/extract-url.d.ts.map +1 -0
- package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
- package/dist/types/features/generic_events/aggregate/index.d.ts +1 -3
- 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 +3 -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 +3 -0
- package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts.map +1 -1
- package/dist/types/features/generic_events/constants.d.ts +1 -0
- package/dist/types/features/generic_events/constants.d.ts.map +1 -1
- package/dist/types/features/generic_events/instrument/index.d.ts +2 -0
- package/dist/types/features/generic_events/instrument/index.d.ts.map +1 -1
- package/dist/types/features/metrics/aggregate/framework-detection.d.ts.map +1 -1
- package/dist/types/features/metrics/aggregate/harvest-metadata.d.ts +6 -0
- package/dist/types/features/metrics/aggregate/harvest-metadata.d.ts.map +1 -0
- package/dist/types/features/metrics/aggregate/index.d.ts +2 -0
- package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/dist/types/loaders/api/noticeError.d.ts.map +1 -1
- package/dist/types/loaders/configure/configure.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/common/dom/selector-path.js +51 -39
- package/src/common/harvest/harvester.js +34 -7
- package/src/common/url/extract-url.js +17 -0
- package/src/features/ajax/instrument/index.js +2 -10
- package/src/features/generic_events/aggregate/index.js +23 -8
- package/src/features/generic_events/aggregate/user-actions/aggregated-user-action.js +5 -3
- package/src/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +80 -24
- package/src/features/generic_events/constants.js +2 -0
- package/src/features/generic_events/instrument/index.js +51 -1
- package/src/features/metrics/aggregate/framework-detection.js +2 -0
- package/src/features/metrics/aggregate/harvest-metadata.js +42 -0
- package/src/features/metrics/aggregate/index.js +21 -0
- package/src/features/session_replay/aggregate/index.js +1 -1
- package/src/features/session_replay/shared/recorder.js +5 -1
- package/src/loaders/api/noticeError.js +1 -0
- package/src/loaders/configure/configure.js +1 -0
|
@@ -29,6 +29,7 @@ const FRAMEWORKS = {
|
|
|
29
29
|
JQUERY: 'Jquery',
|
|
30
30
|
MOOTOOLS: 'MooTools',
|
|
31
31
|
QWIK: 'Qwik',
|
|
32
|
+
FLUTTER: 'Flutter',
|
|
32
33
|
ELECTRON: 'Electron'
|
|
33
34
|
};
|
|
34
35
|
function getFrameworks() {
|
|
@@ -64,6 +65,7 @@ function getFrameworks() {
|
|
|
64
65
|
if (Object.prototype.hasOwnProperty.call(window, 'jQuery')) frameworks.push(FRAMEWORKS.JQUERY);
|
|
65
66
|
if (Object.prototype.hasOwnProperty.call(window, 'MooTools')) frameworks.push(FRAMEWORKS.MOOTOOLS);
|
|
66
67
|
if (Object.prototype.hasOwnProperty.call(window, 'qwikevents')) frameworks.push(FRAMEWORKS.QWIK);
|
|
68
|
+
if (Object.hasOwn(window, '_flutter')) frameworks.push(FRAMEWORKS.FLUTTER);
|
|
67
69
|
if (detectElectron()) frameworks.push(FRAMEWORKS.ELECTRON);
|
|
68
70
|
} catch (err) {
|
|
69
71
|
// Possibly not supported
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.evaluateHarvestMetadata = evaluateHarvestMetadata;
|
|
7
|
+
/**
|
|
8
|
+
* Copyright 2020-2025 New Relic, Inc. All rights reserved.
|
|
9
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function evaluateHarvestMetadata(pageMetadata) {
|
|
13
|
+
try {
|
|
14
|
+
const supportabilityTags = [];
|
|
15
|
+
|
|
16
|
+
// Report SM like... audit/<feature_name>/<hasReplay|hasTrace|hasError>/<true|false>/<negative|positive>
|
|
17
|
+
const formTag = (...strings) => strings.join('/');
|
|
18
|
+
|
|
19
|
+
// Track if replay/trace/error harvests actually occurred (key only exists when harvested)
|
|
20
|
+
function evaluateTag(feature, flag, hasFlag, hasHarvest) {
|
|
21
|
+
const AUDIT = 'audit';
|
|
22
|
+
if (hasFlag) {
|
|
23
|
+
// False positive: flag true, but no harvest
|
|
24
|
+
if (!hasHarvest) supportabilityTags.push(formTag(AUDIT, feature, flag, 'false', 'positive'));
|
|
25
|
+
// True positive (correct)
|
|
26
|
+
else supportabilityTags.push(formTag(AUDIT, feature, flag, 'true', 'positive'));
|
|
27
|
+
} else {
|
|
28
|
+
// False negative: flag false, but harvest occurred
|
|
29
|
+
if (hasHarvest) supportabilityTags.push(formTag(AUDIT, feature, flag, 'false', 'negative'));
|
|
30
|
+
// True negative (correct)
|
|
31
|
+
else supportabilityTags.push(formTag(AUDIT, feature, flag, 'true', 'negative'));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (pageMetadata.page_view_event) {
|
|
35
|
+
evaluateTag('page_view', 'hasReplay', pageMetadata.page_view_event.hasReplay, !!pageMetadata.session_replay);
|
|
36
|
+
evaluateTag('page_view', 'hasTrace', pageMetadata.page_view_event.hasTrace, !!pageMetadata.session_trace);
|
|
37
|
+
}
|
|
38
|
+
if (pageMetadata.session_replay) {
|
|
39
|
+
evaluateTag('session_replay', 'hasError', pageMetadata.session_replay.hasError, !!pageMetadata.jserrors);
|
|
40
|
+
}
|
|
41
|
+
return supportabilityTags;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -13,6 +13,7 @@ var _eventListenerOpts = require("../../../common/event-listener/event-listener-
|
|
|
13
13
|
var _runtime = require("../../../common/constants/runtime");
|
|
14
14
|
var _aggregateBase = require("../../utils/aggregate-base");
|
|
15
15
|
var _iframe = require("../../../common/dom/iframe");
|
|
16
|
+
var _harvestMetadata = require("./harvest-metadata");
|
|
16
17
|
/**
|
|
17
18
|
* Copyright 2020-2025 New Relic, Inc. All rights reserved.
|
|
18
19
|
* SPDX-License-Identifier: Apache-2.0
|
|
@@ -26,6 +27,15 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
26
27
|
constructor(agentRef) {
|
|
27
28
|
super(agentRef, _constants.FEATURE_NAME);
|
|
28
29
|
this.harvestOpts.aggregatorTypes = ['cm', 'sm']; // the types in EventAggregator this feature cares about
|
|
30
|
+
|
|
31
|
+
/** all the harvest metadata metrics need to be evaluated simulataneously at unload time so just temporarily buffer them and dont make SMs immediately from the data */
|
|
32
|
+
this.harvestMetadata = {};
|
|
33
|
+
this.harvestOpts.beforeUnload = () => {
|
|
34
|
+
(0, _harvestMetadata.evaluateHarvestMetadata)(this.harvestMetadata).forEach(smTag => {
|
|
35
|
+
this.storeSupportabilityMetrics(smTag);
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
29
39
|
// This feature only harvests once per potential EoL of the page, which is handled by the central harvester.
|
|
30
40
|
|
|
31
41
|
// this must be read/stored synchronously, as the currentScript is removed from the DOM after this script is executed and this lookup will be void
|
|
@@ -145,6 +155,17 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
145
155
|
// handleWebsocketEvents(this.storeSupportabilityMetrics.bind(this), tag, ...args)
|
|
146
156
|
// }, this.featureName, this.ee)
|
|
147
157
|
// })
|
|
158
|
+
|
|
159
|
+
/** all the harvest metadata metrics need to be evaluated simulataneously at unload time so just temporarily buffer them and dont make SMs immediately from the data */
|
|
160
|
+
(0, _registerHandler.registerHandler)('harvest-metadata', (harvestMetadataObject = {}) => {
|
|
161
|
+
try {
|
|
162
|
+
Object.keys(harvestMetadataObject).forEach(key => {
|
|
163
|
+
Object.assign(this.harvestMetadata[key] ??= {}, harvestMetadataObject[key]);
|
|
164
|
+
});
|
|
165
|
+
} catch (e) {
|
|
166
|
+
// failed to merge harvest metadata... ignore
|
|
167
|
+
}
|
|
168
|
+
}, this.featureName, this.ee);
|
|
148
169
|
}
|
|
149
170
|
eachSessionChecks() {
|
|
150
171
|
if (!_runtime.isBrowserScope) return;
|
|
@@ -142,7 +142,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
142
142
|
sessionReplayMode: this.mode
|
|
143
143
|
});
|
|
144
144
|
} else {
|
|
145
|
-
this.initializeRecording(_constants2.MODE.FULL, true);
|
|
145
|
+
this.initializeRecording(_constants2.MODE.FULL, true, _constants.TRIGGERS.SWITCH_TO_FULL);
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -59,9 +59,13 @@ class Recorder {
|
|
|
59
59
|
this.#canRecord = false;
|
|
60
60
|
this.stopRecording();
|
|
61
61
|
}, this.srFeatureName, this.ee);
|
|
62
|
-
|
|
62
|
+
|
|
63
|
+
/** If Agg is already drained before importing the recorder (likely deferred API call pattern),
|
|
64
|
+
* registerHandler wont do anything. Just set up the on listener directly */
|
|
65
|
+
const processReplayNode = (event, isCheckout) => {
|
|
63
66
|
this.audit(event, isCheckout);
|
|
64
|
-
}
|
|
67
|
+
};
|
|
68
|
+
if (this.srInstrument.featAggregate?.drained) this.ee.on(RRWEB_DATA_CHANNEL, processReplayNode);else (0, _registerHandler.registerHandler)(RRWEB_DATA_CHANNEL, processReplayNode, this.srFeatureName, this.ee);
|
|
65
69
|
}
|
|
66
70
|
get trigger() {
|
|
67
71
|
return this.triggerHistory[this.triggerHistory.length - 1];
|
|
@@ -21,4 +21,5 @@ function setupNoticeErrorAPI(agent) {
|
|
|
21
21
|
function noticeError(err, customAttributes, agentRef, targetEntityGuid, timestamp = (0, _now.now)()) {
|
|
22
22
|
if (typeof err === 'string') err = new Error(err);
|
|
23
23
|
(0, _handle.handle)('err', [err, timestamp, false, customAttributes, agentRef.runtime.isRecording, undefined, targetEntityGuid], undefined, _features.FEATURE_NAMES.jserrors, agentRef.ee);
|
|
24
|
+
(0, _handle.handle)('uaErr', [], undefined, _features.FEATURE_NAMES.genericEvents, agentRef.ee);
|
|
24
25
|
}
|
|
@@ -59,6 +59,7 @@ function configure(agent, opts = {}, loaderType, forceDrain) {
|
|
|
59
59
|
internalTrafficList.push(updatedInit.proxy.assets);
|
|
60
60
|
}
|
|
61
61
|
if (updatedInit.proxy.beacon) internalTrafficList.push(updatedInit.proxy.beacon);
|
|
62
|
+
agent.beacons = [...internalTrafficList];
|
|
62
63
|
(0, _topLevelCallers.setTopLevelCallers)(agent); // no need to set global APIs on newrelic obj more than once
|
|
63
64
|
(0, _nreum.addToNREUM)('activatedFeatures', _featureFlags.activatedFeatures);
|
|
64
65
|
|
|
@@ -4,60 +4,76 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Generates a CSS selector path for the given element, if possible
|
|
7
|
+
* Generates a CSS selector path for the given element, if possible.
|
|
8
|
+
* Also gather metadata about the element's nearest fields, and whether there are any links or buttons in the path.
|
|
9
|
+
*
|
|
10
|
+
* Starts with simple cases like window or document and progresses to more complex dom-tree traversals as needed.
|
|
11
|
+
* Will return path: undefined if no other path can be determined.
|
|
12
|
+
*
|
|
8
13
|
* @param {HTMLElement} elem
|
|
9
|
-
* @param {
|
|
10
|
-
* @
|
|
11
|
-
* @returns {string|undefined}
|
|
14
|
+
* @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}}
|
|
12
16
|
*/
|
|
13
|
-
export const
|
|
14
|
-
|
|
17
|
+
export const analyzeElemPath = (elem, targetFields = []) => {
|
|
18
|
+
const result = {
|
|
15
19
|
path: undefined,
|
|
16
|
-
nearestFields: {}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
let i = 1;
|
|
21
|
-
const {
|
|
22
|
-
tagName
|
|
23
|
-
} = node;
|
|
24
|
-
while (node.previousElementSibling) {
|
|
25
|
-
if (node.previousElementSibling.tagName === tagName) i++;
|
|
26
|
-
node = node.previousElementSibling;
|
|
27
|
-
}
|
|
28
|
-
return i;
|
|
29
|
-
} catch (err) {
|
|
30
|
-
// do nothing for now. An invalid child count will make the path selector not return a nth-of-type selector statement
|
|
31
|
-
}
|
|
20
|
+
nearestFields: {},
|
|
21
|
+
hasButton: false,
|
|
22
|
+
hasLink: false
|
|
32
23
|
};
|
|
24
|
+
if (!elem) return result;
|
|
25
|
+
if (elem === window) {
|
|
26
|
+
result.path = 'window';
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
if (elem === document) {
|
|
30
|
+
result.path = 'document';
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
33
|
let pathSelector = '';
|
|
34
|
-
|
|
35
|
-
const nearestFields = {};
|
|
34
|
+
const index = getNthOfTypeIndex(elem);
|
|
36
35
|
try {
|
|
37
36
|
while (elem?.tagName) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} = elem;
|
|
37
|
+
const tagName = elem.tagName.toLowerCase();
|
|
38
|
+
result.hasLink ||= tagName === 'a';
|
|
39
|
+
result.hasButton ||= tagName === 'button' || tagName === 'input' && elem.type.toLowerCase() === 'button';
|
|
42
40
|
targetFields.forEach(field => {
|
|
43
|
-
nearestFields[nearestAttrName(field)] ||= elem[field]?.baseVal || elem[field];
|
|
41
|
+
result.nearestFields[nearestAttrName(field)] ||= elem[field]?.baseVal || elem[field];
|
|
44
42
|
});
|
|
45
|
-
|
|
46
|
-
pathSelector = selector;
|
|
43
|
+
pathSelector = buildPathSelector(elem, pathSelector);
|
|
47
44
|
elem = elem.parentNode;
|
|
48
45
|
}
|
|
49
46
|
} catch (err) {
|
|
50
47
|
// do nothing for now
|
|
51
48
|
}
|
|
52
|
-
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
result.path = pathSelector ? index ? "".concat(pathSelector, ":nth-of-type(").concat(index, ")") : pathSelector : undefined;
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
function buildPathSelector(elem, pathSelector) {
|
|
53
|
+
const {
|
|
54
|
+
id,
|
|
55
|
+
localName
|
|
56
|
+
} = elem;
|
|
57
|
+
return [localName, id ? "#".concat(id) : '', pathSelector ? ">".concat(pathSelector) : ''].join('');
|
|
58
|
+
}
|
|
59
|
+
function getNthOfTypeIndex(node) {
|
|
60
|
+
try {
|
|
61
|
+
let i = 1;
|
|
62
|
+
const {
|
|
63
|
+
tagName
|
|
64
|
+
} = node;
|
|
65
|
+
while (node.previousElementSibling) {
|
|
66
|
+
if (node.previousElementSibling.tagName === tagName) i++;
|
|
67
|
+
node = node.previousElementSibling;
|
|
68
|
+
}
|
|
69
|
+
return i;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
// do nothing for now. An invalid child count will make the path selector not return a nth-of-type selector statement
|
|
62
72
|
}
|
|
63
|
-
}
|
|
73
|
+
}
|
|
74
|
+
function nearestAttrName(originalFieldName) {
|
|
75
|
+
/** preserve original renaming structure for pre-existing field maps */
|
|
76
|
+
if (originalFieldName === 'tagName') originalFieldName = 'tag';
|
|
77
|
+
if (originalFieldName === 'className') originalFieldName = 'class';
|
|
78
|
+
return "nearest".concat(originalFieldName.charAt(0).toUpperCase() + originalFieldName.slice(1));
|
|
79
|
+
}
|
|
@@ -145,14 +145,16 @@ export function send(agentRef, {
|
|
|
145
145
|
}
|
|
146
146
|
const fullUrl = "".concat(url, "?").concat(baseParams).concat(payloadParams);
|
|
147
147
|
const gzip = !!qs?.attributes?.includes('gzip');
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
148
|
+
|
|
149
|
+
// all gzipped data is already in the correct format and needs no transformation
|
|
150
|
+
// all features going to 'events' endpoint should already be serialized & stringified
|
|
151
|
+
let stringBody = gzip || endpoint === EVENTS ? body : stringify(body);
|
|
153
152
|
|
|
154
153
|
// If body is null, undefined, or an empty object or array after stringifying, send an empty string instead.
|
|
155
|
-
if (!
|
|
154
|
+
if (!stringBody || stringBody.length === 0 || stringBody === '{}' || stringBody === '[]') stringBody = '';
|
|
155
|
+
|
|
156
|
+
// Warn--once per endpoint--if the agent tries to send large payloads
|
|
157
|
+
if (endpoint !== BLOBS && stringBody.length > 750000 && (warnings[endpoint] = (warnings[endpoint] || 0) + 1) === 1) warn(28, endpoint);
|
|
156
158
|
const headers = [{
|
|
157
159
|
key: 'content-type',
|
|
158
160
|
value: 'text/plain'
|
|
@@ -164,7 +166,7 @@ export function send(agentRef, {
|
|
|
164
166
|
Following the removal of img-element method. */
|
|
165
167
|
let result = submitMethod({
|
|
166
168
|
url: fullUrl,
|
|
167
|
-
body,
|
|
169
|
+
body: stringBody,
|
|
168
170
|
sync: localOpts.isFinalHarvest && isWorkerScope,
|
|
169
171
|
headers
|
|
170
172
|
});
|
|
@@ -184,6 +186,9 @@ export function send(agentRef, {
|
|
|
184
186
|
};
|
|
185
187
|
if (localOpts.needResponse) cbResult.responseText = this.responseText;
|
|
186
188
|
cbFinished(cbResult);
|
|
189
|
+
|
|
190
|
+
/** temporary audit of consistency of harvest metadata flags */
|
|
191
|
+
if (!shouldRetry(this.status)) trackHarvestMetadata();
|
|
187
192
|
}, eventListenerOpts(false));
|
|
188
193
|
} else if (submitMethod === fetchMethod) {
|
|
189
194
|
result.then(async function (response) {
|
|
@@ -198,8 +203,33 @@ export function send(agentRef, {
|
|
|
198
203
|
};
|
|
199
204
|
if (localOpts.needResponse) cbResult.responseText = await response.text();
|
|
200
205
|
cbFinished(cbResult);
|
|
206
|
+
/** temporary audit of consistency of harvest metadata flags */
|
|
207
|
+
if (!shouldRetry(status)) trackHarvestMetadata();
|
|
201
208
|
});
|
|
202
209
|
}
|
|
210
|
+
function trackHarvestMetadata() {
|
|
211
|
+
try {
|
|
212
|
+
if (featureName === FEATURE_NAMES.jserrors && !body?.err) return;
|
|
213
|
+
const hasReplay = baseParams.includes('hr=1');
|
|
214
|
+
const hasTrace = baseParams.includes('ht=1');
|
|
215
|
+
const hasError = qs?.attributes?.includes('hasError=true');
|
|
216
|
+
handle('harvest-metadata', [{
|
|
217
|
+
[featureName]: {
|
|
218
|
+
...(hasReplay && {
|
|
219
|
+
hasReplay
|
|
220
|
+
}),
|
|
221
|
+
...(hasTrace && {
|
|
222
|
+
hasTrace
|
|
223
|
+
}),
|
|
224
|
+
...(hasError && {
|
|
225
|
+
hasError
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
}], undefined, FEATURE_NAMES.metrics, agentRef.ee);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
// do nothing
|
|
231
|
+
}
|
|
232
|
+
}
|
|
203
233
|
}
|
|
204
234
|
dispatchGlobalEvent({
|
|
205
235
|
agentIdentifier: agentRef.agentIdentifier,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2020-2025 New Relic, Inc. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { globalScope } from '../constants/runtime';
|
|
6
|
+
import { gosNREUMOriginals } from '../window/nreum';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extracts a URL from various target types.
|
|
10
|
+
* @param {string|Request|URL} target - The target to extract the URL from. It can be a string, a Fetch Request object, or a URL object.
|
|
11
|
+
* @returns {string|undefined} The extracted URL as a string, or undefined if the target type is not supported.
|
|
12
|
+
*/
|
|
13
|
+
export function extractUrl(target) {
|
|
14
|
+
if (typeof target === 'string') return target;else if (target instanceof gosNREUMOriginals().o.REQ) return target.url;else if (globalScope?.URL && target instanceof URL) return target.href;
|
|
15
|
+
}
|
|
@@ -19,6 +19,7 @@ import { FEATURE_NAMES } from '../../../loaders/features/features';
|
|
|
19
19
|
import { SUPPORTABILITY_METRIC } from '../../metrics/constants';
|
|
20
20
|
import { now } from '../../../common/timing/now';
|
|
21
21
|
import { hasUndefinedHostname } from '../../../common/deny-list/deny-list';
|
|
22
|
+
import { extractUrl } from '../../../common/url/extract-url';
|
|
22
23
|
var handlers = ['load', 'error', 'abort', 'timeout'];
|
|
23
24
|
var handlersLen = handlers.length;
|
|
24
25
|
var origRequest = gosNREUMOriginals().o.REQ;
|
|
@@ -290,15 +291,7 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
|
|
|
290
291
|
if (fetchArguments.length >= 2) this.opts = fetchArguments[1];
|
|
291
292
|
var opts = this.opts || {};
|
|
292
293
|
var target = this.target;
|
|
293
|
-
|
|
294
|
-
if (typeof target === 'string') {
|
|
295
|
-
url = target;
|
|
296
|
-
} else if (typeof target === 'object' && target instanceof origRequest) {
|
|
297
|
-
url = target.url;
|
|
298
|
-
} else if (globalScope?.URL && typeof target === 'object' && target instanceof URL) {
|
|
299
|
-
url = target.href;
|
|
300
|
-
}
|
|
301
|
-
addUrl(this, url);
|
|
294
|
+
addUrl(this, extractUrl(target));
|
|
302
295
|
var method = ('' + (target && target instanceof origRequest && target.method || opts.method || 'GET')).toUpperCase();
|
|
303
296
|
this.params.method = method;
|
|
304
297
|
this.body = opts.body;
|
|
@@ -16,6 +16,7 @@ import { isIFrameWindow } from '../../../common/dom/iframe';
|
|
|
16
16
|
import { isPureObject } from '../../../common/util/type-check';
|
|
17
17
|
export class Aggregate extends AggregateBase {
|
|
18
18
|
static featureName = FEATURE_NAME;
|
|
19
|
+
#userActionAggregator;
|
|
19
20
|
constructor(agentRef) {
|
|
20
21
|
super(agentRef, FEATURE_NAME);
|
|
21
22
|
this.referrerUrl = isBrowserScope && document.referrer ? cleanURL(document.referrer) : undefined;
|
|
@@ -25,7 +26,7 @@ export class Aggregate extends AggregateBase {
|
|
|
25
26
|
this.deregisterDrain();
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
|
-
this
|
|
29
|
+
this.#trackSupportabilityMetrics();
|
|
29
30
|
registerHandler('api-recordCustomEvent', (timestamp, eventType, attributes) => {
|
|
30
31
|
if (RESERVED_EVENT_TYPES.includes(eventType)) return warn(46);
|
|
31
32
|
this.addEvent({
|
|
@@ -53,8 +54,8 @@ export class Aggregate extends AggregateBase {
|
|
|
53
54
|
}
|
|
54
55
|
let addUserAction = () => {/** no-op */};
|
|
55
56
|
if (isBrowserScope && agentRef.init.user_actions.enabled) {
|
|
56
|
-
this
|
|
57
|
-
this.harvestOpts.beforeUnload = () => addUserAction?.(this
|
|
57
|
+
this.#userActionAggregator = new UserActionsAggregator(agentRef.init.feature_flags.includes('user_frustrations'));
|
|
58
|
+
this.harvestOpts.beforeUnload = () => addUserAction?.(this.#userActionAggregator.aggregationEvent);
|
|
58
59
|
addUserAction = aggregatedUserAction => {
|
|
59
60
|
try {
|
|
60
61
|
/** The aggregator process only returns an event when it is "done" aggregating -
|
|
@@ -65,7 +66,7 @@ export class Aggregate extends AggregateBase {
|
|
|
65
66
|
timeStamp,
|
|
66
67
|
type
|
|
67
68
|
} = aggregatedUserAction.event;
|
|
68
|
-
|
|
69
|
+
const userActionEvent = {
|
|
69
70
|
eventType: 'UserAction',
|
|
70
71
|
timestamp: this.toEpoch(timeStamp),
|
|
71
72
|
action: type,
|
|
@@ -83,8 +84,16 @@ export class Aggregate extends AggregateBase {
|
|
|
83
84
|
if (canTrustTargetAttribute(field)) acc[targetAttrName(field)] = String(target[field]).trim().slice(0, 128);
|
|
84
85
|
return acc;
|
|
85
86
|
}, {}),
|
|
86
|
-
...aggregatedUserAction.nearestTargetFields
|
|
87
|
-
|
|
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);
|
|
88
97
|
|
|
89
98
|
/**
|
|
90
99
|
* Returns the original target field name with `target` prepended and camelCased
|
|
@@ -114,8 +123,15 @@ export class Aggregate extends AggregateBase {
|
|
|
114
123
|
};
|
|
115
124
|
registerHandler('ua', evt => {
|
|
116
125
|
/** the processor will return the previously aggregated event if it has been completed by processing the current event */
|
|
117
|
-
addUserAction(this
|
|
126
|
+
addUserAction(this.#userActionAggregator.process(evt, this.agentRef.init.user_actions.elementAttributes));
|
|
127
|
+
}, this.featureName, this.ee);
|
|
128
|
+
registerHandler('navChange', () => {
|
|
129
|
+
this.#userActionAggregator.isLiveClick();
|
|
118
130
|
}, this.featureName, this.ee);
|
|
131
|
+
registerHandler('uaXhr', () => {
|
|
132
|
+
this.#userActionAggregator.isLiveClick();
|
|
133
|
+
}, this.featureName, this.ee);
|
|
134
|
+
registerHandler('uaErr', () => this.#userActionAggregator.markAsErrorClick(), this.featureName, this.ee);
|
|
119
135
|
}
|
|
120
136
|
|
|
121
137
|
/**
|
|
@@ -291,7 +307,7 @@ export class Aggregate extends AggregateBase {
|
|
|
291
307
|
toEpoch(timestamp) {
|
|
292
308
|
return Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp));
|
|
293
309
|
}
|
|
294
|
-
trackSupportabilityMetrics() {
|
|
310
|
+
#trackSupportabilityMetrics() {
|
|
295
311
|
/** track usage SMs to improve these experimental features */
|
|
296
312
|
const configPerfTag = 'Config/Performance/';
|
|
297
313
|
if (this.agentRef.init.performance.capture_marks) this.reportSupportabilityMetric(configPerfTag + 'CaptureMarks/Enabled');
|
|
@@ -301,4 +317,9 @@ export class Aggregate extends AggregateBase {
|
|
|
301
317
|
if (this.agentRef.init.performance.resources.first_party_domains?.length !== 0) this.reportSupportabilityMetric(configPerfTag + 'Resources/FirstPartyDomains/Changed');
|
|
302
318
|
if (this.agentRef.init.performance.resources.ignore_newrelic === false) this.reportSupportabilityMetric(configPerfTag + 'Resources/IgnoreNewrelic/Changed');
|
|
303
319
|
}
|
|
320
|
+
#trackUserActionSM(ua) {
|
|
321
|
+
if (ua.rageClick) this.reportSupportabilityMetric('UserAction/RageClick/Seen');
|
|
322
|
+
if (ua.deadClick) this.reportSupportabilityMetric('UserAction/DeadClick/Seen');
|
|
323
|
+
if (ua.errorClick) this.reportSupportabilityMetric('UserAction/ErrorClick/Seen');
|
|
324
|
+
}
|
|
304
325
|
}
|
|
@@ -5,15 +5,17 @@
|
|
|
5
5
|
import { RAGE_CLICK_THRESHOLD_EVENTS, RAGE_CLICK_THRESHOLD_MS } from '../../constants';
|
|
6
6
|
import { cleanURL } from '../../../../common/url/clean-url';
|
|
7
7
|
export class AggregatedUserAction {
|
|
8
|
-
constructor(evt,
|
|
8
|
+
constructor(evt, selectorInfo) {
|
|
9
9
|
this.event = evt;
|
|
10
10
|
this.count = 1;
|
|
11
11
|
this.originMs = Math.floor(evt.timeStamp);
|
|
12
12
|
this.relativeMs = [0];
|
|
13
|
-
this.selectorPath =
|
|
13
|
+
this.selectorPath = selectorInfo.path;
|
|
14
14
|
this.rageClick = undefined;
|
|
15
|
-
this.nearestTargetFields =
|
|
15
|
+
this.nearestTargetFields = selectorInfo.nearestFields;
|
|
16
16
|
this.currentUrl = cleanURL('' + location);
|
|
17
|
+
this.deadClick = false;
|
|
18
|
+
this.errorClick = false;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -2,13 +2,25 @@
|
|
|
2
2
|
* Copyright 2020-2025 New Relic, Inc. All rights reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import { OBSERVED_WINDOW_EVENTS } from '../../constants';
|
|
5
|
+
import { analyzeElemPath } from '../../../../common/dom/selector-path';
|
|
6
|
+
import { FRUSTRATION_TIMEOUT_MS, OBSERVED_WINDOW_EVENTS } from '../../constants';
|
|
7
7
|
import { AggregatedUserAction } from './aggregated-user-action';
|
|
8
|
+
import { Timer } from '../../../../common/timer/timer';
|
|
9
|
+
import { gosNREUMOriginals } from '../../../../common/window/nreum';
|
|
8
10
|
export class UserActionsAggregator {
|
|
9
11
|
/** @type {AggregatedUserAction=} */
|
|
10
12
|
#aggregationEvent = undefined;
|
|
11
13
|
#aggregationKey = '';
|
|
14
|
+
#ufEnabled = false;
|
|
15
|
+
#deadClickTimer = undefined;
|
|
16
|
+
#domObserver = undefined;
|
|
17
|
+
#errorClickTimer = undefined;
|
|
18
|
+
constructor(userFrustrationsEnabled) {
|
|
19
|
+
if (userFrustrationsEnabled && gosNREUMOriginals().o.MO) {
|
|
20
|
+
this.#domObserver = new MutationObserver(this.isLiveClick.bind(this));
|
|
21
|
+
this.#ufEnabled = true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
12
24
|
get aggregationEvent() {
|
|
13
25
|
// if this is accessed externally, we need to be done aggregating on it
|
|
14
26
|
// to prevent potential mutability and duplication issues, so the state is cleared upon returning.
|
|
@@ -26,49 +38,75 @@ export class UserActionsAggregator {
|
|
|
26
38
|
*/
|
|
27
39
|
process(evt, targetFields) {
|
|
28
40
|
if (!evt) return;
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const aggregationKey = getAggregationKey(evt,
|
|
41
|
+
const targetElem = OBSERVED_WINDOW_EVENTS.includes(evt.type) ? window : evt.target;
|
|
42
|
+
const selectorInfo = analyzeElemPath(targetElem, targetFields);
|
|
43
|
+
|
|
44
|
+
// if selectorInfo.path is undefined, aggregation will be skipped for this event
|
|
45
|
+
const aggregationKey = getAggregationKey(evt, selectorInfo.path);
|
|
34
46
|
if (!!aggregationKey && aggregationKey === this.#aggregationKey) {
|
|
35
47
|
// an aggregation exists already, so lets just continue to increment
|
|
36
48
|
this.#aggregationEvent.aggregate(evt);
|
|
37
49
|
} else {
|
|
38
50
|
// return the prev existing one (if there is one)
|
|
39
51
|
const finishedEvent = this.#aggregationEvent;
|
|
40
|
-
|
|
52
|
+
if (this.#ufEnabled) {
|
|
53
|
+
this.#deadClickCleanup();
|
|
54
|
+
this.#errorClickCleanup();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// then start new event aggregation
|
|
41
58
|
this.#aggregationKey = aggregationKey;
|
|
42
|
-
this.#aggregationEvent = new AggregatedUserAction(evt,
|
|
59
|
+
this.#aggregationEvent = new AggregatedUserAction(evt, selectorInfo);
|
|
60
|
+
if (this.#ufEnabled && evt.type === 'click' && (selectorInfo.hasButton || selectorInfo.hasLink)) {
|
|
61
|
+
this.#deadClickSetup(this.#aggregationEvent);
|
|
62
|
+
this.#errorClickSetup();
|
|
63
|
+
}
|
|
43
64
|
return finishedEvent;
|
|
44
65
|
}
|
|
45
66
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
markAsErrorClick() {
|
|
68
|
+
if (this.#aggregationEvent && this.#errorClickTimer) {
|
|
69
|
+
this.#aggregationEvent.errorClick = true;
|
|
70
|
+
this.#errorClickCleanup();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
#errorClickSetup() {
|
|
74
|
+
this.#errorClickTimer = new Timer({
|
|
75
|
+
onEnd: () => {
|
|
76
|
+
this.#errorClickCleanup();
|
|
77
|
+
}
|
|
78
|
+
}, FRUSTRATION_TIMEOUT_MS);
|
|
79
|
+
}
|
|
80
|
+
#errorClickCleanup() {
|
|
81
|
+
this.#errorClickTimer?.clear();
|
|
82
|
+
this.#errorClickTimer = undefined;
|
|
83
|
+
}
|
|
84
|
+
#deadClickSetup(userAction) {
|
|
85
|
+
if (this.#isEvaluatingDeadClick() || !this.#domObserver) return;
|
|
86
|
+
this.#domObserver.observe(document, {
|
|
87
|
+
attributes: true,
|
|
88
|
+
characterData: true,
|
|
89
|
+
childList: true,
|
|
90
|
+
subtree: true
|
|
91
|
+
});
|
|
92
|
+
this.#deadClickTimer = new Timer({
|
|
93
|
+
onEnd: () => {
|
|
94
|
+
userAction.deadClick = true;
|
|
95
|
+
this.#deadClickCleanup();
|
|
96
|
+
}
|
|
97
|
+
}, FRUSTRATION_TIMEOUT_MS);
|
|
98
|
+
}
|
|
99
|
+
#deadClickCleanup() {
|
|
100
|
+
this.#domObserver?.disconnect();
|
|
101
|
+
this.#deadClickTimer?.clear();
|
|
102
|
+
this.#deadClickTimer = undefined;
|
|
103
|
+
}
|
|
104
|
+
#isEvaluatingDeadClick() {
|
|
105
|
+
return this.#deadClickTimer !== undefined;
|
|
106
|
+
}
|
|
107
|
+
isLiveClick() {
|
|
108
|
+
if (this.#isEvaluatingDeadClick()) this.#deadClickCleanup();
|
|
66
109
|
}
|
|
67
|
-
// if STILL no selectorPath, it will return undefined which will skip aggregation for this event
|
|
68
|
-
return {
|
|
69
|
-
selectorPath,
|
|
70
|
-
nearestTargetFields
|
|
71
|
-
};
|
|
72
110
|
}
|
|
73
111
|
|
|
74
112
|
/**
|