@newrelic/browser-agent 1.306.0 → 1.307.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 (73) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cjs/cdn/spa.js +4 -6
  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/deny-list/deny-list.js +22 -36
  6. package/dist/cjs/common/harvest/harvester.js +2 -1
  7. package/dist/cjs/features/ajax/aggregate/index.js +1 -20
  8. package/dist/cjs/features/jserrors/aggregate/index.js +9 -67
  9. package/dist/cjs/features/soft_navigations/aggregate/index.js +26 -16
  10. package/dist/cjs/features/utils/agent-session.js +10 -0
  11. package/dist/cjs/features/utils/aggregate-base.js +1 -1
  12. package/dist/cjs/interfaces/registered-entity.js +2 -1
  13. package/dist/cjs/loaders/agent.js +5 -4
  14. package/dist/cjs/loaders/api/interaction.js +4 -4
  15. package/dist/cjs/loaders/api/register-api-types.js +1 -1
  16. package/dist/cjs/loaders/api/setUserId.js +13 -3
  17. package/dist/cjs/loaders/api-base.js +3 -2
  18. package/dist/cjs/loaders/browser-agent.js +6 -7
  19. package/dist/cjs/loaders/configure/configure.js +0 -3
  20. package/dist/esm/cdn/spa.js +2 -4
  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/deny-list/deny-list.js +22 -36
  24. package/dist/esm/common/harvest/harvester.js +2 -1
  25. package/dist/esm/features/ajax/aggregate/index.js +1 -20
  26. package/dist/esm/features/jserrors/aggregate/index.js +9 -67
  27. package/dist/esm/features/soft_navigations/aggregate/index.js +26 -16
  28. package/dist/esm/features/utils/agent-session.js +10 -0
  29. package/dist/esm/features/utils/aggregate-base.js +1 -1
  30. package/dist/esm/interfaces/registered-entity.js +2 -1
  31. package/dist/esm/loaders/agent.js +5 -4
  32. package/dist/esm/loaders/api/interaction.js +5 -5
  33. package/dist/esm/loaders/api/register-api-types.js +1 -1
  34. package/dist/esm/loaders/api/setUserId.js +14 -4
  35. package/dist/esm/loaders/api-base.js +3 -2
  36. package/dist/esm/loaders/browser-agent.js +2 -3
  37. package/dist/esm/loaders/configure/configure.js +0 -3
  38. package/dist/types/common/deny-list/deny-list.d.ts.map +1 -1
  39. package/dist/types/features/ajax/aggregate/index.d.ts +0 -1
  40. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  41. package/dist/types/features/jserrors/aggregate/index.d.ts +0 -3
  42. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  43. package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
  44. package/dist/types/features/utils/agent-session.d.ts.map +1 -1
  45. package/dist/types/interfaces/registered-entity.d.ts +2 -1
  46. package/dist/types/interfaces/registered-entity.d.ts.map +1 -1
  47. package/dist/types/loaders/agent.d.ts +0 -1
  48. package/dist/types/loaders/agent.d.ts.map +1 -1
  49. package/dist/types/loaders/api/interaction.d.ts.map +1 -1
  50. package/dist/types/loaders/api/register-api-types.d.ts +2 -2
  51. package/dist/types/loaders/api/register-api-types.d.ts.map +1 -1
  52. package/dist/types/loaders/api/setUserId.d.ts.map +1 -1
  53. package/dist/types/loaders/api-base.d.ts +2 -1
  54. package/dist/types/loaders/api-base.d.ts.map +1 -1
  55. package/dist/types/loaders/browser-agent.d.ts.map +1 -1
  56. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  57. package/package.json +2 -2
  58. package/src/cdn/spa.js +2 -4
  59. package/src/common/deny-list/deny-list.js +25 -40
  60. package/src/common/harvest/harvester.js +1 -1
  61. package/src/features/ajax/aggregate/index.js +2 -18
  62. package/src/features/jserrors/aggregate/index.js +8 -76
  63. package/src/features/soft_navigations/aggregate/index.js +26 -18
  64. package/src/features/utils/agent-session.js +11 -0
  65. package/src/features/utils/aggregate-base.js +1 -1
  66. package/src/interfaces/registered-entity.js +2 -1
  67. package/src/loaders/agent.js +5 -4
  68. package/src/loaders/api/interaction.js +5 -6
  69. package/src/loaders/api/register-api-types.js +1 -1
  70. package/src/loaders/api/setUserId.js +15 -4
  71. package/src/loaders/api-base.js +3 -2
  72. package/src/loaders/browser-agent.js +1 -3
  73. package/src/loaders/configure/configure.js +0 -3
package/CHANGELOG.md CHANGED
@@ -3,6 +3,15 @@
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.307.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.306.0...v1.307.0) (2026-01-06)
7
+
8
+
9
+ ### Features
10
+
11
+ * Add resetSession option to setUserId() API ([#1646](https://github.com/newrelic/newrelic-browser-agent/issues/1646)) ([c284cfd](https://github.com/newrelic/newrelic-browser-agent/commit/c284cfde4a18fb9afa75c11b181944401a902e13))
12
+ * Ajax deny list wildcard support ([#1655](https://github.com/newrelic/newrelic-browser-agent/issues/1655)) ([61fa99b](https://github.com/newrelic/newrelic-browser-agent/commit/61fa99ba39334b85f7611a076943f15356f65364))
13
+ * Make soft navigations feature the default SPA ([#1638](https://github.com/newrelic/newrelic-browser-agent/issues/1638)) ([d93f3f3](https://github.com/newrelic/newrelic-browser-agent/commit/d93f3f388085b7e76292e79749977db19a10765a))
14
+
6
15
  ## [1.306.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.305.0...v1.306.0) (2025-12-16)
7
16
 
8
17
 
@@ -9,11 +9,10 @@ var _instrument5 = require("../features/ajax/instrument");
9
9
  var _instrument6 = require("../features/session_trace/instrument");
10
10
  var _instrument7 = require("../features/session_replay/instrument");
11
11
  var _instrument8 = require("../features/soft_navigations/instrument");
12
- var _instrument9 = require("../features/spa/instrument");
13
- var _instrument0 = require("../features/generic_events/instrument");
14
- var _instrument1 = require("../features/logging/instrument");
12
+ var _instrument9 = require("../features/generic_events/instrument");
13
+ var _instrument0 = require("../features/logging/instrument");
15
14
  /**
16
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
15
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
17
16
  * SPDX-License-Identifier: Apache-2.0
18
17
  */
19
18
 
@@ -22,7 +21,6 @@ var _instrument1 = require("../features/logging/instrument");
22
21
  */
23
22
 
24
23
  new _agent.Agent({
25
- features: [_instrument5.Instrument, _instrument.Instrument, _instrument2.Instrument, _instrument6.Instrument, _instrument7.Instrument, _instrument3.Instrument, _instrument4.Instrument, _instrument0.Instrument, _instrument1.Instrument, _instrument8.Instrument, _instrument9.Instrument // either the softnav or the old spa will be used (not both), but we still need to pack both to avoid dynamic import for instrument files
26
- ],
24
+ features: [_instrument5.Instrument, _instrument.Instrument, _instrument2.Instrument, _instrument6.Instrument, _instrument7.Instrument, _instrument3.Instrument, _instrument4.Instrument, _instrument9.Instrument, _instrument0.Instrument, _instrument8.Instrument],
27
25
  loaderType: 'spa'
28
26
  });
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.RRWEB_PACKAGE_NAME = exports.D
17
17
  /**
18
18
  * Exposes the version of the agent
19
19
  */
20
- const VERSION = exports.VERSION = "1.306.0";
20
+ const VERSION = exports.VERSION = "1.307.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.RRWEB_PACKAGE_NAME = exports.D
17
17
  /**
18
18
  * Exposes the version of the agent
19
19
  */
20
- const VERSION = exports.VERSION = "1.306.0";
20
+ const VERSION = exports.VERSION = "1.307.0-rc.1";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -26,12 +26,12 @@ var denyList = [];
26
26
  function shouldCollectEvent(params) {
27
27
  if (!params || hasUndefinedHostname(params)) return false;
28
28
  if (denyList.length === 0) return true;
29
+
30
+ // short circuit if deny list contains just a wildcard
31
+ if (denyList[0].hostname === '*') return false;
29
32
  for (var i = 0; i < denyList.length; i++) {
30
33
  var parsed = denyList[i];
31
- if (parsed.hostname === '*') {
32
- return false;
33
- }
34
- if (domainMatchesPattern(parsed.hostname, params.hostname) && comparePath(parsed.pathname, params.pathname)) {
34
+ if (parsed.hostname.test(params.hostname) && parsed.pathname.test(params.pathname)) {
35
35
  return false;
36
36
  }
37
37
  }
@@ -54,6 +54,13 @@ function setDenyList(denyListConfig) {
54
54
  let url = denyListConfig[i];
55
55
  if (!url) continue; // ignore bad values like undefined or empty strings
56
56
 
57
+ // short circuit if deny list entry is just a wildcard
58
+ if (url === '*') {
59
+ denyList = [{
60
+ hostname: '*'
61
+ }];
62
+ return;
63
+ }
57
64
  if (url.indexOf('http://') === 0) {
58
65
  url = url.substring(7);
59
66
  } else if (url.indexOf('https://') === 0) {
@@ -66,45 +73,24 @@ function setDenyList(denyListConfig) {
66
73
  pathname = url.substring(firstSlash);
67
74
  } else {
68
75
  host = url;
69
- pathname = '';
76
+ pathname = '*';
70
77
  }
71
78
  let [hostname] = host.split(':');
72
79
  denyList.push({
73
- hostname,
74
- pathname
80
+ hostname: convertToRegularExpression(hostname),
81
+ pathname: convertToRegularExpression(pathname, true)
75
82
  });
76
83
  }
77
84
  }
78
- /**
79
- * Returns true if the right side of `domain` (end of string) matches `pattern`.
80
- * @param {string} pattern - a string to be matched against the end of `domain` string
81
- * @param {string} domain - a domain string with no protocol or path (e.g., app1.example.com)
82
- * @returns {boolean} `true` if domain matches pattern; else `false`
83
- */
84
- function domainMatchesPattern(pattern, domain) {
85
- if (pattern.length > domain.length) {
86
- return false;
87
- }
88
- return domain.indexOf(pattern) === domain.length - pattern.length;
89
- }
90
85
 
91
86
  /**
92
- * Returns true if a URL path matches a pattern string, disregarding leading slashes.
93
- * @param {string} pattern - a string to compare with path (e.g., api/v1)
94
- * @param {string} path - a string representing a URL path (e.g., /api/v1)
95
- * @returns {boolean} `true` if path and pattern are an exact string match (except for leading slashes); else `false`
87
+ * Converts a deny list filter string into a regular expression object with wildcard support
88
+ * @param {string} filter - deny list filter to convert
89
+ * @param {boolean} [isPathname=false] - indicates if the filter is a pathname
90
+ * @returns {RegExp} - regular expression object built from the input string
96
91
  */
97
- function comparePath(pattern, path) {
98
- if (pattern.indexOf('/') === 0) {
99
- pattern = pattern.substring(1);
100
- }
101
- if (path.indexOf('/') === 0) {
102
- path = path.substring(1);
103
- }
104
-
105
- // No path in pattern means match all paths.
106
- if (pattern === '') {
107
- return true;
108
- }
109
- return pattern === path;
92
+ function convertToRegularExpression(filter, isPathname = false) {
93
+ const newFilter = filter.replace(/[.+?^${}()|[\]\\]/g, m => '\\' + m) // use a replacer function to not break apm injection
94
+ .replace(/\*/g, '.*?'); // use lazy matching instead of greedy
95
+ return new RegExp((isPathname ? '^' : '') + newFilter + '$');
110
96
  }
@@ -85,7 +85,8 @@ class Harvester {
85
85
  featureName: aggregateInst.featureName,
86
86
  endpointVersion: output.endpointVersion
87
87
  });
88
- output.ranSend = true;
88
+ output.ranSend = true; // Set to true if we attempted to send (even if send() returned false due to missing errorBeacon in tests)
89
+
89
90
  return output;
90
91
 
91
92
  /**
@@ -24,7 +24,6 @@ class Aggregate extends _aggregateBase.AggregateBase {
24
24
  constructor(agentRef) {
25
25
  super(agentRef, _constants.FEATURE_NAME);
26
26
  (0, _denyList.setDenyList)(agentRef.runtime.denyList);
27
- this.underSpaEvents = {};
28
27
  const classThis = this;
29
28
  if (!agentRef.init.ajax.block_internal) {
30
29
  // if the agent is tracking ITSELF, it can spawn endless ajax requests early if they are large from custom attributes, so we just disable early harvest for ajax in this case.
@@ -32,20 +31,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
32
31
  } else {
33
32
  super.customAttributesAreSeparate = true;
34
33
  }
35
-
36
- // --- v Used by old spa feature
37
- this.ee.on('interactionDone', (interaction, wasSaved) => {
38
- if (!this.underSpaEvents[interaction.id]) return;
39
- if (!wasSaved) {
40
- // if the ixn was saved, then its ajax reqs are part of the payload whereas if it was discarded, it should still be harvested in the ajax feature itself
41
- this.underSpaEvents[interaction.id].forEach(item => this.events.add(item));
42
- }
43
- delete this.underSpaEvents[interaction.id];
44
- });
45
- // --- ^
46
- // --- v Used by new soft nav
47
34
  (0, _registerHandler.registerHandler)('returnAjax', event => this.events.add(event), this.featureName, this.ee);
48
- // --- ^
49
35
  (0, _registerHandler.registerHandler)('xhr', function () {
50
36
  // the EE-drain system not only switches "this" but also passes a new EventContext with info. Should consider platform refactor to another system which passes a mutable context around separately and predictably to avoid problems like this.
51
37
  classThis.storeXhr(...arguments, this); // this switches the context back to the class instance while passing the NR context as an argument -- see "ctx" in storeXhr
@@ -116,13 +102,8 @@ class Aggregate extends _aggregateBase.AggregateBase {
116
102
  if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', (0, _stringify.stringify)(event.gql).length);
117
103
  const softNavInUse = Boolean(this.agentRef.features?.[_features.FEATURE_NAMES.softNav]);
118
104
  if (softNavInUse) {
119
- // For newer soft nav (when running), pass the event w/ info to it for evaluation -- either part of an interaction or is given back
105
+ // when SN is running, pass the event w/ info to it for evaluation -- either part of an interaction or is given back
120
106
  (0, _handle.handle)('ajax', [event, ctx], undefined, _features.FEATURE_NAMES.softNav, this.ee);
121
- } else if (ctx.spaNode) {
122
- // For old spa (when running), if the ajax happened inside an interaction, hold it until the interaction finishes
123
- const interactionId = ctx.spaNode.interaction.id;
124
- this.underSpaEvents[interactionId] ??= [];
125
- this.underSpaEvents[interactionId].push(event);
126
107
  } else {
127
108
  this.events.add(event);
128
109
  }
@@ -39,14 +39,10 @@ class Aggregate extends _aggregateBase.AggregateBase {
39
39
  this.stackReported = {};
40
40
  this.observedAt = {};
41
41
  this.pageviewReported = {};
42
- this.bufferedErrorsUnderSpa = {};
43
42
  this.errorOnPage = false;
44
-
45
- // this will need to change to match whatever ee we use in the instrument
46
- this.ee.on('interactionDone', (interaction, wasSaved) => this.onInteractionDone(interaction, wasSaved));
47
43
  (0, _registerHandler.registerHandler)('err', (...args) => this.storeError(...args), this.featureName, this.ee);
48
44
  (0, _registerHandler.registerHandler)('ierr', (...args) => this.storeError(...args), this.featureName, this.ee);
49
- (0, _registerHandler.registerHandler)('softNavFlush', (interactionId, wasFinished, softNavAttrs, interactionEndTime) => this.onSoftNavNotification(interactionId, wasFinished, softNavAttrs, interactionEndTime), this.featureName, this.ee); // when an ixn is done or cancelled
45
+ (0, _registerHandler.registerHandler)('returnJserror', (jsErrorEvent, softNavAttrs) => this.#storeJserrorForHarvest(jsErrorEvent, softNavAttrs), this.featureName, this.ee);
50
46
 
51
47
  // 0 == off, 1 == on
52
48
  this.waitForFlags(['err']).then(([errFlag]) => {
@@ -193,43 +189,26 @@ class Aggregate extends _aggregateBase.AggregateBase {
193
189
  }
194
190
  if (this.shouldAllowMainAgentToCapture(target)) {
195
191
  const softNavInUse = Boolean(this.agentRef.features?.[_features.FEATURE_NAMES.softNav]);
196
- // Note: the following are subject to potential race cond wherein if the other feature aren't fully initialized, it'll be treated as there being no associated interaction.
197
- // They each will also tack on their respective properties to the params object as part of the decision flow.
198
- if (softNavInUse) (0, _handle.handle)('jserror', [params, time], undefined, _features.FEATURE_NAMES.softNav, this.ee);else (0, _handle.handle)('spa-jserror', jsErrorEvent, undefined, _features.FEATURE_NAMES.spa, this.ee);
199
- if (params.browserInteractionId && !params._softNavFinished) {
200
- // hold onto the error until the in-progress interaction is done, eithered saved or discarded
201
- this.bufferedErrorsUnderSpa[params.browserInteractionId] ??= [];
202
- this.bufferedErrorsUnderSpa[params.browserInteractionId].push(jsErrorEvent);
203
- } else if (params._interactionId != null) {
204
- // same as above, except tailored for the way old spa does it
205
- this.bufferedErrorsUnderSpa[params._interactionId] = this.bufferedErrorsUnderSpa[params._interactionId] || [];
206
- this.bufferedErrorsUnderSpa[params._interactionId].push(jsErrorEvent);
192
+ if (softNavInUse) {
193
+ // pass the error to soft nav for evaluation - it will return it via 'returnJserror' when interaction is resolved
194
+ (0, _handle.handle)('jserror', [jsErrorEvent], undefined, _features.FEATURE_NAMES.softNav, this.ee);
207
195
  } else {
208
- // Either there is no interaction (then all these params properties will be undefined) OR there's a related soft navigation that's already completed.
209
- // The old spa does not look up completed interactions at all, so there's no need to consider it.
210
- this.#storeJserrorForHarvest(jsErrorEvent, params.browserInteractionId !== undefined, params._softNavAttributes);
196
+ this.#storeJserrorForHarvest(jsErrorEvent, false);
211
197
  }
212
198
  }
213
199
 
214
200
  // always add directly if scoped to a sub-entity, the other pathways above will be deterministic if the main agent should procede
215
201
  if (target) this.#storeJserrorForHarvest([...jsErrorEvent, target], false, params._softNavAttributes);
216
202
  }
217
- #storeJserrorForHarvest(errorInfoArr, softNavOccurredFinished, softNavCustomAttrs = {}) {
203
+ #storeJserrorForHarvest(errorInfoArr, softNavCustomAttrs = {}) {
218
204
  let [type, bucketHash, params, newMetrics, localAttrs, target] = errorInfoArr;
219
205
  const allCustomAttrs = {
220
206
  /** MFE specific attributes if in "multiple" mode (ie consumer version 2) */
221
207
  ...(0, _mfe.getVersion2Attributes)(target, this)
222
208
  };
223
- if (softNavOccurredFinished) {
224
- Object.entries(softNavCustomAttrs).forEach(([k, v]) => setCustom(k, v)); // when an ixn finishes, it'll include stuff in jsAttributes + attrs specific to the ixn
225
- bucketHash += params.browserInteractionId;
226
- delete params._softNavAttributes; // cleanup temp properties from synchronous evaluation; this is harmless when async from soft nav (properties DNE)
227
- delete params._softNavFinished;
228
- } else {
229
- // interaction was cancelled -> error should not be associated OR there was no interaction
230
- Object.entries(this.agentRef.info.jsAttributes).forEach(([k, v]) => setCustom(k, v));
231
- delete params.browserInteractionId;
232
- }
209
+ Object.entries(this.agentRef.info.jsAttributes).forEach(([k, v]) => setCustom(k, v));
210
+ Object.entries(softNavCustomAttrs).forEach(([k, v]) => setCustom(k, v)); // when an ixn finishes, it'll pass attrs specific to the ixn; if no associated ixn, this defaults to empty
211
+ if (params.browserInteractionId) bucketHash += params.browserInteractionId;
233
212
  if (localAttrs) Object.entries(localAttrs).forEach(([k, v]) => setCustom(k, v)); // local custom attrs are applied in either case with the highest precedence
234
213
 
235
214
  const jsAttributesHash = (0, _stringHashCode.stringHashCode)((0, _stringify.stringify)(allCustomAttrs));
@@ -249,42 +228,5 @@ class Aggregate extends _aggregateBase.AggregateBase {
249
228
  shouldAllowMainAgentToCapture(target) {
250
229
  return !target || this.agentRef.init.api.duplicate_registered_data;
251
230
  }
252
-
253
- // TO-DO: Remove this function when old spa is taken out. #storeJserrorForHarvest handles the work with the softnav feature.
254
- onInteractionDone(interaction, wasSaved) {
255
- if (!this.bufferedErrorsUnderSpa[interaction.id] || this.blocked) return;
256
- this.bufferedErrorsUnderSpa[interaction.id].forEach(item => {
257
- var allCustomAttrs = {};
258
- const localCustomAttrs = item[4];
259
- Object.entries(interaction.root.attrs.custom || {}).forEach(setCustom); // tack on custom attrs from the interaction
260
- Object.entries(localCustomAttrs || {}).forEach(setCustom);
261
- var params = item[2];
262
- if (wasSaved) {
263
- params.browserInteractionId = interaction.root.attrs.id;
264
- if (params._interactionNodeId) params.parentNodeId = params._interactionNodeId.toString();
265
- }
266
- delete params._interactionId;
267
- delete params._interactionNodeId;
268
- var hash = wasSaved ? item[1] + interaction.root.attrs.id : item[1];
269
- var jsAttributesHash = (0, _stringHashCode.stringHashCode)((0, _stringify.stringify)(allCustomAttrs));
270
- var aggregateHash = hash + ':' + jsAttributesHash;
271
- this.events.add([item[0], aggregateHash, params, item[3], allCustomAttrs], item[5]);
272
- function setCustom([key, val]) {
273
- allCustomAttrs[key] = val && typeof val === 'object' ? (0, _stringify.stringify)(val) : val;
274
- }
275
- });
276
- delete this.bufferedErrorsUnderSpa[interaction.id];
277
- }
278
- onSoftNavNotification(interactionId, wasFinished, softNavAttrs, interactionEndTime) {
279
- if (this.blocked) return;
280
- this.bufferedErrorsUnderSpa[interactionId]?.forEach(jsErrorEvent => {
281
- // this should not modify the re-used softNavAttrs contents
282
- if (!wasFinished) return this.#storeJserrorForHarvest(jsErrorEvent, false, softNavAttrs);
283
- const startTime = jsErrorEvent[3].time; // in storeError fn, the newMetrics obj contains the time passed to & used by SN to seek the ixn
284
- if (startTime > interactionEndTime) return this.#storeJserrorForHarvest(jsErrorEvent, false, softNavAttrs); // disassociate any error that ultimately falls outside the final ixn span
285
- return this.#storeJserrorForHarvest(jsErrorEvent, true, softNavAttrs);
286
- });
287
- delete this.bufferedErrorsUnderSpa[interactionId]; // wipe the list of jserrors so they aren't duplicated by another call to the same id
288
- }
289
231
  }
290
232
  exports.Aggregate = Aggregate;
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.Aggregate = void 0;
7
7
  var _handle = require("../../../common/event-emitter/handle");
8
8
  var _registerHandler = require("../../../common/event-emitter/register-handler");
9
- var _invoke = require("../../../common/util/invoke");
10
9
  var _loadTime = require("../../../common/vitals/load-time");
11
10
  var _features = require("../../../loaders/features/features");
12
11
  var _aggregateBase = require("../../utils/aggregate-base");
@@ -193,25 +192,36 @@ class Aggregate extends _aggregateBase.AggregateBase {
193
192
  }
194
193
 
195
194
  /**
196
- * Decorate the passed-in params obj with properties relating to any associated interaction at the time of the timestamp.
197
- * @param {Object} params reference to the local var instance in Jserrors feature's storeError
198
- * @param {DOMHighResTimeStamp} timestamp time the jserror occurred
195
+ * Handles or redirects jserror event based on the interaction, if any, that it's tied to.
196
+ * @param {Array} jsErrorEvent the error event array from jserrors feature
199
197
  */
200
- #handleJserror(params, timestamp) {
198
+ #handleJserror(jsErrorEvent) {
199
+ const timestamp = jsErrorEvent[3].time; // in storeError fn, the newMetrics obj contains the time of the error
201
200
  const associatedInteraction = this.getInteractionFor(timestamp);
202
- if (!associatedInteraction) return; // do not need to decorate this jserror params
201
+ if (!associatedInteraction) {
202
+ // No interaction was happening when this error occurred, so give it back to jserrors feature for processing
203
+ return (0, _handle.handle)('returnJserror', [jsErrorEvent], undefined, _features.FEATURE_NAMES.jserrors, this.ee);
204
+ }
203
205
 
204
- // Whether the interaction is in-progress or already finished, the id will let jserror buffer it under its index, until it gets the next step instruction.
205
- params.browserInteractionId = associatedInteraction.id;
206
+ // Store the error info to be returned when interaction finishes
206
207
  if (associatedInteraction.status === _constants.INTERACTION_STATUS.FIN) {
207
- // This information cannot be relayed back via handle() that flushes buffered errs because this is being called by a jserror's handle() per se and before the err is buffered.
208
- params._softNavFinished = true; // instead, signal that this err can be processed right away without needing to be buffered aka wait for an in-progress ixn
209
- params._softNavAttributes = associatedInteraction.customAttributes;
208
+ // Interaction already finished, return immediately with the interaction ID and attributes
209
+ processJserror.call(this, jsErrorEvent, associatedInteraction);
210
210
  } else {
211
- // These callbacks may be added multiple times for an ixn, but just a single run will deal with all jserrors associated with the interaction.
212
- // As such, be cautious not to use the params object since that's tied to one specific jserror and won't affect the rest of them.
213
- associatedInteraction.on('finished', (0, _invoke.single)(() => (0, _handle.handle)('softNavFlush', [associatedInteraction.id, true, associatedInteraction.customAttributes, associatedInteraction.end], undefined, _features.FEATURE_NAMES.jserrors, this.ee)));
214
- associatedInteraction.on('cancelled', (0, _invoke.single)(() => (0, _handle.handle)('softNavFlush', [associatedInteraction.id, false, undefined], undefined, _features.FEATURE_NAMES.jserrors, this.ee))); // don't take custom attrs from cancelled ixns
211
+ // Interaction still in progress, wait for it to finish or be cancelled
212
+ associatedInteraction.on('finished', () => processJserror.call(this, jsErrorEvent, associatedInteraction));
213
+ associatedInteraction.on('cancelled', () => (0, _handle.handle)('returnJserror', [jsErrorEvent], undefined, _features.FEATURE_NAMES.jserrors, this.ee));
214
+ }
215
+ function processJserror(jsErrorEvent, parentInteraction) {
216
+ const finalEnd = parentInteraction.end;
217
+ if (timestamp > finalEnd) {
218
+ // check if error falls within the final interaction span, after any & all long task extension(s) are considered
219
+ return (0, _handle.handle)('returnJserror', [jsErrorEvent], undefined, _features.FEATURE_NAMES.jserrors, this.ee);
220
+ }
221
+ // Error is within the interaction span, return with the correct interaction ID set, plus any custom attributes from the interaction
222
+ const params = jsErrorEvent[2];
223
+ params.browserInteractionId = parentInteraction.id;
224
+ (0, _handle.handle)('returnJserror', [jsErrorEvent, parentInteraction.customAttributes], undefined, _features.FEATURE_NAMES.jserrors, this.ee);
215
225
  }
216
226
  }
217
227
  #registerApiHandlers() {
@@ -226,7 +236,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
226
236
  if (this.associatedInteraction?.trigger === _constants.IPL_TRIGGER_NAME) this.associatedInteraction = null; // the api get-interaction method cannot target IPL
227
237
  if (!this.associatedInteraction) {
228
238
  // This new api-driven interaction will be the target of any subsequent .interaction() call, until it is closed by EITHER .end() OR the regular url>dom change process.
229
- this.associatedInteraction = thisClass.interactionInProgress = new _interaction.Interaction(_constants.API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi);
239
+ this.associatedInteraction = thisClass.interactionInProgress = new _interaction.Interaction(_constants.API_TRIGGER_NAME, Math.floor(time), thisClass.latestRouteSetByApi);
230
240
  thisClass.domObserver.observe(document.body, {
231
241
  attributes: true,
232
242
  childList: true,
@@ -12,6 +12,11 @@ var _localStorage = require("../../common/storage/local-storage.js");
12
12
  var _constants = require("../../common/session/constants");
13
13
  var _info = require("../../common/config/info");
14
14
  var _attributeSize = require("../../common/util/attribute-size");
15
+ var _handle = require("../../common/event-emitter/handle");
16
+ var _constants2 = require("../../loaders/api/constants");
17
+ var _constants3 = require("../metrics/constants");
18
+ var _features = require("../../loaders/features/features");
19
+ var _sharedHandlers = require("../../loaders/api/sharedHandlers");
15
20
  /**
16
21
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
17
22
  * SPDX-License-Identifier: Apache-2.0
@@ -59,6 +64,11 @@ function setupAgentSession(agentRef) {
59
64
  (0, _registerHandler.registerHandler)('api-setUserId', (time, key, value) => {
60
65
  agentRef.runtime.session.syncCustomAttribute(key, value);
61
66
  }, 'session', sharedEE);
67
+ (0, _registerHandler.registerHandler)('api-setUserIdAndResetSession', value => {
68
+ agentRef.runtime.session.reset();
69
+ (0, _handle.handle)(_constants3.SUPPORTABILITY_METRIC_CHANNEL, ['API/' + _constants2.SET_USER_ID + '/resetSession/called'], undefined, _features.FEATURE_NAMES.metrics, sharedEE);
70
+ (0, _sharedHandlers.appendJsAttribute)(agentRef, 'enduser.id', value, _constants2.SET_USER_ID, true);
71
+ }, 'session', sharedEE);
62
72
  (0, _registerHandler.registerHandler)('api-consent', accept => {
63
73
  agentRef.runtime.session.write({
64
74
  consent: accept === undefined ? true : accept
@@ -143,7 +143,7 @@ class AggregateBase extends _featureBase.FeatureBase {
143
143
  (0, _drain.drain)(this.agentIdentifier, this.featureName);
144
144
  }
145
145
  preHarvestChecks(opts) {
146
- return !this.blocked;
146
+ return !this.blocked && !this.ee.aborted;
147
147
  }
148
148
 
149
149
  /**
@@ -102,8 +102,9 @@ class RegisteredEntity {
102
102
  * Adds a user-defined identifier string to subsequent events on the page for the registered taret.
103
103
  * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/setuserid/}
104
104
  * @param {string|null} value A string identifier for the end-user, useful for tying all browser events to specific users. The value parameter does not have to be unique. If IDs should be unique, the caller is responsible for that validation. Passing a null value unsets any existing user ID.
105
+ * @param {boolean} [resetSession=false] Optional param. Should not be used from a registered entity context. To reset a session when updating user id, must be initiated by the main agent.
105
106
  */
106
- setUserId(value) {
107
+ setUserId(value, resetSession = false) {
107
108
  /** this method will be overset once register is successful */
108
109
  (0, _console.warn)(35, 'setUserId');
109
110
  }
@@ -21,7 +21,7 @@ var _setApplicationVersion = require("./api/setApplicationVersion");
21
21
  var _start = require("./api/start");
22
22
  var _consent = require("./api/consent");
23
23
  /**
24
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
24
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
25
25
  * SPDX-License-Identifier: Apache-2.0
26
26
  */
27
27
 
@@ -70,7 +70,6 @@ class Agent extends _agentBase.AgentBase {
70
70
  // NR1 creates an index on the rum call, and if not seen for a few days, will remove the browser app!
71
71
  // Future work is being planned to evaluate removing this behavior from the backend, but for now we must ensure this call is made
72
72
  this.desiredFeatures.add(_instrument.Instrument);
73
- this.runSoftNavOverSpa = [...this.desiredFeatures].some(instr => instr.featureName === _features.FEATURE_NAMES.softNav);
74
73
  (0, _configure.configure)(this, options, options.loaderType || 'agent'); // add api, exposed, and other config properties
75
74
 
76
75
  /** assign base agent-level API definitions, feature-level APIs will be handled by the features to allow better code-splitting */
@@ -100,8 +99,10 @@ class Agent extends _agentBase.AgentBase {
100
99
  featuresToStart.sort((a, b) => _features.featurePriority[a.featureName] - _features.featurePriority[b.featureName]);
101
100
  featuresToStart.forEach(InstrumentCtor => {
102
101
  if (!enabledFeatures[InstrumentCtor.featureName] && InstrumentCtor.featureName !== _features.FEATURE_NAMES.pageViewEvent) return; // PVE is required to run even if it's marked disabled
103
- if (this.runSoftNavOverSpa && InstrumentCtor.featureName === _features.FEATURE_NAMES.spa) return;
104
- if (!this.runSoftNavOverSpa && InstrumentCtor.featureName === _features.FEATURE_NAMES.softNav) return;
102
+ if (InstrumentCtor.featureName === _features.FEATURE_NAMES.spa) {
103
+ (0, _console.warn)(67);
104
+ return; // Skip old SPA
105
+ }
105
106
  const dependencies = (0, _featureDependencies.getFeatureDependencyNames)(InstrumentCtor.featureName);
106
107
  const missingDependencies = dependencies.filter(featName => !(featName in this.features)); // any other feature(s) this depends on should've been initialized on prior iterations by priority order
107
108
  if (missingDependencies.length > 0) {
@@ -23,12 +23,12 @@ function setupInteractionAPI(agent) {
23
23
  function InteractionHandle() {}
24
24
  const InteractionApiProto = InteractionHandle.prototype = {
25
25
  createTracer: function (name, cb) {
26
+ // Primarily used by old SPA feature to create custom child tracers under an interaction.
27
+ // Soft navigations won't support Tracer nodes, but this fn should still work the same otherwise (e.g., run the orig cb).
26
28
  var contextStore = {};
27
29
  var ixn = this;
28
30
  var hasCb = typeof cb === 'function';
29
31
  (0, _handle.handle)(_constants.SUPPORTABILITY_METRIC_CHANNEL, ['API/createTracer/called'], undefined, _features.FEATURE_NAMES.metrics, agent.ee);
30
- // Soft navigations won't support Tracer nodes, but this fn should still work the same otherwise (e.g., run the orig cb).
31
- if (!agent.runSoftNavOverSpa) (0, _handle.handle)(_constants2.spaPrefix + 'tracer', [(0, _now.now)(), name, contextStore], ixn, _features.FEATURE_NAMES.spa, agent.ee);
32
32
  return function () {
33
33
  tracerEE.emit((hasCb ? '' : 'no-') + 'fn-start', [(0, _now.now)(), ixn, hasCb], contextStore);
34
34
  if (hasCb) {
@@ -48,11 +48,11 @@ function setupInteractionAPI(agent) {
48
48
  };
49
49
  ['actionText', 'setName', 'setAttribute', 'save', 'ignore', 'onEnd', 'getContext', 'end', 'get'].forEach(name => {
50
50
  _sharedHandlers.setupAPI.apply(this, [name, function () {
51
- (0, _handle.handle)(_constants2.spaPrefix + name, [(0, _now.now)(), ...arguments], this, agent.runSoftNavOverSpa ? _features.FEATURE_NAMES.softNav : _features.FEATURE_NAMES.spa, agent.ee);
51
+ (0, _handle.handle)(_constants2.spaPrefix + name, [performance.now(), ...arguments], this, _features.FEATURE_NAMES.softNav, agent.ee);
52
52
  return this;
53
53
  }, agent, InteractionApiProto]);
54
54
  });
55
55
  (0, _sharedHandlers.setupAPI)(_constants2.SET_CURRENT_ROUTE_NAME, function () {
56
- if (agent.runSoftNavOverSpa) (0, _handle.handle)(_constants2.spaPrefix + 'routeName', [performance.now(), ...arguments], undefined, _features.FEATURE_NAMES.softNav, agent.ee);else (0, _handle.handle)(_constants2.prefix + 'routeName', [(0, _now.now)(), ...arguments], this, _features.FEATURE_NAMES.spa, agent.ee);
56
+ (0, _handle.handle)(_constants2.spaPrefix + 'routeName', [performance.now(), ...arguments], undefined, _features.FEATURE_NAMES.softNav, agent.ee);
57
57
  }, agent);
58
58
  }
@@ -18,7 +18,7 @@ exports.default = void 0;
18
18
  * @property {(eventType: string, options?: {start: number, end: number, duration: number, customAttributes: object}) => ({start: number, end: number, duration: number, customAttributes: object})} measure - Measures a task that is recorded as a BrowserPerformance event.
19
19
  * @property {(value: string | null) => void} setApplicationVersion - Add an application.version attribute to all outgoing data for the registered entity.
20
20
  * @property {(name: string, value: string | number | boolean | null, persist?: boolean) => void} setCustomAttribute - Add a custom attribute to outgoing data for the registered entity.
21
- * @property {(value: string | null) => void} setUserId - Add an enduser.id attribute to all outgoing API data for the registered entity.
21
+ * @property {(value: string | null, resetSession?: boolean) => void} setUserId - Add an enduser.id attribute to all outgoing API data for the registered entity. Note: a registered entity will not be able to initiate a session reset. It must be done from the main agent.
22
22
  * @property {RegisterAPIMetadata} metadata - The metadata object containing the custom attributes and target information for the registered entity.
23
23
  */
24
24
  /**
@@ -7,6 +7,7 @@ exports.setupSetUserIdAPI = setupSetUserIdAPI;
7
7
  var _console = require("../../common/util/console");
8
8
  var _constants = require("./constants");
9
9
  var _sharedHandlers = require("./sharedHandlers");
10
+ var _handle = require("../../common/event-emitter/handle");
10
11
  /**
11
12
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
12
13
  * SPDX-License-Identifier: Apache-2.0
@@ -15,14 +16,23 @@ var _sharedHandlers = require("./sharedHandlers");
15
16
  function setupSetUserIdAPI(agent) {
16
17
  /**
17
18
  * Attach the 'enduser.id' attribute onto agent payloads. This may be used in NR queries to group all browser events by specific users.
18
- * @param {string} value - unique user identifier; a null user id suggests none should exist
19
+ * @param {string|null} value - unique user identifier; a null user id suggests none should exist
20
+ * @param {boolean} [resetSession=false] - Optional param. When true, resets the current session ONLY when changing user id from an existing value to another value or null. If the current user id is null when calling the API, the session cannot be reset.
19
21
  * @returns @see apiCall
20
22
  */
21
- (0, _sharedHandlers.setupAPI)(_constants.SET_USER_ID, function (value) {
23
+ (0, _sharedHandlers.setupAPI)(_constants.SET_USER_ID, function (value, resetSession = false) {
22
24
  if (!(typeof value === 'string' || value === null)) {
23
25
  (0, _console.warn)(41, typeof value);
24
26
  return;
25
27
  }
26
- return (0, _sharedHandlers.appendJsAttribute)(agent, 'enduser.id', value, _constants.SET_USER_ID, true);
28
+ const currUser = agent.info.jsAttributes['enduser.id'];
29
+
30
+ // reset session ONLY if we are updating the userid (from one value to another, or from a value to null/undefined)
31
+ const shouldAttemptReset = resetSession && currUser !== undefined && currUser !== null && currUser !== value;
32
+ if (shouldAttemptReset) {
33
+ (0, _handle.handle)(_constants.prefix + 'setUserIdAndResetSession', [value], undefined, 'session', agent.ee);
34
+ } else {
35
+ (0, _sharedHandlers.appendJsAttribute)(agent, 'enduser.id', value, _constants.SET_USER_ID, true);
36
+ }
27
37
  }, agent);
28
38
  }
@@ -89,9 +89,10 @@ class ApiBase {
89
89
  * Adds a user-defined identifier string to subsequent events on the page.
90
90
  * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/setuserid/}
91
91
  * @param {string|null} value A string identifier for the end-user, useful for tying all browser events to specific users. The value parameter does not have to be unique. If IDs should be unique, the caller is responsible for that validation. Passing a null value unsets any existing user ID.
92
+ * @param {boolean} [resetSession=false] Optional param. When true, resets the current session ONLY when changing user id from an existing value to another value or null. If the current user id is null when calling the API, the session cannot be reset.
92
93
  */
93
- setUserId(value) {
94
- return this.#callMethod(_constants.SET_USER_ID, value);
94
+ setUserId(value, resetSession = false) {
95
+ return this.#callMethod(_constants.SET_USER_ID, value, resetSession);
95
96
  }
96
97
 
97
98
  /**
@@ -11,13 +11,12 @@ var _instrument3 = require("../features/metrics/instrument");
11
11
  var _instrument4 = require("../features/jserrors/instrument");
12
12
  var _instrument5 = require("../features/ajax/instrument");
13
13
  var _instrument6 = require("../features/session_trace/instrument");
14
- var _instrument7 = require("../features/spa/instrument");
15
- var _instrument8 = require("../features/session_replay/instrument");
16
- var _instrument9 = require("../features/generic_events/instrument");
17
- var _instrument0 = require("../features/logging/instrument");
18
- var _instrument1 = require("../features/soft_navigations/instrument");
14
+ var _instrument7 = require("../features/session_replay/instrument");
15
+ var _instrument8 = require("../features/generic_events/instrument");
16
+ var _instrument9 = require("../features/logging/instrument");
17
+ var _instrument0 = require("../features/soft_navigations/instrument");
19
18
  /**
20
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
19
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
21
20
  * SPDX-License-Identifier: Apache-2.0
22
21
  */
23
22
 
@@ -32,7 +31,7 @@ class BrowserAgent extends _agent.Agent {
32
31
  constructor(options) {
33
32
  super({
34
33
  ...options,
35
- features: [_instrument5.Instrument, _instrument.Instrument, _instrument2.Instrument, _instrument6.Instrument, _instrument3.Instrument, _instrument4.Instrument, _instrument7.Instrument, _instrument1.Instrument, _instrument8.Instrument, _instrument9.Instrument, _instrument0.Instrument],
34
+ features: [_instrument5.Instrument, _instrument.Instrument, _instrument2.Instrument, _instrument6.Instrument, _instrument3.Instrument, _instrument4.Instrument, _instrument0.Instrument, _instrument7.Instrument, _instrument8.Instrument, _instrument9.Instrument],
36
35
  loaderType: 'browser-agent'
37
36
  });
38
37
  }
@@ -62,9 +62,6 @@ function configure(agent, opts = {}, loaderType, forceDrain) {
62
62
  agent.beacons = [...internalTrafficList];
63
63
  (0, _topLevelCallers.setTopLevelCallers)(agent); // no need to set global APIs on newrelic obj more than once
64
64
  (0, _nreum.addToNREUM)('activatedFeatures', _featureFlags.activatedFeatures);
65
-
66
- // Update if soft_navigations is allowed to run AND part of this agent build, used to override old spa functions.
67
- agent.runSoftNavOverSpa &&= updatedInit.soft_navigations.enabled === true && updatedInit.feature_flags.includes('soft_nav');
68
65
  }
69
66
  runtime.denyList = [...(updatedInit.ajax.deny_list || []), ...(updatedInit.ajax.block_internal ? internalTrafficList : [])];
70
67
  runtime.ptid = agent.agentIdentifier;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
 
@@ -16,11 +16,9 @@ import { Instrument as InstrumentXhr } from '../features/ajax/instrument';
16
16
  import { Instrument as InstrumentSessionTrace } from '../features/session_trace/instrument';
17
17
  import { Instrument as InstrumentSessionReplay } from '../features/session_replay/instrument';
18
18
  import { Instrument as InstrumentSoftNav } from '../features/soft_navigations/instrument';
19
- import { Instrument as InstrumentSpa } from '../features/spa/instrument';
20
19
  import { Instrument as InstrumentGenericEvents } from '../features/generic_events/instrument';
21
20
  import { Instrument as InstrumentLogs } from '../features/logging/instrument';
22
21
  new Agent({
23
- features: [InstrumentXhr, InstrumentPageViewEvent, InstrumentPageViewTiming, InstrumentSessionTrace, InstrumentSessionReplay, InstrumentMetrics, InstrumentErrors, InstrumentGenericEvents, InstrumentLogs, InstrumentSoftNav, InstrumentSpa // either the softnav or the old spa will be used (not both), but we still need to pack both to avoid dynamic import for instrument files
24
- ],
22
+ features: [InstrumentXhr, InstrumentPageViewEvent, InstrumentPageViewTiming, InstrumentSessionTrace, InstrumentSessionReplay, InstrumentMetrics, InstrumentErrors, InstrumentGenericEvents, InstrumentLogs, InstrumentSoftNav],
25
23
  loaderType: 'spa'
26
24
  });