@newrelic/browser-agent 1.298.0 → 1.299.0-rc.1

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 (80) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +12 -2
  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 +60 -44
  6. package/dist/cjs/common/harvest/harvester.js +37 -7
  7. package/dist/cjs/common/url/extract-url.js +21 -0
  8. package/dist/cjs/features/ajax/instrument/index.js +2 -9
  9. package/dist/cjs/features/generic_events/aggregate/index.js +29 -8
  10. package/dist/cjs/features/generic_events/aggregate/user-actions/aggregated-user-action.js +5 -3
  11. package/dist/cjs/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +71 -33
  12. package/dist/cjs/features/generic_events/constants.js +2 -1
  13. package/dist/cjs/features/generic_events/instrument/index.js +45 -0
  14. package/dist/cjs/features/metrics/aggregate/framework-detection.js +2 -0
  15. package/dist/cjs/features/metrics/aggregate/harvest-metadata.js +45 -0
  16. package/dist/cjs/features/metrics/aggregate/index.js +21 -0
  17. package/dist/cjs/features/session_replay/aggregate/index.js +1 -1
  18. package/dist/cjs/features/session_replay/shared/recorder.js +6 -2
  19. package/dist/cjs/loaders/api/noticeError.js +1 -0
  20. package/dist/cjs/loaders/configure/configure.js +1 -0
  21. package/dist/esm/common/constants/env.cdn.js +1 -1
  22. package/dist/esm/common/constants/env.npm.js +1 -1
  23. package/dist/esm/common/dom/selector-path.js +58 -42
  24. package/dist/esm/common/harvest/harvester.js +37 -7
  25. package/dist/esm/common/url/extract-url.js +15 -0
  26. package/dist/esm/features/ajax/instrument/index.js +2 -9
  27. package/dist/esm/features/generic_events/aggregate/index.js +29 -8
  28. package/dist/esm/features/generic_events/aggregate/user-actions/aggregated-user-action.js +5 -3
  29. package/dist/esm/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +72 -34
  30. package/dist/esm/features/generic_events/constants.js +1 -0
  31. package/dist/esm/features/generic_events/instrument/index.js +46 -1
  32. package/dist/esm/features/metrics/aggregate/framework-detection.js +2 -0
  33. package/dist/esm/features/metrics/aggregate/harvest-metadata.js +39 -0
  34. package/dist/esm/features/metrics/aggregate/index.js +21 -0
  35. package/dist/esm/features/session_replay/aggregate/index.js +1 -1
  36. package/dist/esm/features/session_replay/shared/recorder.js +6 -2
  37. package/dist/esm/loaders/api/noticeError.js +1 -0
  38. package/dist/esm/loaders/configure/configure.js +1 -0
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/dist/types/common/dom/selector-path.d.ts +6 -1
  41. package/dist/types/common/dom/selector-path.d.ts.map +1 -1
  42. package/dist/types/common/harvest/harvester.d.ts.map +1 -1
  43. package/dist/types/common/url/extract-url.d.ts +7 -0
  44. package/dist/types/common/url/extract-url.d.ts.map +1 -0
  45. package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
  46. package/dist/types/features/generic_events/aggregate/index.d.ts +1 -3
  47. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  48. package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts +3 -1
  49. package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts.map +1 -1
  50. package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts +3 -0
  51. package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts.map +1 -1
  52. package/dist/types/features/generic_events/constants.d.ts +1 -0
  53. package/dist/types/features/generic_events/constants.d.ts.map +1 -1
  54. package/dist/types/features/generic_events/instrument/index.d.ts +2 -0
  55. package/dist/types/features/generic_events/instrument/index.d.ts.map +1 -1
  56. package/dist/types/features/metrics/aggregate/framework-detection.d.ts.map +1 -1
  57. package/dist/types/features/metrics/aggregate/harvest-metadata.d.ts +6 -0
  58. package/dist/types/features/metrics/aggregate/harvest-metadata.d.ts.map +1 -0
  59. package/dist/types/features/metrics/aggregate/index.d.ts +2 -0
  60. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  61. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  62. package/dist/types/loaders/api/noticeError.d.ts.map +1 -1
  63. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  64. package/package.json +2 -2
  65. package/src/common/dom/selector-path.js +51 -39
  66. package/src/common/harvest/harvester.js +34 -7
  67. package/src/common/url/extract-url.js +17 -0
  68. package/src/features/ajax/instrument/index.js +2 -10
  69. package/src/features/generic_events/aggregate/index.js +23 -8
  70. package/src/features/generic_events/aggregate/user-actions/aggregated-user-action.js +5 -3
  71. package/src/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +80 -24
  72. package/src/features/generic_events/constants.js +2 -0
  73. package/src/features/generic_events/instrument/index.js +51 -1
  74. package/src/features/metrics/aggregate/framework-detection.js +2 -0
  75. package/src/features/metrics/aggregate/harvest-metadata.js +42 -0
  76. package/src/features/metrics/aggregate/index.js +21 -0
  77. package/src/features/session_replay/aggregate/index.js +1 -1
  78. package/src/features/session_replay/shared/recorder.js +5 -1
  79. package/src/loaders/api/noticeError.js +1 -0
  80. package/src/loaders/configure/configure.js +1 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
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.299.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.298.0...v1.299.0) (2025-10-07)
7
+
8
+
9
+ ### Features
10
+
11
+ * Add Flutter supportability metric ([#1580](https://github.com/newrelic/newrelic-browser-agent/issues/1580)) ([5083067](https://github.com/newrelic/newrelic-browser-agent/commit/5083067e1dab2fed972af8010d95c07ba11ec436))
12
+ * Add user frustration signals to UserAction ([#1534](https://github.com/newrelic/newrelic-browser-agent/issues/1534)) ([4d654c3](https://github.com/newrelic/newrelic-browser-agent/commit/4d654c3e4cef906885545b36142fc03da34d525c))
13
+ * Evaluate the accuracy of cross-feature attribution ([#1573](https://github.com/newrelic/newrelic-browser-agent/issues/1573)) ([b1e03e2](https://github.com/newrelic/newrelic-browser-agent/commit/b1e03e216ec4eedb77cbb49241d51b5526b97997))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * Fix recordReplay API inconsistencies ([#1582](https://github.com/newrelic/newrelic-browser-agent/issues/1582)) ([e4465c1](https://github.com/newrelic/newrelic-browser-agent/commit/e4465c1ebfd6f50d3fce99074ae190ce3bc2f4e4))
19
+
6
20
  ## [1.298.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.297.1...v1.298.0) (2025-09-19)
7
21
 
8
22
 
package/README.md CHANGED
@@ -389,9 +389,19 @@ A lot of new frameworks support the concept of server-side rendering the pages o
389
389
 
390
390
  ## Support
391
391
 
392
- New Relic hosts and moderates an online forum where customers can interact with New Relic employees as well as other customers to get help and share best practices. Like all official New Relic open source projects, there's a related Community topic in the New Relic Explorers Hub. You can find this project's topic/threads here:
392
+ Should you need assistance with New Relic products, you are in good hands with several support channels.
393
393
 
394
- https://discuss.newrelic.com/c/full-stack-observability/browser
394
+ If the issue has been confirmed as a bug or is a feature request, please file a GitHub issue.
395
+
396
+ ### Support Channels
397
+
398
+ - [New Relic Documentation](https://docs.newrelic.com/docs/browser/browser-monitoring/getting-started/introduction-browser-monitoring/): Comprehensive guidance for using our platform
399
+
400
+ - [New Relic Community](https://support.newrelic.com/s/category/Category__c/Default): The best place to engage in troubleshooting questions
401
+
402
+ - [New Relic University](https://learn.newrelic.com/): A range of online training for New Relic users of every level
403
+
404
+ - [New Relic Technical Support](https://support.newrelic.com/s/#): 24/7/365 ticketed support. Read more about our [Technical Support Offerings](https://docs.newrelic.com/docs/licenses/license-information/general-usage-licenses/global-technical-support-offerings/).
395
405
 
396
406
  ## Contribute
397
407
 
@@ -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.298.0";
20
+ const VERSION = exports.VERSION = "1.299.0-rc.1";
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.298.0";
20
+ const VERSION = exports.VERSION = "1.299.0-rc.1";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -3,68 +3,84 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.generateSelectorPath = void 0;
6
+ exports.analyzeElemPath = void 0;
7
7
  /**
8
8
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
9
9
  * SPDX-License-Identifier: Apache-2.0
10
10
  */
11
11
 
12
12
  /**
13
- * Generates a CSS selector path for the given element, if possible
13
+ * Generates a CSS selector path for the given element, if possible.
14
+ * Also gather metadata about the element's nearest fields, and whether there are any links or buttons in the path.
15
+ *
16
+ * Starts with simple cases like window or document and progresses to more complex dom-tree traversals as needed.
17
+ * Will return path: undefined if no other path can be determined.
18
+ *
14
19
  * @param {HTMLElement} elem
15
- * @param {boolean} includeId
16
- * @param {boolean} includeClass
17
- * @returns {string|undefined}
20
+ * @param {Array<string>} [targetFields=[]] specifies which fields to gather from the nearest element in the path
21
+ * @returns {{path: (undefined|string), nearestFields: {}, hasButton: boolean, hasLink: boolean}}
18
22
  */
19
- const generateSelectorPath = (elem, targetFields = []) => {
20
- if (!elem) return {
23
+ const analyzeElemPath = (elem, targetFields = []) => {
24
+ const result = {
21
25
  path: undefined,
22
- nearestFields: {}
23
- };
24
- const getNthOfTypeIndex = node => {
25
- try {
26
- let i = 1;
27
- const {
28
- tagName
29
- } = node;
30
- while (node.previousElementSibling) {
31
- if (node.previousElementSibling.tagName === tagName) i++;
32
- node = node.previousElementSibling;
33
- }
34
- return i;
35
- } catch (err) {
36
- // do nothing for now. An invalid child count will make the path selector not return a nth-of-type selector statement
37
- }
26
+ nearestFields: {},
27
+ hasButton: false,
28
+ hasLink: false
38
29
  };
30
+ if (!elem) return result;
31
+ if (elem === window) {
32
+ result.path = 'window';
33
+ return result;
34
+ }
35
+ if (elem === document) {
36
+ result.path = 'document';
37
+ return result;
38
+ }
39
39
  let pathSelector = '';
40
- let index = getNthOfTypeIndex(elem);
41
- const nearestFields = {};
40
+ const index = getNthOfTypeIndex(elem);
42
41
  try {
43
42
  while (elem?.tagName) {
44
- const {
45
- id,
46
- localName
47
- } = elem;
43
+ const tagName = elem.tagName.toLowerCase();
44
+ result.hasLink ||= tagName === 'a';
45
+ result.hasButton ||= tagName === 'button' || tagName === 'input' && elem.type.toLowerCase() === 'button';
48
46
  targetFields.forEach(field => {
49
- nearestFields[nearestAttrName(field)] ||= elem[field]?.baseVal || elem[field];
47
+ result.nearestFields[nearestAttrName(field)] ||= elem[field]?.baseVal || elem[field];
50
48
  });
51
- const selector = [localName, id ? "#".concat(id) : '', pathSelector ? ">".concat(pathSelector) : ''].join('');
52
- pathSelector = selector;
49
+ pathSelector = buildPathSelector(elem, pathSelector);
53
50
  elem = elem.parentNode;
54
51
  }
55
52
  } catch (err) {
56
53
  // do nothing for now
57
54
  }
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
- }
55
+ result.path = pathSelector ? index ? "".concat(pathSelector, ":nth-of-type(").concat(index, ")") : pathSelector : undefined;
56
+ return result;
69
57
  };
70
- exports.generateSelectorPath = generateSelectorPath;
58
+ exports.analyzeElemPath = analyzeElemPath;
59
+ function buildPathSelector(elem, pathSelector) {
60
+ const {
61
+ id,
62
+ localName
63
+ } = elem;
64
+ return [localName, id ? "#".concat(id) : '', pathSelector ? ">".concat(pathSelector) : ''].join('');
65
+ }
66
+ function getNthOfTypeIndex(node) {
67
+ try {
68
+ let i = 1;
69
+ const {
70
+ tagName
71
+ } = node;
72
+ while (node.previousElementSibling) {
73
+ if (node.previousElementSibling.tagName === tagName) i++;
74
+ node = node.previousElementSibling;
75
+ }
76
+ return i;
77
+ } catch (err) {
78
+ // do nothing for now. An invalid child count will make the path selector not return a nth-of-type selector statement
79
+ }
80
+ }
81
+ function nearestAttrName(originalFieldName) {
82
+ /** preserve original renaming structure for pre-existing field maps */
83
+ if (originalFieldName === 'tagName') originalFieldName = 'tag';
84
+ if (originalFieldName === 'className') originalFieldName = 'class';
85
+ return "nearest".concat(originalFieldName.charAt(0).toUpperCase() + originalFieldName.slice(1));
86
+ }
@@ -153,14 +153,16 @@ function send(agentRef, {
153
153
  }
154
154
  const fullUrl = "".concat(url, "?").concat(baseParams).concat(payloadParams);
155
155
  const gzip = !!qs?.attributes?.includes('gzip');
156
- if (!gzip) {
157
- if (endpoint !== _features.EVENTS) body = (0, _stringify.stringify)(body); // all features going to 'events' endpoint should already be serialized & stringified
158
- // Warn--once per endpoint--if the agent tries to send large payloads
159
- if (body.length > 750000 && (warnings[endpoint] = (warnings[endpoint] || 0) + 1) === 1) (0, _console.warn)(28, endpoint);
160
- }
156
+
157
+ // all gzipped data is already in the correct format and needs no transformation
158
+ // all features going to 'events' endpoint should already be serialized & stringified
159
+ let stringBody = gzip || endpoint === _features.EVENTS ? body : (0, _stringify.stringify)(body);
161
160
 
162
161
  // If body is null, undefined, or an empty object or array after stringifying, send an empty string instead.
163
- if (!body || body.length === 0 || body === '{}' || body === '[]') body = '';
162
+ if (!stringBody || stringBody.length === 0 || stringBody === '{}' || stringBody === '[]') stringBody = '';
163
+
164
+ // Warn--once per endpoint--if the agent tries to send large payloads
165
+ if (endpoint !== _features.BLOBS && stringBody.length > 750000 && (warnings[endpoint] = (warnings[endpoint] || 0) + 1) === 1) (0, _console.warn)(28, endpoint);
164
166
  const headers = [{
165
167
  key: 'content-type',
166
168
  value: 'text/plain'
@@ -172,7 +174,7 @@ function send(agentRef, {
172
174
  Following the removal of img-element method. */
173
175
  let result = submitMethod({
174
176
  url: fullUrl,
175
- body,
177
+ body: stringBody,
176
178
  sync: localOpts.isFinalHarvest && _runtime.isWorkerScope,
177
179
  headers
178
180
  });
@@ -192,6 +194,9 @@ function send(agentRef, {
192
194
  };
193
195
  if (localOpts.needResponse) cbResult.responseText = this.responseText;
194
196
  cbFinished(cbResult);
197
+
198
+ /** temporary audit of consistency of harvest metadata flags */
199
+ if (!shouldRetry(this.status)) trackHarvestMetadata();
195
200
  }, (0, _eventListenerOpts.eventListenerOpts)(false));
196
201
  } else if (submitMethod === _submitData.xhrFetch) {
197
202
  result.then(async function (response) {
@@ -206,8 +211,33 @@ function send(agentRef, {
206
211
  };
207
212
  if (localOpts.needResponse) cbResult.responseText = await response.text();
208
213
  cbFinished(cbResult);
214
+ /** temporary audit of consistency of harvest metadata flags */
215
+ if (!shouldRetry(status)) trackHarvestMetadata();
209
216
  });
210
217
  }
218
+ function trackHarvestMetadata() {
219
+ try {
220
+ if (featureName === _features.FEATURE_NAMES.jserrors && !body?.err) return;
221
+ const hasReplay = baseParams.includes('hr=1');
222
+ const hasTrace = baseParams.includes('ht=1');
223
+ const hasError = qs?.attributes?.includes('hasError=true');
224
+ (0, _handle.handle)('harvest-metadata', [{
225
+ [featureName]: {
226
+ ...(hasReplay && {
227
+ hasReplay
228
+ }),
229
+ ...(hasTrace && {
230
+ hasTrace
231
+ }),
232
+ ...(hasError && {
233
+ hasError
234
+ })
235
+ }
236
+ }], undefined, _features.FEATURE_NAMES.metrics, agentRef.ee);
237
+ } catch (err) {
238
+ // do nothing
239
+ }
240
+ }
211
241
  }
212
242
  (0, _globalEvent.dispatchGlobalEvent)({
213
243
  agentIdentifier: agentRef.agentIdentifier,
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.extractUrl = extractUrl;
7
+ var _runtime = require("../constants/runtime");
8
+ var _nreum = require("../window/nreum");
9
+ /**
10
+ * Copyright 2020-2025 New Relic, Inc. All rights reserved.
11
+ * SPDX-License-Identifier: Apache-2.0
12
+ */
13
+
14
+ /**
15
+ * Extracts a URL from various target types.
16
+ * @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.
17
+ * @returns {string|undefined} The extracted URL as a string, or undefined if the target type is not supported.
18
+ */
19
+ function extractUrl(target) {
20
+ if (typeof target === 'string') return target;else if (target instanceof (0, _nreum.gosNREUMOriginals)().o.REQ) return target.url;else if (_runtime.globalScope?.URL && target instanceof URL) return target.href;
21
+ }
@@ -21,6 +21,7 @@ var _features = require("../../../loaders/features/features");
21
21
  var _constants2 = require("../../metrics/constants");
22
22
  var _now = require("../../../common/timing/now");
23
23
  var _denyList = require("../../../common/deny-list/deny-list");
24
+ var _extractUrl = require("../../../common/url/extract-url");
24
25
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
25
26
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } /**
26
27
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
@@ -298,15 +299,7 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
298
299
  if (fetchArguments.length >= 2) this.opts = fetchArguments[1];
299
300
  var opts = this.opts || {};
300
301
  var target = this.target;
301
- var url;
302
- if (typeof target === 'string') {
303
- url = target;
304
- } else if (typeof target === 'object' && target instanceof origRequest) {
305
- url = target.url;
306
- } else if (_runtime.globalScope?.URL && typeof target === 'object' && target instanceof URL) {
307
- url = target.href;
308
- }
309
- addUrl(this, url);
302
+ addUrl(this, (0, _extractUrl.extractUrl)(target));
310
303
  var method = ('' + (target && target instanceof origRequest && target.method || opts.method || 'GET')).toUpperCase();
311
304
  this.params.method = method;
312
305
  this.body = opts.body;
@@ -23,6 +23,7 @@ var _typeCheck = require("../../../common/util/type-check");
23
23
 
24
24
  class Aggregate extends _aggregateBase.AggregateBase {
25
25
  static featureName = _constants.FEATURE_NAME;
26
+ #userActionAggregator;
26
27
  constructor(agentRef) {
27
28
  super(agentRef, _constants.FEATURE_NAME);
28
29
  this.referrerUrl = _runtime.isBrowserScope && document.referrer ? (0, _cleanUrl.cleanURL)(document.referrer) : undefined;
@@ -32,7 +33,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
32
33
  this.deregisterDrain();
33
34
  return;
34
35
  }
35
- this.trackSupportabilityMetrics();
36
+ this.#trackSupportabilityMetrics();
36
37
  (0, _registerHandler.registerHandler)('api-recordCustomEvent', (timestamp, eventType, attributes) => {
37
38
  if (_constants.RESERVED_EVENT_TYPES.includes(eventType)) return (0, _console.warn)(46);
38
39
  this.addEvent({
@@ -60,8 +61,8 @@ class Aggregate extends _aggregateBase.AggregateBase {
60
61
  }
61
62
  let addUserAction = () => {/** no-op */};
62
63
  if (_runtime.isBrowserScope && agentRef.init.user_actions.enabled) {
63
- this.userActionAggregator = new _userActionsAggregator.UserActionsAggregator();
64
- this.harvestOpts.beforeUnload = () => addUserAction?.(this.userActionAggregator.aggregationEvent);
64
+ this.#userActionAggregator = new _userActionsAggregator.UserActionsAggregator(agentRef.init.feature_flags.includes('user_frustrations'));
65
+ this.harvestOpts.beforeUnload = () => addUserAction?.(this.#userActionAggregator.aggregationEvent);
65
66
  addUserAction = aggregatedUserAction => {
66
67
  try {
67
68
  /** The aggregator process only returns an event when it is "done" aggregating -
@@ -72,7 +73,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
72
73
  timeStamp,
73
74
  type
74
75
  } = aggregatedUserAction.event;
75
- this.addEvent({
76
+ const userActionEvent = {
76
77
  eventType: 'UserAction',
77
78
  timestamp: this.toEpoch(timeStamp),
78
79
  action: type,
@@ -90,8 +91,16 @@ class Aggregate extends _aggregateBase.AggregateBase {
90
91
  if (canTrustTargetAttribute(field)) acc[targetAttrName(field)] = String(target[field]).trim().slice(0, 128);
91
92
  return acc;
92
93
  }, {}),
93
- ...aggregatedUserAction.nearestTargetFields
94
- });
94
+ ...aggregatedUserAction.nearestTargetFields,
95
+ ...(aggregatedUserAction.deadClick && {
96
+ deadClick: true
97
+ }),
98
+ ...(aggregatedUserAction.errorClick && {
99
+ errorClick: true
100
+ })
101
+ };
102
+ this.addEvent(userActionEvent);
103
+ this.#trackUserActionSM(userActionEvent);
95
104
 
96
105
  /**
97
106
  * Returns the original target field name with `target` prepended and camelCased
@@ -121,8 +130,15 @@ class Aggregate extends _aggregateBase.AggregateBase {
121
130
  };
122
131
  (0, _registerHandler.registerHandler)('ua', evt => {
123
132
  /** the processor will return the previously aggregated event if it has been completed by processing the current event */
124
- addUserAction(this.userActionAggregator.process(evt, this.agentRef.init.user_actions.elementAttributes));
133
+ addUserAction(this.#userActionAggregator.process(evt, this.agentRef.init.user_actions.elementAttributes));
134
+ }, this.featureName, this.ee);
135
+ (0, _registerHandler.registerHandler)('navChange', () => {
136
+ this.#userActionAggregator.isLiveClick();
125
137
  }, this.featureName, this.ee);
138
+ (0, _registerHandler.registerHandler)('uaXhr', () => {
139
+ this.#userActionAggregator.isLiveClick();
140
+ }, this.featureName, this.ee);
141
+ (0, _registerHandler.registerHandler)('uaErr', () => this.#userActionAggregator.markAsErrorClick(), this.featureName, this.ee);
126
142
  }
127
143
 
128
144
  /**
@@ -298,7 +314,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
298
314
  toEpoch(timestamp) {
299
315
  return Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp));
300
316
  }
301
- trackSupportabilityMetrics() {
317
+ #trackSupportabilityMetrics() {
302
318
  /** track usage SMs to improve these experimental features */
303
319
  const configPerfTag = 'Config/Performance/';
304
320
  if (this.agentRef.init.performance.capture_marks) this.reportSupportabilityMetric(configPerfTag + 'CaptureMarks/Enabled');
@@ -308,5 +324,10 @@ class Aggregate extends _aggregateBase.AggregateBase {
308
324
  if (this.agentRef.init.performance.resources.first_party_domains?.length !== 0) this.reportSupportabilityMetric(configPerfTag + 'Resources/FirstPartyDomains/Changed');
309
325
  if (this.agentRef.init.performance.resources.ignore_newrelic === false) this.reportSupportabilityMetric(configPerfTag + 'Resources/IgnoreNewrelic/Changed');
310
326
  }
327
+ #trackUserActionSM(ua) {
328
+ if (ua.rageClick) this.reportSupportabilityMetric('UserAction/RageClick/Seen');
329
+ if (ua.deadClick) this.reportSupportabilityMetric('UserAction/DeadClick/Seen');
330
+ if (ua.errorClick) this.reportSupportabilityMetric('UserAction/ErrorClick/Seen');
331
+ }
311
332
  }
312
333
  exports.Aggregate = Aggregate;
@@ -12,15 +12,17 @@ var _cleanUrl = require("../../../../common/url/clean-url");
12
12
  */
13
13
 
14
14
  class AggregatedUserAction {
15
- constructor(evt, selectorPath, nearestTargetFields) {
15
+ constructor(evt, selectorInfo) {
16
16
  this.event = evt;
17
17
  this.count = 1;
18
18
  this.originMs = Math.floor(evt.timeStamp);
19
19
  this.relativeMs = [0];
20
- this.selectorPath = selectorPath;
20
+ this.selectorPath = selectorInfo.path;
21
21
  this.rageClick = undefined;
22
- this.nearestTargetFields = nearestTargetFields;
22
+ this.nearestTargetFields = selectorInfo.nearestFields;
23
23
  this.currentUrl = (0, _cleanUrl.cleanURL)('' + location);
24
+ this.deadClick = false;
25
+ this.errorClick = false;
24
26
  }
25
27
 
26
28
  /**
@@ -7,6 +7,8 @@ exports.UserActionsAggregator = void 0;
7
7
  var _selectorPath = require("../../../../common/dom/selector-path");
8
8
  var _constants = require("../../constants");
9
9
  var _aggregatedUserAction = require("./aggregated-user-action");
10
+ var _timer = require("../../../../common/timer/timer");
11
+ var _nreum = require("../../../../common/window/nreum");
10
12
  /**
11
13
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
12
14
  * SPDX-License-Identifier: Apache-2.0
@@ -16,6 +18,16 @@ class UserActionsAggregator {
16
18
  /** @type {AggregatedUserAction=} */
17
19
  #aggregationEvent = undefined;
18
20
  #aggregationKey = '';
21
+ #ufEnabled = false;
22
+ #deadClickTimer = undefined;
23
+ #domObserver = undefined;
24
+ #errorClickTimer = undefined;
25
+ constructor(userFrustrationsEnabled) {
26
+ if (userFrustrationsEnabled && (0, _nreum.gosNREUMOriginals)().o.MO) {
27
+ this.#domObserver = new MutationObserver(this.isLiveClick.bind(this));
28
+ this.#ufEnabled = true;
29
+ }
30
+ }
19
31
  get aggregationEvent() {
20
32
  // if this is accessed externally, we need to be done aggregating on it
21
33
  // to prevent potential mutability and duplication issues, so the state is cleared upon returning.
@@ -33,50 +45,75 @@ class UserActionsAggregator {
33
45
  */
34
46
  process(evt, targetFields) {
35
47
  if (!evt) return;
36
- const {
37
- selectorPath,
38
- nearestTargetFields
39
- } = getSelectorPath(evt, targetFields);
40
- const aggregationKey = getAggregationKey(evt, selectorPath);
48
+ const targetElem = _constants.OBSERVED_WINDOW_EVENTS.includes(evt.type) ? window : evt.target;
49
+ const selectorInfo = (0, _selectorPath.analyzeElemPath)(targetElem, targetFields);
50
+
51
+ // if selectorInfo.path is undefined, aggregation will be skipped for this event
52
+ const aggregationKey = getAggregationKey(evt, selectorInfo.path);
41
53
  if (!!aggregationKey && aggregationKey === this.#aggregationKey) {
42
54
  // an aggregation exists already, so lets just continue to increment
43
55
  this.#aggregationEvent.aggregate(evt);
44
56
  } else {
45
57
  // return the prev existing one (if there is one)
46
58
  const finishedEvent = this.#aggregationEvent;
47
- // then set as this new event aggregation
59
+ if (this.#ufEnabled) {
60
+ this.#deadClickCleanup();
61
+ this.#errorClickCleanup();
62
+ }
63
+
64
+ // then start new event aggregation
48
65
  this.#aggregationKey = aggregationKey;
49
- this.#aggregationEvent = new _aggregatedUserAction.AggregatedUserAction(evt, selectorPath, nearestTargetFields);
66
+ this.#aggregationEvent = new _aggregatedUserAction.AggregatedUserAction(evt, selectorInfo);
67
+ if (this.#ufEnabled && evt.type === 'click' && (selectorInfo.hasButton || selectorInfo.hasLink)) {
68
+ this.#deadClickSetup(this.#aggregationEvent);
69
+ this.#errorClickSetup();
70
+ }
50
71
  return finishedEvent;
51
72
  }
52
73
  }
53
- }
54
-
55
- /**
56
- * Generates a selector path for the event, starting with simple cases like window or document and getting more complex for dom-tree traversals as needed.
57
- * Will return a random selector path value if no other path can be determined, to force the aggregator to skip aggregation for this event.
58
- * @param {Event} evt
59
- * @returns {string}
60
- */
61
- exports.UserActionsAggregator = UserActionsAggregator;
62
- function getSelectorPath(evt, targetFields) {
63
- let selectorPath;
64
- let nearestTargetFields = {};
65
- if (_constants.OBSERVED_WINDOW_EVENTS.includes(evt.type) || evt.target === window) selectorPath = 'window';else if (evt.target === document) selectorPath = 'document';
66
- // if still no selectorPath, generate one from target tree that includes elem ids
67
- else {
68
- const {
69
- path,
70
- nearestFields
71
- } = (0, _selectorPath.generateSelectorPath)(evt.target, targetFields);
72
- selectorPath = path;
73
- nearestTargetFields = nearestFields;
74
+ markAsErrorClick() {
75
+ if (this.#aggregationEvent && this.#errorClickTimer) {
76
+ this.#aggregationEvent.errorClick = true;
77
+ this.#errorClickCleanup();
78
+ }
79
+ }
80
+ #errorClickSetup() {
81
+ this.#errorClickTimer = new _timer.Timer({
82
+ onEnd: () => {
83
+ this.#errorClickCleanup();
84
+ }
85
+ }, _constants.FRUSTRATION_TIMEOUT_MS);
86
+ }
87
+ #errorClickCleanup() {
88
+ this.#errorClickTimer?.clear();
89
+ this.#errorClickTimer = undefined;
90
+ }
91
+ #deadClickSetup(userAction) {
92
+ if (this.#isEvaluatingDeadClick() || !this.#domObserver) return;
93
+ this.#domObserver.observe(document, {
94
+ attributes: true,
95
+ characterData: true,
96
+ childList: true,
97
+ subtree: true
98
+ });
99
+ this.#deadClickTimer = new _timer.Timer({
100
+ onEnd: () => {
101
+ userAction.deadClick = true;
102
+ this.#deadClickCleanup();
103
+ }
104
+ }, _constants.FRUSTRATION_TIMEOUT_MS);
105
+ }
106
+ #deadClickCleanup() {
107
+ this.#domObserver?.disconnect();
108
+ this.#deadClickTimer?.clear();
109
+ this.#deadClickTimer = undefined;
110
+ }
111
+ #isEvaluatingDeadClick() {
112
+ return this.#deadClickTimer !== undefined;
113
+ }
114
+ isLiveClick() {
115
+ if (this.#isEvaluatingDeadClick()) this.#deadClickCleanup();
74
116
  }
75
- // if STILL no selectorPath, it will return undefined which will skip aggregation for this event
76
- return {
77
- selectorPath,
78
- nearestTargetFields
79
- };
80
117
  }
81
118
 
82
119
  /**
@@ -86,6 +123,7 @@ function getSelectorPath(evt, targetFields) {
86
123
  * @param {string} selectorPath
87
124
  * @returns {string}
88
125
  */
126
+ exports.UserActionsAggregator = UserActionsAggregator;
89
127
  function getAggregationKey(evt, selectorPath) {
90
128
  let aggregationKey = evt.type;
91
129
  /** aggregate all scrollends into one set (if sequential), no matter what their target is
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.RESERVED_EVENT_TYPES = exports.RAGE_CLICK_THRESHOLD_MS = exports.RAGE_CLICK_THRESHOLD_EVENTS = exports.OBSERVED_WINDOW_EVENTS = exports.OBSERVED_EVENTS = exports.FEATURE_NAME = exports.FEATURE_FLAGS = void 0;
6
+ exports.RESERVED_EVENT_TYPES = exports.RAGE_CLICK_THRESHOLD_MS = exports.RAGE_CLICK_THRESHOLD_EVENTS = exports.OBSERVED_WINDOW_EVENTS = exports.OBSERVED_EVENTS = exports.FRUSTRATION_TIMEOUT_MS = exports.FEATURE_NAME = exports.FEATURE_FLAGS = void 0;
7
7
  var _features = require("../../loaders/features/features");
8
8
  /**
9
9
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
@@ -15,6 +15,7 @@ const OBSERVED_EVENTS = exports.OBSERVED_EVENTS = ['auxclick', 'click', 'copy',
15
15
  const OBSERVED_WINDOW_EVENTS = exports.OBSERVED_WINDOW_EVENTS = ['focus', 'blur'];
16
16
  const RAGE_CLICK_THRESHOLD_EVENTS = exports.RAGE_CLICK_THRESHOLD_EVENTS = 4;
17
17
  const RAGE_CLICK_THRESHOLD_MS = exports.RAGE_CLICK_THRESHOLD_MS = 1000;
18
+ const FRUSTRATION_TIMEOUT_MS = exports.FRUSTRATION_TIMEOUT_MS = 2000;
18
19
  const RESERVED_EVENT_TYPES = exports.RESERVED_EVENT_TYPES = ['PageAction', 'UserAction', 'BrowserPerformance'];
19
20
  const FEATURE_FLAGS = exports.FEATURE_FLAGS = {
20
21
  MARKS: 'experimental.marks',
@@ -15,6 +15,12 @@ var _register = require("../../../loaders/api/register");
15
15
  var _measure = require("../../../loaders/api/measure");
16
16
  var _instrumentBase = require("../../utils/instrument-base");
17
17
  var _constants = require("../constants");
18
+ var _features = require("../../../loaders/features/features");
19
+ var _wrapHistory = require("../../../common/wrap/wrap-history");
20
+ var _wrapFetch = require("../../../common/wrap/wrap-fetch");
21
+ var _wrapXhr = require("../../../common/wrap/wrap-xhr");
22
+ var _parseUrl = require("../../../common/url/parse-url");
23
+ var _extractUrl = require("../../../common/url/extract-url");
18
24
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
19
25
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } /**
20
26
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
@@ -58,6 +64,45 @@ class Instrument extends _instrumentBase.InstrumentBase {
58
64
  buffered: true
59
65
  });
60
66
  }
67
+ const historyEE = (0, _wrapHistory.wrapHistory)(this.ee);
68
+ historyEE.on('pushState-end', navigationChange);
69
+ historyEE.on('replaceState-end', navigationChange);
70
+ window.addEventListener('hashchange', navigationChange, (0, _eventListenerOpts.eventListenerOpts)(true, this.removeOnAbort?.signal));
71
+ window.addEventListener('popstate', navigationChange, (0, _eventListenerOpts.eventListenerOpts)(true, this.removeOnAbort?.signal));
72
+ function navigationChange() {
73
+ historyEE.emit('navChange');
74
+ }
75
+ }
76
+ try {
77
+ this.removeOnAbort = new AbortController();
78
+ } catch (e) {}
79
+ this.abortHandler = () => {
80
+ this.removeOnAbort?.abort();
81
+ this.abortHandler = undefined; // weakly allow this abort op to run only once
82
+ };
83
+ _runtime.globalScope.addEventListener('error', () => {
84
+ (0, _handle.handle)('uaErr', [], undefined, _features.FEATURE_NAMES.genericEvents, this.ee);
85
+ }, (0, _eventListenerOpts.eventListenerOpts)(false, this.removeOnAbort?.signal));
86
+ (0, _wrapFetch.wrapFetch)(this.ee);
87
+ (0, _wrapXhr.wrapXhr)(this.ee);
88
+ this.ee.on('open-xhr-start', (args, xhr) => {
89
+ if (!isInternalTraffic(args[1])) {
90
+ xhr.addEventListener('readystatechange', () => {
91
+ if (xhr.readyState === 2) {
92
+ // HEADERS_RECEIVED
93
+ (0, _handle.handle)('uaXhr', [], undefined, _features.FEATURE_NAMES.genericEvents, this.ee);
94
+ }
95
+ });
96
+ }
97
+ });
98
+ this.ee.on('fetch-start', fetchArguments => {
99
+ if (fetchArguments.length >= 1 && !isInternalTraffic((0, _extractUrl.extractUrl)(fetchArguments[0]))) {
100
+ (0, _handle.handle)('uaXhr', [], undefined, _features.FEATURE_NAMES.genericEvents, this.ee);
101
+ }
102
+ });
103
+ function isInternalTraffic(url) {
104
+ const parsedUrl = (0, _parseUrl.parseUrl)(url);
105
+ return agentRef.beacons.includes(parsedUrl.hostname + ':' + parsedUrl.port);
61
106
  }
62
107
 
63
108
  /** If any of the sources are active, import the aggregator. otherwise deregister */