@newrelic/browser-agent 1.293.0 → 1.294.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/common/aggregate/event-aggregator.js +3 -0
  3. package/dist/cjs/common/config/runtime.js +4 -0
  4. package/dist/cjs/common/constants/agent-constants.js +1 -1
  5. package/dist/cjs/common/constants/env.cdn.js +1 -1
  6. package/dist/cjs/common/constants/env.npm.js +1 -1
  7. package/dist/cjs/common/event-emitter/contextual-ee.js +3 -2
  8. package/dist/cjs/common/url/clean-url.js +2 -1
  9. package/dist/cjs/common/util/attribute-size.js +31 -0
  10. package/dist/cjs/features/ajax/aggregate/index.js +6 -0
  11. package/dist/cjs/features/generic_events/aggregate/index.js +1 -14
  12. package/dist/cjs/features/generic_events/constants.js +1 -3
  13. package/dist/cjs/features/logging/aggregate/index.js +2 -23
  14. package/dist/cjs/features/page_view_timing/aggregate/index.js +2 -0
  15. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +4 -4
  16. package/dist/cjs/features/soft_navigations/aggregate/bel-node.js +0 -4
  17. package/dist/cjs/features/soft_navigations/aggregate/index.js +12 -12
  18. package/dist/cjs/features/soft_navigations/aggregate/initial-page-load-interaction.js +3 -2
  19. package/dist/cjs/features/soft_navigations/aggregate/interaction.js +18 -11
  20. package/dist/cjs/features/spa/aggregate/interaction.js +1 -1
  21. package/dist/cjs/features/utils/agent-session.js +13 -5
  22. package/dist/cjs/features/utils/aggregate-base.js +25 -2
  23. package/dist/cjs/features/utils/event-buffer.js +12 -3
  24. package/dist/cjs/features/utils/event-store-manager.js +6 -6
  25. package/dist/esm/common/aggregate/event-aggregator.js +3 -0
  26. package/dist/esm/common/config/runtime.js +4 -0
  27. package/dist/esm/common/constants/agent-constants.js +1 -1
  28. package/dist/esm/common/constants/env.cdn.js +1 -1
  29. package/dist/esm/common/constants/env.npm.js +1 -1
  30. package/dist/esm/common/event-emitter/contextual-ee.js +3 -2
  31. package/dist/esm/common/url/clean-url.js +2 -1
  32. package/dist/esm/common/util/attribute-size.js +24 -0
  33. package/dist/esm/features/ajax/aggregate/index.js +6 -0
  34. package/dist/esm/features/generic_events/aggregate/index.js +1 -14
  35. package/dist/esm/features/generic_events/constants.js +0 -2
  36. package/dist/esm/features/logging/aggregate/index.js +2 -23
  37. package/dist/esm/features/page_view_timing/aggregate/index.js +2 -0
  38. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +4 -4
  39. package/dist/esm/features/soft_navigations/aggregate/bel-node.js +0 -4
  40. package/dist/esm/features/soft_navigations/aggregate/index.js +12 -12
  41. package/dist/esm/features/soft_navigations/aggregate/initial-page-load-interaction.js +3 -2
  42. package/dist/esm/features/soft_navigations/aggregate/interaction.js +18 -11
  43. package/dist/esm/features/spa/aggregate/interaction.js +1 -1
  44. package/dist/esm/features/utils/agent-session.js +13 -5
  45. package/dist/esm/features/utils/aggregate-base.js +25 -2
  46. package/dist/esm/features/utils/event-buffer.js +12 -3
  47. package/dist/esm/features/utils/event-store-manager.js +7 -7
  48. package/dist/tsconfig.tsbuildinfo +1 -1
  49. package/dist/types/common/aggregate/event-aggregator.d.ts +1 -0
  50. package/dist/types/common/aggregate/event-aggregator.d.ts.map +1 -1
  51. package/dist/types/common/config/runtime.d.ts.map +1 -1
  52. package/dist/types/common/constants/agent-constants.d.ts +1 -1
  53. package/dist/types/common/event-emitter/contextual-ee.d.ts.map +1 -1
  54. package/dist/types/common/url/clean-url.d.ts +2 -2
  55. package/dist/types/common/url/clean-url.d.ts.map +1 -1
  56. package/dist/types/common/util/attribute-size.d.ts +4 -0
  57. package/dist/types/common/util/attribute-size.d.ts.map +1 -0
  58. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  59. package/dist/types/features/generic_events/aggregate/index.d.ts +0 -1
  60. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  61. package/dist/types/features/generic_events/constants.d.ts +0 -2
  62. package/dist/types/features/generic_events/constants.d.ts.map +1 -1
  63. package/dist/types/features/logging/aggregate/index.d.ts.map +1 -1
  64. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  65. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +2 -2
  66. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  67. package/dist/types/features/soft_navigations/aggregate/bel-node.d.ts +0 -3
  68. package/dist/types/features/soft_navigations/aggregate/bel-node.d.ts.map +1 -1
  69. package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
  70. package/dist/types/features/soft_navigations/aggregate/initial-page-load-interaction.d.ts +0 -1
  71. package/dist/types/features/soft_navigations/aggregate/initial-page-load-interaction.d.ts.map +1 -1
  72. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +8 -2
  73. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
  74. package/dist/types/features/utils/agent-session.d.ts.map +1 -1
  75. package/dist/types/features/utils/aggregate-base.d.ts +10 -0
  76. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  77. package/dist/types/features/utils/event-buffer.d.ts +5 -2
  78. package/dist/types/features/utils/event-buffer.d.ts.map +1 -1
  79. package/dist/types/features/utils/event-store-manager.d.ts +3 -3
  80. package/dist/types/features/utils/event-store-manager.d.ts.map +1 -1
  81. package/package.json +3 -2
  82. package/src/common/aggregate/event-aggregator.js +4 -0
  83. package/src/common/config/runtime.js +2 -0
  84. package/src/common/constants/agent-constants.js +1 -1
  85. package/src/common/event-emitter/contextual-ee.js +3 -2
  86. package/src/common/url/clean-url.js +2 -1
  87. package/src/common/util/attribute-size.js +24 -0
  88. package/src/features/ajax/aggregate/index.js +7 -0
  89. package/src/features/generic_events/aggregate/index.js +1 -12
  90. package/src/features/generic_events/constants.js +0 -2
  91. package/src/features/logging/aggregate/index.js +3 -20
  92. package/src/features/page_view_timing/aggregate/index.js +3 -0
  93. package/src/features/soft_navigations/aggregate/ajax-node.js +4 -4
  94. package/src/features/soft_navigations/aggregate/bel-node.js +0 -5
  95. package/src/features/soft_navigations/aggregate/index.js +13 -10
  96. package/src/features/soft_navigations/aggregate/initial-page-load-interaction.js +3 -2
  97. package/src/features/soft_navigations/aggregate/interaction.js +14 -8
  98. package/src/features/spa/aggregate/interaction.js +1 -1
  99. package/src/features/utils/agent-session.js +13 -2
  100. package/src/features/utils/aggregate-base.js +22 -2
  101. package/src/features/utils/event-buffer.js +12 -3
  102. package/src/features/utils/event-store-manager.js +7 -7
package/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
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.294.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.293.0...v1.294.0) (2025-07-23)
7
+
8
+
9
+ ### Features
10
+
11
+ * Harvest early ([#1513](https://github.com/newrelic/newrelic-browser-agent/issues/1513)) ([d347eaa](https://github.com/newrelic/newrelic-browser-agent/commit/d347eaa76e26a9a4fc8e5c191de6a3c737712c68))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * report empty previousUrl as undefined ([#1526](https://github.com/newrelic/newrelic-browser-agent/issues/1526)) ([e3ca824](https://github.com/newrelic/newrelic-browser-agent/commit/e3ca824847e8f91a5da8cca8bf7717001c16929b))
17
+
6
18
  ## [1.293.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.292.1...v1.293.0) (2025-07-01)
7
19
 
8
20
 
@@ -17,6 +17,9 @@ var _aggregator = require("./aggregator");
17
17
  class EventAggregator {
18
18
  #aggregator = new _aggregator.Aggregator();
19
19
  #savedNamesToBuckets = {};
20
+ byteSize() {
21
+ return 0; // EventAggregator does not currently track byte size like EventBuffer does, but will in a future update. This is a placeholder to maintain interface consistency.
22
+ }
20
23
  isEmpty({
21
24
  aggregatorTypes
22
25
  }) {
@@ -42,6 +42,10 @@ const RuntimeModel = {
42
42
  releaseIds: {},
43
43
  session: undefined,
44
44
  timeKeeper: undefined,
45
+ /** a proxy is set in agent-session to track jsAttributes changes for harvesting mechanics */
46
+ jsAttributesMetadata: {
47
+ bytes: 0
48
+ },
45
49
  get harvestCount() {
46
50
  return ++_harvestCount;
47
51
  }
@@ -8,6 +8,6 @@ exports.MAX_PAYLOAD_SIZE = exports.IDEAL_PAYLOAD_SIZE = exports.DEFAULT_KEY = vo
8
8
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
9
9
  * SPDX-License-Identifier: Apache-2.0
10
10
  */
11
- const IDEAL_PAYLOAD_SIZE = exports.IDEAL_PAYLOAD_SIZE = 64000;
11
+ const IDEAL_PAYLOAD_SIZE = exports.IDEAL_PAYLOAD_SIZE = 16000;
12
12
  const MAX_PAYLOAD_SIZE = exports.MAX_PAYLOAD_SIZE = 1000000;
13
13
  const DEFAULT_KEY = exports.DEFAULT_KEY = 'NR_CONTAINER_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.293.0";
20
+ const VERSION = exports.VERSION = "1.294.0-rc.0";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.DIST_METHOD = exports.BUILD_EN
17
17
  /**
18
18
  * Exposes the version of the agent
19
19
  */
20
- const VERSION = exports.VERSION = "1.293.0";
20
+ const VERSION = exports.VERSION = "1.294.0-rc.0";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -87,12 +87,13 @@ function ee(old, debugId) {
87
87
  if (old && bubble) old.emit(type, args, contextOrStore);
88
88
  var ctx = context(contextOrStore);
89
89
  var handlersArray = listeners(type);
90
- var len = handlersArray.length;
91
90
 
92
91
  // Apply each handler function in the order they were added
93
92
  // to the context with the arguments
94
93
 
95
- for (var i = 0; i < len; i++) handlersArray[i].apply(ctx, args);
94
+ handlersArray.forEach(handler => {
95
+ handler.apply(ctx, args);
96
+ });
96
97
 
97
98
  // Buffer after emitting for consistent ordering
98
99
  var bufferGroup = getBuffer()[bufferGroupMap[type]];
@@ -14,10 +14,11 @@ var patternWithoutHash = /([^?#]*)().*/;
14
14
 
15
15
  /**
16
16
  * Cleans a URL by removing the query string and fragment (hash portion).
17
- * @param {string} url - The original URL to be cleaned.
17
+ * @param {string} [url] - The original URL to be cleaned.
18
18
  * @param {boolean} [keepHash=false] - Whether to preserve the hash portion of the URL.
19
19
  * @returns {string} The cleaned URL.
20
20
  */
21
21
  function cleanURL(url, keepHash) {
22
+ if (!url) return url;
22
23
  return url.replace(keepHash ? patternWithHash : patternWithoutHash, '$1$2');
23
24
  }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.trackObjectAttributeSize = trackObjectAttributeSize;
7
+ var _stringify = require("./stringify");
8
+ /**
9
+ * Copyright 2020-2025 New Relic, Inc. All rights reserved.
10
+ * SPDX-License-Identifier: Apache-2.0
11
+ */
12
+
13
+ function trackObjectAttributeSize(parent, object) {
14
+ const originalAttribute = parent[object] ??= {};
15
+ const output = {
16
+ bytes: Object.keys(originalAttribute).reduce((acc, key) => acc + key.length + (0, _stringify.stringify)(originalAttribute[key]).length, 0)
17
+ };
18
+ // proxy attribute to calculate its size when changed
19
+ parent[object] = new Proxy(originalAttribute, {
20
+ set(target, prop, value) {
21
+ output.bytes += prop.length + (0, _stringify.stringify)(value).length;
22
+ target[prop] = value;
23
+ return true;
24
+ },
25
+ deleteProperty(target, prop) {
26
+ output.bytes -= prop.length + (0, _stringify.stringify)(target[prop]).length;
27
+ return delete target[prop];
28
+ }
29
+ });
30
+ return output;
31
+ }
@@ -25,6 +25,12 @@ class Aggregate extends _aggregateBase.AggregateBase {
25
25
  (0, _denyList.setDenyList)(agentRef.runtime.denyList);
26
26
  this.underSpaEvents = {};
27
27
  const classThis = this;
28
+ if (!agentRef.init.ajax.block_internal) {
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.
30
+ super.canHarvestEarly = false;
31
+ } else {
32
+ super.customAttributesAreSeparate = true;
33
+ }
28
34
 
29
35
  // --- v Used by old spa feature
30
36
  this.ee.on('interactionDone', (interaction, wasSaved) => {
@@ -12,7 +12,6 @@ var _aggregateBase = require("../../utils/aggregate-base");
12
12
  var _console = require("../../../common/util/console");
13
13
  var _now = require("../../../common/timing/now");
14
14
  var _registerHandler = require("../../../common/event-emitter/register-handler");
15
- var _constants2 = require("../../metrics/constants");
16
15
  var _traverse = require("../../../common/util/traverse");
17
16
  var _userActionsAggregator = require("./user-actions/user-actions-aggregator");
18
17
  var _iframe = require("../../../common/dom/iframe");
@@ -26,7 +25,6 @@ class Aggregate extends _aggregateBase.AggregateBase {
26
25
  static featureName = _constants.FEATURE_NAME;
27
26
  constructor(agentRef) {
28
27
  super(agentRef, _constants.FEATURE_NAME);
29
- this.eventsPerHarvest = 1000;
30
28
  this.referrerUrl = _runtime.isBrowserScope && document.referrer ? (0, _cleanUrl.cleanURL)(document.referrer) : undefined;
31
29
  this.waitForFlags(['ins']).then(([ins]) => {
32
30
  if (!ins) {
@@ -284,18 +282,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
284
282
  /** Event-specific attributes take precedence over agent-level custom attributes and fallbacks */
285
283
  ...obj
286
284
  };
287
- const addedEvent = this.events.add(eventAttributes, targetEntityGuid);
288
- if (!addedEvent && !this.events.isEmpty(undefined, targetEntityGuid)) {
289
- /** could not add the event because it pushed the buffer over the limit
290
- * so we harvest early, and try to add it again now that the buffer is cleared
291
- * if it fails again, we do nothing
292
- */
293
- this.ee.emit(_constants2.SUPPORTABILITY_METRIC_CHANNEL, ['GenericEvents/Harvest/Max/Seen']);
294
- this.agentRef.runtime.harvester.triggerHarvestFor(this, {
295
- targetEntityGuid
296
- });
297
- this.events.add(eventAttributes);
298
- }
285
+ this.events.add(eventAttributes, targetEntityGuid);
299
286
  }
300
287
  serializer(eventBuffer) {
301
288
  return (0, _traverse.applyFnToProps)({
@@ -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.MAX_PAYLOAD_SIZE = exports.IDEAL_PAYLOAD_SIZE = 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.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.
@@ -11,8 +11,6 @@ var _features = require("../../loaders/features/features");
11
11
  */
12
12
 
13
13
  const FEATURE_NAME = exports.FEATURE_NAME = _features.FEATURE_NAMES.genericEvents;
14
- const IDEAL_PAYLOAD_SIZE = exports.IDEAL_PAYLOAD_SIZE = 64000;
15
- const MAX_PAYLOAD_SIZE = exports.MAX_PAYLOAD_SIZE = 1000000;
16
14
  const OBSERVED_EVENTS = exports.OBSERVED_EVENTS = ['auxclick', 'click', 'copy', 'keydown', 'paste', 'scrollend'];
17
15
  const OBSERVED_WINDOW_EVENTS = exports.OBSERVED_WINDOW_EVENTS = ['focus', 'blur'];
18
16
  const RAGE_CLICK_THRESHOLD_EVENTS = exports.RAGE_CLICK_THRESHOLD_EVENTS = 4;
@@ -12,7 +12,6 @@ var _constants = require("../constants");
12
12
  var _log = require("../shared/log");
13
13
  var _utils = require("../shared/utils");
14
14
  var _traverse = require("../../../common/util/traverse");
15
- var _agentConstants = require("../../../common/constants/agent-constants");
16
15
  var _target = require("../../../common/util/target");
17
16
  var _constants2 = require("../../../common/session/constants");
18
17
  var _constants3 = require("../../session_replay/constants");
@@ -27,6 +26,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
27
26
  constructor(agentRef) {
28
27
  super(agentRef, _constants.FEATURE_NAME);
29
28
  this.isSessionTrackingEnabled = (0, _featureGates.canEnableSessionTracking)(agentRef.init) && agentRef.runtime.session;
29
+ super.customAttributesAreSeparate = true;
30
30
 
31
31
  // The SessionEntity class can emit a message indicating the session was cleared and reset (expiry, inactivity). This feature must abort and never resume if that occurs.
32
32
  this.ee.on(_constants2.SESSION_EVENTS.RESET, () => {
@@ -90,28 +90,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
90
90
  }
91
91
  if (typeof message !== 'string' || !message) return (0, _console.warn)(32);
92
92
  const log = new _log.Log(Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp)), message, attributes, level);
93
- const logBytes = log.message.length + (0, _stringify.stringify)(log.attributes).length + log.level.length + 10; // timestamp == 10 chars
94
-
95
- const failToHarvestMessage = 'Logging/Harvest/Failed/Seen';
96
- if (logBytes > _agentConstants.MAX_PAYLOAD_SIZE) {
97
- // cannot possibly send this, even with an empty buffer
98
- this.reportSupportabilityMetric(failToHarvestMessage, logBytes);
99
- (0, _console.warn)(31, log.message.slice(0, 25) + '...');
100
- return;
101
- }
102
- if (this.events.wouldExceedMaxSize(logBytes, targetEntityGuid)) {
103
- this.reportSupportabilityMetric('Logging/Harvest/Early/Seen', this.events.byteSize() + logBytes);
104
- this.agentRef.runtime.harvester.triggerHarvestFor(this, {
105
- targetEntityGuid
106
- }); // force a harvest synchronously to try adding again
107
- }
108
- if (!this.events.add(log, targetEntityGuid)) {
109
- // still failed after a harvest attempt despite not being too large would mean harvest failed with options.retry
110
- this.reportSupportabilityMetric(failToHarvestMessage, logBytes);
111
- (0, _console.warn)(31, log.message.slice(0, 25) + '...');
112
- } else {
113
- this.reportSupportabilityMetric('Logging/Event/Added/Seen');
114
- }
93
+ this.events.add(log, targetEntityGuid);
115
94
  }
116
95
  serializer(eventBuffer, targetEntityGuid) {
117
96
  const target = this.agentRef.runtime.entityManager.get(targetEntityGuid);
@@ -38,6 +38,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
38
38
  super(agentRef, _constants.FEATURE_NAME);
39
39
  this.curSessEndRecorded = false;
40
40
  this.firstIxnRecorded = false;
41
+ super.customAttributesAreSeparate = true;
41
42
  (0, _registerHandler.registerHandler)('docHidden', msTimestamp => this.endCurrentSession(msTimestamp), this.featureName, this.ee);
42
43
  // Add the time of _window pagehide event_ firing to the next PVT harvest == NRDB windowUnload attr:
43
44
  (0, _registerHandler.registerHandler)('winPagehide', msTimestamp => this.addTiming('unload', msTimestamp, null), this.featureName, this.ee);
@@ -139,6 +140,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
139
140
 
140
141
  // serialize array of timing data
141
142
  serializer(eventBuffer) {
143
+ if (!eventBuffer?.length) return '';
142
144
  var addString = (0, _belSerializer.getAddStringContext)(this.agentRef.runtime.obfuscator);
143
145
  var payload = 'bel.6;';
144
146
  for (var i = 0; i < eventBuffer.length; i++) {
@@ -13,8 +13,8 @@ var _belNode = require("./bel-node");
13
13
  */
14
14
 
15
15
  class AjaxNode extends _belNode.BelNode {
16
- constructor(agentRef, ajaxEvent) {
17
- super(agentRef);
16
+ constructor(ajaxEvent) {
17
+ super();
18
18
  this.belType = _constants.NODE_TYPE.AJAX;
19
19
  this.method = ajaxEvent.method;
20
20
  this.status = ajaxEvent.status;
@@ -30,8 +30,8 @@ class AjaxNode extends _belNode.BelNode {
30
30
  this.start = ajaxEvent.startTime; // 5000 --- 5500 --> 10500
31
31
  this.end = ajaxEvent.endTime;
32
32
  }
33
- serialize(parentStartTimestamp) {
34
- const addString = (0, _belSerializer.getAddStringContext)(this.obfuscator);
33
+ serialize(parentStartTimestamp, agentRef) {
34
+ const addString = (0, _belSerializer.getAddStringContext)(agentRef.runtime.obfuscator);
35
35
  const nodeList = [];
36
36
 
37
37
  // IMPORTANT: The order in which addString is called matters and correlates to the order in which string shows up in the harvest payload. Do not re-order the following code.
@@ -18,10 +18,6 @@ class BelNode {
18
18
  callbackEnd = 0;
19
19
  callbackDuration = 0;
20
20
  nodeId = ++nodesSeen;
21
- constructor(agentRef) {
22
- this.obfuscator = agentRef.runtime.obfuscator;
23
- this.info = agentRef.info;
24
- }
25
21
  addChild(child) {
26
22
  this.children.push(child);
27
23
  }
@@ -25,6 +25,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
25
25
  domObserver
26
26
  }) {
27
27
  super(agentRef, _constants.FEATURE_NAME);
28
+ super.customAttributesAreSeparate = true;
28
29
  this.interactionsToHarvest = this.events;
29
30
  this.domObserver = domObserver;
30
31
  this.initialPageLoadInteraction = new _initialPageLoadInteraction.InitialPageLoadInteraction(agentRef);
@@ -33,7 +34,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
33
34
  if (agentRef.runtime.session?.isNew) this.initialPageLoadInteraction.customAttributes.isFirstOfSession = true; // mark the hard page load as first of its session
34
35
  this.initialPageLoadInteraction.forceSave = true; // unless forcibly ignored, iPL always finish by default
35
36
  const ixn = this.initialPageLoadInteraction;
36
- this.interactionsToHarvest.add(ixn);
37
+ this.events.add(ixn); // add the iPL ixn to the buffer for harvest
37
38
  this.initialPageLoadInteraction = null;
38
39
  });
39
40
  _timeToFirstByte.timeToFirstByte.subscribe(({
@@ -80,7 +81,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
80
81
  let firstIxnStartTime;
81
82
  const serializedIxnList = [];
82
83
  for (const interaction of eventBuffer) {
83
- serializedIxnList.push(interaction.serialize(firstIxnStartTime));
84
+ serializedIxnList.push(interaction.serialize(firstIxnStartTime, this.agentRef));
84
85
  if (firstIxnStartTime === undefined) firstIxnStartTime = Math.floor(interaction.start); // careful not to match or overwrite on 0 value!
85
86
  }
86
87
  return "bel.7;".concat(serializedIxnList.join(';'));
@@ -91,7 +92,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
91
92
  if (this.interactionInProgress?.done() === false) return; // current in-progress is blocked from closing, e.g. by 'waitForEnd' api option
92
93
 
93
94
  const oldURL = eventName === _constants.INTERACTION_TRIGGERS[3] ? this.latestHistoryUrl : undefined; // see related comment in 'newURL' handler above, 'popstate'
94
- this.interactionInProgress = new _interaction.Interaction(this.agentRef, eventName, startedAt, this.latestRouteSetByApi, oldURL);
95
+ this.interactionInProgress = new _interaction.Interaction(eventName, startedAt, this.latestRouteSetByApi, oldURL);
95
96
  if (eventName === _constants.INTERACTION_TRIGGERS[0]) {
96
97
  // 'click'
97
98
  const sourceElemText = getActionText(sourceElem);
@@ -107,7 +108,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
107
108
  setClosureHandlers() {
108
109
  this.interactionInProgress.on('finished', () => {
109
110
  const ref = this.interactionInProgress;
110
- this.interactionsToHarvest.add(this.interactionInProgress);
111
+ this.events.add(this.interactionInProgress); // add the ixn to the buffer for harvest
111
112
  this.interactionInProgress = null;
112
113
  this.domObserver.disconnect(); // can stop observing whenever our interaction logic completes a cycle
113
114
 
@@ -134,9 +135,8 @@ class Aggregate extends _aggregateBase.AggregateBase {
134
135
  */
135
136
  if (this.interactionInProgress?.isActiveDuring(timestamp)) return this.interactionInProgress;
136
137
  let saveIxn;
137
- const [{
138
- data: interactionsBuffer
139
- }] = this.interactionsToHarvest.get();
138
+ const interactionsBuffer = this.interactionsToHarvest.get()?.[0]?.data;
139
+ if (!interactionsBuffer) return undefined; // no interactions have been staged yet, so nothing to search through)
140
140
  for (let idx = interactionsBuffer.length - 1; idx >= 0; idx--) {
141
141
  // reverse search for the latest completed interaction for efficiency
142
142
  const finishedInteraction = interactionsBuffer[idx];
@@ -161,15 +161,15 @@ class Aggregate extends _aggregateBase.AggregateBase {
161
161
  // no interaction was happening when this ajax started, so give it back to Ajax feature for processing
162
162
  (0, _handle.handle)('returnAjax', [event], undefined, _features.FEATURE_NAMES.ajax, this.ee);
163
163
  } else {
164
- if (associatedInteraction.status === _constants.INTERACTION_STATUS.FIN) processAjax(this.agentRef, event, associatedInteraction); // tack ajax onto the ixn object awaiting harvest
164
+ if (associatedInteraction.status === _constants.INTERACTION_STATUS.FIN) processAjax(event, associatedInteraction); // tack ajax onto the ixn object awaiting harvest
165
165
  else {
166
166
  // same thing as above, just at a later time -- if the interaction in progress is cancelled, just send the event back to ajax feat unmodified
167
- associatedInteraction.on('finished', () => processAjax(this.agentRef, event, associatedInteraction));
167
+ associatedInteraction.on('finished', () => processAjax(event, associatedInteraction));
168
168
  associatedInteraction.on('cancelled', () => (0, _handle.handle)('returnAjax', [event], undefined, _features.FEATURE_NAMES.ajax, this.ee));
169
169
  }
170
170
  }
171
- function processAjax(agent, event, parentInteraction) {
172
- const newNode = new _ajaxNode.AjaxNode(agent, event);
171
+ function processAjax(event, parentInteraction) {
172
+ const newNode = new _ajaxNode.AjaxNode(event);
173
173
  parentInteraction.addChild(newNode);
174
174
  }
175
175
  }
@@ -208,7 +208,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
208
208
  if (this.associatedInteraction?.trigger === _constants.IPL_TRIGGER_NAME) this.associatedInteraction = null; // the api get-interaction method cannot target IPL
209
209
  if (!this.associatedInteraction) {
210
210
  // This new api-driven interaction will be the target of any subsequent .interaction() call, until it is closed by EITHER .end() OR the regular seenHistoryAndDomChange process.
211
- this.associatedInteraction = thisClass.interactionInProgress = new _interaction.Interaction(thisClass.agentRef, _constants.API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi);
211
+ this.associatedInteraction = thisClass.interactionInProgress = new _interaction.Interaction(_constants.API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi);
212
212
  thisClass.domObserver.observe(document.body, {
213
213
  attributes: true,
214
214
  childList: true,
@@ -17,10 +17,11 @@ var _constants = require("../constants");
17
17
 
18
18
  class InitialPageLoadInteraction extends _interaction.Interaction {
19
19
  constructor(agentRef) {
20
- super(agentRef, _constants.IPL_TRIGGER_NAME, 0, null);
20
+ super(_constants.IPL_TRIGGER_NAME, 0, null);
21
21
  this.queueTime = agentRef.info.queueTime;
22
22
  this.appTime = agentRef.info.applicationTime;
23
- this.oldURL = document.referrer;
23
+ /** @type {string|undefined} we assign as undefined if no referrer value is available so that URL grouping is not applied to an empty string at ingest */
24
+ this.oldURL = document.referrer || undefined;
24
25
  }
25
26
  get firstPaint() {
26
27
  return _firstPaint.firstPaint.current.value;
@@ -36,8 +36,8 @@ class Interaction extends _belNode.BelNode {
36
36
  keepOpenUntilEndApi = false;
37
37
  onDone = [];
38
38
  cancellationTimer;
39
- constructor(agentRef, uiEvent, uiEventTimestamp, currentRouteKnown, currentUrl) {
40
- super(agentRef);
39
+ constructor(uiEvent, uiEventTimestamp, currentRouteKnown, currentUrl) {
40
+ super();
41
41
  this.belType = _constants.NODE_TYPE.INTERACTION;
42
42
  this.trigger = uiEvent;
43
43
  this.start = uiEventTimestamp;
@@ -78,10 +78,6 @@ class Interaction extends _belNode.BelNode {
78
78
  #finish(customEndTime = 0) {
79
79
  clearTimeout(this.cancellationTimer);
80
80
  this.end = Math.max(this.domTimestamp, this.historyTimestamp, customEndTime);
81
- this.customAttributes = {
82
- ...this.info.jsAttributes,
83
- ...this.customAttributes
84
- }; // attrs specific to this interaction should have precedence over the general custom attrs
85
81
  this.status = _constants.INTERACTION_STATUS.FIN;
86
82
 
87
83
  // Run all the callbacks awaiting this interaction to finish.
@@ -112,9 +108,16 @@ class Interaction extends _belNode.BelNode {
112
108
  get firstPaint() {}
113
109
  get firstContentfulPaint() {}
114
110
  get navTiming() {}
115
- serialize(firstStartTimeOfPayload) {
111
+
112
+ /**
113
+ * Serializes (BEL) the interaction data for transmission.
114
+ * @param {Number} firstStartTimeOfPayload timestamp
115
+ * @param {Agent} agentRef Pass in the agent reference directly so that the event itself doesnt need to store the pointers and ruin the evaluation of the event size by including unused object references.
116
+ * @returns {String} A string that is the serialized representation of this interaction.
117
+ */
118
+ serialize(firstStartTimeOfPayload, agentRef) {
116
119
  const isFirstIxnOfPayload = firstStartTimeOfPayload === undefined;
117
- const addString = (0, _belSerializer.getAddStringContext)(this.obfuscator);
120
+ const addString = (0, _belSerializer.getAddStringContext)(agentRef.runtime.obfuscator);
118
121
  const nodeList = [];
119
122
  let ixnType;
120
123
  if (this.trigger === _constants.IPL_TRIGGER_NAME) ixnType = _constants.INTERACTION_TYPE.INITIAL_PAGE_LOAD;else if (this.newURL !== this.oldURL) ixnType = _constants.INTERACTION_TYPE.ROUTE_CHANGE;else ixnType = _constants.INTERACTION_TYPE.UNSPECIFIED;
@@ -131,12 +134,16 @@ class Interaction extends _belNode.BelNode {
131
134
  (0, _belSerializer.numeric)(this.callbackDuration),
132
135
  // not relative
133
136
  addString(this.trigger), addString((0, _cleanUrl.cleanURL)(this.initialPageURL, true)), addString((0, _cleanUrl.cleanURL)(this.oldURL, true)), addString((0, _cleanUrl.cleanURL)(this.newURL, true)), addString(this.customName), ixnType, (0, _belSerializer.nullable)(this.queueTime, _belSerializer.numeric, true) + (0, _belSerializer.nullable)(this.appTime, _belSerializer.numeric, true) + (0, _belSerializer.nullable)(this.oldRoute, addString, true) + (0, _belSerializer.nullable)(this.newRoute, addString, true) + addString(this.id), addString(this.nodeId), (0, _belSerializer.nullable)(this.firstPaint, _belSerializer.numeric, true) + (0, _belSerializer.nullable)(this.firstContentfulPaint, _belSerializer.numeric)];
134
- const allAttachedNodes = (0, _belSerializer.addCustomAttributes)(this.customAttributes || {}, addString); // start with all custom attributes
135
- if (this.info.atts) allAttachedNodes.push('a,' + addString(this.info.atts)); // add apm provided attributes
137
+ const customAttributes = {
138
+ ...agentRef.info.jsAttributes,
139
+ ...this.customAttributes
140
+ }; // attrs specific to this interaction should have precedence over the general custom attrs
141
+ const allAttachedNodes = (0, _belSerializer.addCustomAttributes)(customAttributes || {}, addString); // start with all custom attributes
142
+ if (agentRef.info.atts) allAttachedNodes.push('a,' + addString(agentRef.info.atts)); // add apm provided attributes
136
143
  /* Querypack encoder+decoder quirkiness:
137
144
  - If first ixn node of payload is being processed, its children's start time must be offset by this node's start. (firstStartTime should be undefined.)
138
145
  - Else for subsequent ixns in the same payload, we go back to using that first ixn node's start to offset their children's start. */
139
- this.children.forEach(node => allAttachedNodes.push(node.serialize(isFirstIxnOfPayload ? this.start : firstStartTimeOfPayload))); // recursively add the serialized string of every child of this (ixn) bel node
146
+ this.children.forEach(node => allAttachedNodes.push(node.serialize(isFirstIxnOfPayload ? this.start : firstStartTimeOfPayload, agentRef))); // recursively add the serialized string of every child of this (ixn) bel node
140
147
 
141
148
  fields[1] = (0, _belSerializer.numeric)(allAttachedNodes.length);
142
149
  nodeList.push(fields);
@@ -34,7 +34,7 @@ function Interaction(eventName, timestamp, url, routeName, onFinished, agentRef)
34
34
  attrs.initialPageURL = _runtime.initialLocation;
35
35
  attrs.oldRoute = routeName;
36
36
  attrs.newURL = url;
37
- attrs.oldURL = eventName === 'initialPageLoad' ? document.referrer : url;
37
+ attrs.oldURL = eventName === 'initialPageLoad' ? document.referrer || undefined : url; // document referrer can return '' and flipper url grouping gets weird with empty strings. Pass undefined to skip flipper.
38
38
  attrs.custom = {};
39
39
  attrs.store = {};
40
40
  }
@@ -10,6 +10,8 @@ var _registerHandler = require("../../common/event-emitter/register-handler");
10
10
  var _sessionEntity = require("../../common/session/session-entity");
11
11
  var _localStorage = require("../../common/storage/local-storage.js");
12
12
  var _constants = require("../../common/session/constants");
13
+ var _info = require("../../common/config/info");
14
+ var _attributeSize = require("../../common/util/attribute-size");
13
15
  /**
14
16
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
15
17
  * SPDX-License-Identifier: Apache-2.0
@@ -29,13 +31,19 @@ function setupAgentSession(agentRef) {
29
31
 
30
32
  // Retrieve & re-add all of the persisted setCustomAttribute|setUserId k-v from previous page load(s), if any was stored.
31
33
  const customSessionData = agentRef.runtime.session.state.custom;
32
- if (customSessionData) {
34
+ if (customSessionData && Object.keys(customSessionData).length) {
33
35
  /** stored attributes from previous page should not take precedence over attributes stored on this page via API before the page load */
34
- agentRef.info.jsAttributes = {
35
- ...customSessionData,
36
- ...agentRef.info.jsAttributes
37
- };
36
+ agentRef.info = (0, _info.mergeInfo)({
37
+ ...agentRef.info,
38
+ jsAttributes: {
39
+ ...customSessionData,
40
+ ...agentRef.info.jsAttributes
41
+ }
42
+ });
38
43
  }
44
+
45
+ /** track changes to the jsAttributes field over time for aiding with harvest mechanics */
46
+ agentRef.runtime.jsAttributesMetadata = (0, _attributeSize.trackObjectAttributeSize)(agentRef.info, 'jsAttributes');
39
47
  const sharedEE = _contextualEe.ee.get(agentRef.agentIdentifier);
40
48
 
41
49
  // any calls to newrelic.setCustomAttribute(<persisted>) will need to be added to:
@@ -20,6 +20,7 @@ var _eventBuffer = require("./event-buffer");
20
20
  var _handle = require("../../common/event-emitter/handle");
21
21
  var _constants = require("../metrics/constants");
22
22
  var _eventAggregator = require("../../common/aggregate/event-aggregator");
23
+ var _agentConstants = require("../../common/constants/agent-constants");
23
24
  /**
24
25
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
25
26
  * SPDX-License-Identifier: Apache-2.0
@@ -36,6 +37,12 @@ class AggregateBase extends _featureBase.FeatureBase {
36
37
  this.agentRef = agentRef;
37
38
  this.checkConfiguration(agentRef);
38
39
  this.doOnceForAllAggregate(agentRef);
40
+
41
+ /** @type {Boolean} indicates if custom attributes are combined in each event payload for size estimation purposes. this is set to true in derived classes that need to evaluate custom attributes separately from the event payload */
42
+ this.customAttributesAreSeparate = false;
43
+ /** @type {Boolean} indicates if the feature can harvest early. This is set to false in derived classes that need to block early harvests, like ajax under certain conditions */
44
+ this.canHarvestEarly = true; // this is set to false in derived classes that need to block early harvests, like ajax under certain conditions
45
+
39
46
  this.harvestOpts = {}; // features aggregate classes can define custom opts for when their harvest is called
40
47
 
41
48
  const agentEntityGuid = this.agentRef?.runtime?.appMetadata?.agents?.[0]?.entityGuid;
@@ -65,17 +72,33 @@ class AggregateBase extends _featureBase.FeatureBase {
65
72
  // Jserror and Metric features uses a singleton EventAggregator instead of a regular EventBuffer.
66
73
  case _features.FEATURE_NAMES.jserrors:
67
74
  case _features.FEATURE_NAMES.metrics:
68
- this.events = this.agentRef.sharedAggregator ??= new _eventStoreManager.EventStoreManager(this.agentRef, _eventAggregator.EventAggregator, entityGuid, 'shared_aggregator');
75
+ this.events = this.agentRef.sharedAggregator ??= new _eventStoreManager.EventStoreManager(this.agentRef, _eventAggregator.EventAggregator, entityGuid, {
76
+ featureName: 'shared_aggregator'
77
+ });
69
78
  break;
70
79
  /** All other features get EventBuffer in the ESM by default. Note: PVE is included here, but event buffer will always be empty so future harvests will still not happen by interval or EOL.
71
80
  This was necessary to prevent race cond. issues where the event buffer was checked before the feature could "block" itself.
72
81
  Its easier to just keep an empty event buffer in place. */
73
82
  default:
74
- this.events = new _eventStoreManager.EventStoreManager(this.agentRef, _eventBuffer.EventBuffer, entityGuid, this.featureName);
83
+ this.events = new _eventStoreManager.EventStoreManager(this.agentRef, _eventBuffer.EventBuffer, entityGuid, this);
75
84
  break;
76
85
  }
77
86
  }
78
87
 
88
+ /**
89
+ * Evaluates whether a harvest should be made early by estimating the size of the current payload. Currently, this only happens if the event storage is EventBuffer, since that triggers this method directly.
90
+ * If conditions are met, a new harvest will be triggered immediately.
91
+ * @returns void
92
+ */
93
+ decideEarlyHarvest() {
94
+ if (!this.canHarvestEarly) return;
95
+ const estimatedSize = this.events.byteSize() + (this.customAttributesAreSeparate ? this.agentRef.runtime.jsAttributesMetadata.bytes : 0);
96
+ if (estimatedSize > _agentConstants.IDEAL_PAYLOAD_SIZE) {
97
+ this.agentRef.runtime.harvester.triggerHarvestFor(this);
98
+ this.reportSupportabilityMetric("".concat(this.featureName, "/Harvest/Early/Seen"), estimatedSize);
99
+ }
100
+ }
101
+
79
102
  /**
80
103
  * New handler for waiting for multiple flags. Useful when expecting multiple flags simultaneously (ex. stn vs sr)
81
104
  * @param {string[]} flagNames
@@ -18,10 +18,13 @@ class EventBuffer {
18
18
  #rawBytesBackup;
19
19
 
20
20
  /**
21
- * @param {number} maxPayloadSize
21
+ * Creates an event buffer that can hold feature-processed events.
22
+ * @param {Number} maxPayloadSize The maximum size of the payload that can be stored in this buffer.
23
+ * @param {Object} [featureAgg] - the feature aggregate instance
22
24
  */
23
- constructor(maxPayloadSize = _agentConstants.MAX_PAYLOAD_SIZE) {
25
+ constructor(maxPayloadSize = _agentConstants.MAX_PAYLOAD_SIZE, featureAgg) {
24
26
  this.maxPayloadSize = maxPayloadSize;
27
+ this.featureAgg = featureAgg;
25
28
  }
26
29
  isEmpty() {
27
30
  return this.#buffer.length === 0;
@@ -43,9 +46,15 @@ class EventBuffer {
43
46
  */
44
47
  add(event) {
45
48
  const addSize = (0, _stringify.stringify)(event)?.length || 0; // (estimate) # of bytes a directly stringified event it would take to send
46
- if (this.#rawBytes + addSize > this.maxPayloadSize) return false;
49
+ if (this.#rawBytes + addSize > this.maxPayloadSize) {
50
+ const smTag = inject => "EventBuffer/".concat(inject, "/Dropped/Bytes");
51
+ this.featureAgg?.reportSupportabilityMetric(smTag(this.featureAgg.featureName), addSize); // bytes dropped for this feature will aggregate with this metric tag
52
+ this.featureAgg?.reportSupportabilityMetric(smTag('Combined'), addSize); // all bytes dropped across all features will aggregate with this metric tag
53
+ return false;
54
+ }
47
55
  this.#buffer.push(event);
48
56
  this.#rawBytes += addSize;
57
+ this.featureAgg?.decideEarlyHarvest(); // check if we should harvest early with new data
49
58
  return true;
50
59
  }
51
60
 
@@ -22,14 +22,14 @@ class EventStoreManager {
22
22
  * @param {object} agentRef - reference to base agent class
23
23
  * @param {EventBuffer|EventAggregator} storageClass - the type of storage to use in this manager; 'EventBuffer' (1), 'EventAggregator' (2)
24
24
  * @param {string} [defaultEntityGuid] - the entity guid to use as the default storage instance; if not provided, a new one is created
25
- * @param {string} featureName - the name of the feature this manager is for; used for event dispatching
25
+ * @param {Object} featureAgg - the feature aggregate instance that initialized this manager
26
26
  */
27
- constructor(agentRef, storageClass, defaultEntityGuid, featureName) {
27
+ constructor(agentRef, storageClass, defaultEntityGuid, featureAgg) {
28
28
  this.agentRef = agentRef;
29
29
  this.entityManager = agentRef.runtime.entityManager;
30
30
  this.StorageClass = storageClass;
31
- this.appStorageMap = new Map([[_agentConstants.DEFAULT_KEY, new this.StorageClass()]]);
32
- this.featureName = featureName;
31
+ this.appStorageMap = new Map([[_agentConstants.DEFAULT_KEY, new this.StorageClass(_agentConstants.MAX_PAYLOAD_SIZE, featureAgg)]]);
32
+ this.featureAgg = featureAgg;
33
33
  this.setEventStore(defaultEntityGuid);
34
34
  }
35
35
 
@@ -46,7 +46,7 @@ class EventStoreManager {
46
46
  /** we should already have an event store for the default */
47
47
  if (!targetEntityGuid) return;
48
48
  /** if the target is the container agent, SHARE the default storage -- otherwise create a new event store */
49
- const eventStorage = (0, _target.isContainerAgentTarget)(this.entityManager.get(targetEntityGuid), this.agentRef) ? this.appStorageMap.get(_agentConstants.DEFAULT_KEY) : new this.StorageClass();
49
+ const eventStorage = (0, _target.isContainerAgentTarget)(this.entityManager.get(targetEntityGuid), this.agentRef) ? this.appStorageMap.get(_agentConstants.DEFAULT_KEY) : new this.StorageClass(_agentConstants.MAX_PAYLOAD_SIZE, this.featureAgg);
50
50
  this.appStorageMap.set(targetEntityGuid, eventStorage);
51
51
  }
52
52
 
@@ -78,7 +78,7 @@ class EventStoreManager {
78
78
  drained: !!_featureFlags.activatedFeatures?.[this.agentRef.agentIdentifier],
79
79
  type: 'data',
80
80
  name: 'buffer',
81
- feature: this.featureName,
81
+ feature: this.featureAgg.featureName,
82
82
  data: event
83
83
  });
84
84
  return this.#getEventStore(targetEntityGuid).add(event);