@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
@@ -11,6 +11,9 @@ import { Aggregator } from './aggregator';
11
11
  export class EventAggregator {
12
12
  #aggregator = new Aggregator();
13
13
  #savedNamesToBuckets = {};
14
+ byteSize() {
15
+ 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.
16
+ }
14
17
  isEmpty({
15
18
  aggregatorTypes
16
19
  }) {
@@ -36,6 +36,10 @@ const RuntimeModel = {
36
36
  releaseIds: {},
37
37
  session: undefined,
38
38
  timeKeeper: undefined,
39
+ /** a proxy is set in agent-session to track jsAttributes changes for harvesting mechanics */
40
+ jsAttributesMetadata: {
41
+ bytes: 0
42
+ },
39
43
  get harvestCount() {
40
44
  return ++_harvestCount;
41
45
  }
@@ -2,6 +2,6 @@
2
2
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
- export const IDEAL_PAYLOAD_SIZE = 64000;
5
+ export const IDEAL_PAYLOAD_SIZE = 16000;
6
6
  export const MAX_PAYLOAD_SIZE = 1000000;
7
7
  export const DEFAULT_KEY = 'NR_CONTAINER_AGENT';
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.293.0";
14
+ export const VERSION = "1.294.0-rc.0";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.293.0";
14
+ export const VERSION = "1.294.0-rc.0";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -83,12 +83,13 @@ function ee(old, debugId) {
83
83
  if (old && bubble) old.emit(type, args, contextOrStore);
84
84
  var ctx = context(contextOrStore);
85
85
  var handlersArray = listeners(type);
86
- var len = handlersArray.length;
87
86
 
88
87
  // Apply each handler function in the order they were added
89
88
  // to the context with the arguments
90
89
 
91
- for (var i = 0; i < len; i++) handlersArray[i].apply(ctx, args);
90
+ handlersArray.forEach(handler => {
91
+ handler.apply(ctx, args);
92
+ });
92
93
 
93
94
  // Buffer after emitting for consistent ordering
94
95
  var bufferGroup = getBuffer()[bufferGroupMap[type]];
@@ -8,10 +8,11 @@ var patternWithoutHash = /([^?#]*)().*/;
8
8
 
9
9
  /**
10
10
  * Cleans a URL by removing the query string and fragment (hash portion).
11
- * @param {string} url - The original URL to be cleaned.
11
+ * @param {string} [url] - The original URL to be cleaned.
12
12
  * @param {boolean} [keepHash=false] - Whether to preserve the hash portion of the URL.
13
13
  * @returns {string} The cleaned URL.
14
14
  */
15
15
  export function cleanURL(url, keepHash) {
16
+ if (!url) return url;
16
17
  return url.replace(keepHash ? patternWithHash : patternWithoutHash, '$1$2');
17
18
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Copyright 2020-2025 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import { stringify } from './stringify';
6
+ export function trackObjectAttributeSize(parent, object) {
7
+ const originalAttribute = parent[object] ??= {};
8
+ const output = {
9
+ bytes: Object.keys(originalAttribute).reduce((acc, key) => acc + key.length + stringify(originalAttribute[key]).length, 0)
10
+ };
11
+ // proxy attribute to calculate its size when changed
12
+ parent[object] = new Proxy(originalAttribute, {
13
+ set(target, prop, value) {
14
+ output.bytes += prop.length + stringify(value).length;
15
+ target[prop] = value;
16
+ return true;
17
+ },
18
+ deleteProperty(target, prop) {
19
+ output.bytes -= prop.length + stringify(target[prop]).length;
20
+ return delete target[prop];
21
+ }
22
+ });
23
+ return output;
24
+ }
@@ -18,6 +18,12 @@ export class Aggregate extends AggregateBase {
18
18
  setDenyList(agentRef.runtime.denyList);
19
19
  this.underSpaEvents = {};
20
20
  const classThis = this;
21
+ if (!agentRef.init.ajax.block_internal) {
22
+ // 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.
23
+ super.canHarvestEarly = false;
24
+ } else {
25
+ super.customAttributesAreSeparate = true;
26
+ }
21
27
 
22
28
  // --- v Used by old spa feature
23
29
  this.ee.on('interactionDone', (interaction, wasSaved) => {
@@ -10,7 +10,6 @@ import { AggregateBase } from '../../utils/aggregate-base';
10
10
  import { warn } from '../../../common/util/console';
11
11
  import { now } from '../../../common/timing/now';
12
12
  import { registerHandler } from '../../../common/event-emitter/register-handler';
13
- import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants';
14
13
  import { applyFnToProps } from '../../../common/util/traverse';
15
14
  import { UserActionsAggregator } from './user-actions/user-actions-aggregator';
16
15
  import { isIFrameWindow } from '../../../common/dom/iframe';
@@ -19,7 +18,6 @@ export class Aggregate extends AggregateBase {
19
18
  static featureName = FEATURE_NAME;
20
19
  constructor(agentRef) {
21
20
  super(agentRef, FEATURE_NAME);
22
- this.eventsPerHarvest = 1000;
23
21
  this.referrerUrl = isBrowserScope && document.referrer ? cleanURL(document.referrer) : undefined;
24
22
  this.waitForFlags(['ins']).then(([ins]) => {
25
23
  if (!ins) {
@@ -277,18 +275,7 @@ export class Aggregate extends AggregateBase {
277
275
  /** Event-specific attributes take precedence over agent-level custom attributes and fallbacks */
278
276
  ...obj
279
277
  };
280
- const addedEvent = this.events.add(eventAttributes, targetEntityGuid);
281
- if (!addedEvent && !this.events.isEmpty(undefined, targetEntityGuid)) {
282
- /** could not add the event because it pushed the buffer over the limit
283
- * so we harvest early, and try to add it again now that the buffer is cleared
284
- * if it fails again, we do nothing
285
- */
286
- this.ee.emit(SUPPORTABILITY_METRIC_CHANNEL, ['GenericEvents/Harvest/Max/Seen']);
287
- this.agentRef.runtime.harvester.triggerHarvestFor(this, {
288
- targetEntityGuid
289
- });
290
- this.events.add(eventAttributes);
291
- }
278
+ this.events.add(eventAttributes, targetEntityGuid);
292
279
  }
293
280
  serializer(eventBuffer) {
294
281
  return applyFnToProps({
@@ -4,8 +4,6 @@
4
4
  */
5
5
  import { FEATURE_NAMES } from '../../loaders/features/features';
6
6
  export const FEATURE_NAME = FEATURE_NAMES.genericEvents;
7
- export const IDEAL_PAYLOAD_SIZE = 64000;
8
- export const MAX_PAYLOAD_SIZE = 1000000;
9
7
  export const OBSERVED_EVENTS = ['auxclick', 'click', 'copy', 'keydown', 'paste', 'scrollend'];
10
8
  export const OBSERVED_WINDOW_EVENTS = ['focus', 'blur'];
11
9
  export const RAGE_CLICK_THRESHOLD_EVENTS = 4;
@@ -10,7 +10,6 @@ import { FEATURE_NAME, LOGGING_EVENT_EMITTER_CHANNEL, LOG_LEVELS, LOGGING_MODE }
10
10
  import { Log } from '../shared/log';
11
11
  import { isValidLogLevel } from '../shared/utils';
12
12
  import { applyFnToProps } from '../../../common/util/traverse';
13
- import { MAX_PAYLOAD_SIZE } from '../../../common/constants/agent-constants';
14
13
  import { isContainerAgentTarget } from '../../../common/util/target';
15
14
  import { SESSION_EVENT_TYPES, SESSION_EVENTS } from '../../../common/session/constants';
16
15
  import { ABORT_REASONS } from '../../session_replay/constants';
@@ -20,6 +19,7 @@ export class Aggregate extends AggregateBase {
20
19
  constructor(agentRef) {
21
20
  super(agentRef, FEATURE_NAME);
22
21
  this.isSessionTrackingEnabled = canEnableSessionTracking(agentRef.init) && agentRef.runtime.session;
22
+ super.customAttributesAreSeparate = true;
23
23
 
24
24
  // 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.
25
25
  this.ee.on(SESSION_EVENTS.RESET, () => {
@@ -83,28 +83,7 @@ export class Aggregate extends AggregateBase {
83
83
  }
84
84
  if (typeof message !== 'string' || !message) return warn(32);
85
85
  const log = new Log(Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp)), message, attributes, level);
86
- const logBytes = log.message.length + stringify(log.attributes).length + log.level.length + 10; // timestamp == 10 chars
87
-
88
- const failToHarvestMessage = 'Logging/Harvest/Failed/Seen';
89
- if (logBytes > MAX_PAYLOAD_SIZE) {
90
- // cannot possibly send this, even with an empty buffer
91
- this.reportSupportabilityMetric(failToHarvestMessage, logBytes);
92
- warn(31, log.message.slice(0, 25) + '...');
93
- return;
94
- }
95
- if (this.events.wouldExceedMaxSize(logBytes, targetEntityGuid)) {
96
- this.reportSupportabilityMetric('Logging/Harvest/Early/Seen', this.events.byteSize() + logBytes);
97
- this.agentRef.runtime.harvester.triggerHarvestFor(this, {
98
- targetEntityGuid
99
- }); // force a harvest synchronously to try adding again
100
- }
101
- if (!this.events.add(log, targetEntityGuid)) {
102
- // still failed after a harvest attempt despite not being too large would mean harvest failed with options.retry
103
- this.reportSupportabilityMetric(failToHarvestMessage, logBytes);
104
- warn(31, log.message.slice(0, 25) + '...');
105
- } else {
106
- this.reportSupportabilityMetric('Logging/Event/Added/Seen');
107
- }
86
+ this.events.add(log, targetEntityGuid);
108
87
  }
109
88
  serializer(eventBuffer, targetEntityGuid) {
110
89
  const target = this.agentRef.runtime.entityManager.get(targetEntityGuid);
@@ -32,6 +32,7 @@ export class Aggregate extends AggregateBase {
32
32
  super(agentRef, FEATURE_NAME);
33
33
  this.curSessEndRecorded = false;
34
34
  this.firstIxnRecorded = false;
35
+ super.customAttributesAreSeparate = true;
35
36
  registerHandler('docHidden', msTimestamp => this.endCurrentSession(msTimestamp), this.featureName, this.ee);
36
37
  // Add the time of _window pagehide event_ firing to the next PVT harvest == NRDB windowUnload attr:
37
38
  registerHandler('winPagehide', msTimestamp => this.addTiming('unload', msTimestamp, null), this.featureName, this.ee);
@@ -133,6 +134,7 @@ export class Aggregate extends AggregateBase {
133
134
 
134
135
  // serialize array of timing data
135
136
  serializer(eventBuffer) {
137
+ if (!eventBuffer?.length) return '';
136
138
  var addString = getAddStringContext(this.agentRef.runtime.obfuscator);
137
139
  var payload = 'bel.6;';
138
140
  for (var i = 0; i < eventBuffer.length; i++) {
@@ -6,8 +6,8 @@ import { addCustomAttributes, getAddStringContext, nullable, numeric } from '../
6
6
  import { NODE_TYPE } from '../constants';
7
7
  import { BelNode } from './bel-node';
8
8
  export class AjaxNode extends BelNode {
9
- constructor(agentRef, ajaxEvent) {
10
- super(agentRef);
9
+ constructor(ajaxEvent) {
10
+ super();
11
11
  this.belType = NODE_TYPE.AJAX;
12
12
  this.method = ajaxEvent.method;
13
13
  this.status = ajaxEvent.status;
@@ -23,8 +23,8 @@ export class AjaxNode extends BelNode {
23
23
  this.start = ajaxEvent.startTime; // 5000 --- 5500 --> 10500
24
24
  this.end = ajaxEvent.endTime;
25
25
  }
26
- serialize(parentStartTimestamp) {
27
- const addString = getAddStringContext(this.obfuscator);
26
+ serialize(parentStartTimestamp, agentRef) {
27
+ const addString = getAddStringContext(agentRef.runtime.obfuscator);
28
28
  const nodeList = [];
29
29
 
30
30
  // 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.
@@ -12,10 +12,6 @@ export class BelNode {
12
12
  callbackEnd = 0;
13
13
  callbackDuration = 0;
14
14
  nodeId = ++nodesSeen;
15
- constructor(agentRef) {
16
- this.obfuscator = agentRef.runtime.obfuscator;
17
- this.info = agentRef.info;
18
- }
19
15
  addChild(child) {
20
16
  this.children.push(child);
21
17
  }
@@ -18,6 +18,7 @@ export class Aggregate extends AggregateBase {
18
18
  domObserver
19
19
  }) {
20
20
  super(agentRef, FEATURE_NAME);
21
+ super.customAttributesAreSeparate = true;
21
22
  this.interactionsToHarvest = this.events;
22
23
  this.domObserver = domObserver;
23
24
  this.initialPageLoadInteraction = new InitialPageLoadInteraction(agentRef);
@@ -26,7 +27,7 @@ export class Aggregate extends AggregateBase {
26
27
  if (agentRef.runtime.session?.isNew) this.initialPageLoadInteraction.customAttributes.isFirstOfSession = true; // mark the hard page load as first of its session
27
28
  this.initialPageLoadInteraction.forceSave = true; // unless forcibly ignored, iPL always finish by default
28
29
  const ixn = this.initialPageLoadInteraction;
29
- this.interactionsToHarvest.add(ixn);
30
+ this.events.add(ixn); // add the iPL ixn to the buffer for harvest
30
31
  this.initialPageLoadInteraction = null;
31
32
  });
32
33
  timeToFirstByte.subscribe(({
@@ -73,7 +74,7 @@ export class Aggregate extends AggregateBase {
73
74
  let firstIxnStartTime;
74
75
  const serializedIxnList = [];
75
76
  for (const interaction of eventBuffer) {
76
- serializedIxnList.push(interaction.serialize(firstIxnStartTime));
77
+ serializedIxnList.push(interaction.serialize(firstIxnStartTime, this.agentRef));
77
78
  if (firstIxnStartTime === undefined) firstIxnStartTime = Math.floor(interaction.start); // careful not to match or overwrite on 0 value!
78
79
  }
79
80
  return "bel.7;".concat(serializedIxnList.join(';'));
@@ -84,7 +85,7 @@ export class Aggregate extends AggregateBase {
84
85
  if (this.interactionInProgress?.done() === false) return; // current in-progress is blocked from closing, e.g. by 'waitForEnd' api option
85
86
 
86
87
  const oldURL = eventName === INTERACTION_TRIGGERS[3] ? this.latestHistoryUrl : undefined; // see related comment in 'newURL' handler above, 'popstate'
87
- this.interactionInProgress = new Interaction(this.agentRef, eventName, startedAt, this.latestRouteSetByApi, oldURL);
88
+ this.interactionInProgress = new Interaction(eventName, startedAt, this.latestRouteSetByApi, oldURL);
88
89
  if (eventName === INTERACTION_TRIGGERS[0]) {
89
90
  // 'click'
90
91
  const sourceElemText = getActionText(sourceElem);
@@ -100,7 +101,7 @@ export class Aggregate extends AggregateBase {
100
101
  setClosureHandlers() {
101
102
  this.interactionInProgress.on('finished', () => {
102
103
  const ref = this.interactionInProgress;
103
- this.interactionsToHarvest.add(this.interactionInProgress);
104
+ this.events.add(this.interactionInProgress); // add the ixn to the buffer for harvest
104
105
  this.interactionInProgress = null;
105
106
  this.domObserver.disconnect(); // can stop observing whenever our interaction logic completes a cycle
106
107
 
@@ -127,9 +128,8 @@ export class Aggregate extends AggregateBase {
127
128
  */
128
129
  if (this.interactionInProgress?.isActiveDuring(timestamp)) return this.interactionInProgress;
129
130
  let saveIxn;
130
- const [{
131
- data: interactionsBuffer
132
- }] = this.interactionsToHarvest.get();
131
+ const interactionsBuffer = this.interactionsToHarvest.get()?.[0]?.data;
132
+ if (!interactionsBuffer) return undefined; // no interactions have been staged yet, so nothing to search through)
133
133
  for (let idx = interactionsBuffer.length - 1; idx >= 0; idx--) {
134
134
  // reverse search for the latest completed interaction for efficiency
135
135
  const finishedInteraction = interactionsBuffer[idx];
@@ -154,15 +154,15 @@ export class Aggregate extends AggregateBase {
154
154
  // no interaction was happening when this ajax started, so give it back to Ajax feature for processing
155
155
  handle('returnAjax', [event], undefined, FEATURE_NAMES.ajax, this.ee);
156
156
  } else {
157
- if (associatedInteraction.status === INTERACTION_STATUS.FIN) processAjax(this.agentRef, event, associatedInteraction); // tack ajax onto the ixn object awaiting harvest
157
+ if (associatedInteraction.status === INTERACTION_STATUS.FIN) processAjax(event, associatedInteraction); // tack ajax onto the ixn object awaiting harvest
158
158
  else {
159
159
  // 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
160
- associatedInteraction.on('finished', () => processAjax(this.agentRef, event, associatedInteraction));
160
+ associatedInteraction.on('finished', () => processAjax(event, associatedInteraction));
161
161
  associatedInteraction.on('cancelled', () => handle('returnAjax', [event], undefined, FEATURE_NAMES.ajax, this.ee));
162
162
  }
163
163
  }
164
- function processAjax(agent, event, parentInteraction) {
165
- const newNode = new AjaxNode(agent, event);
164
+ function processAjax(event, parentInteraction) {
165
+ const newNode = new AjaxNode(event);
166
166
  parentInteraction.addChild(newNode);
167
167
  }
168
168
  }
@@ -201,7 +201,7 @@ export class Aggregate extends AggregateBase {
201
201
  if (this.associatedInteraction?.trigger === IPL_TRIGGER_NAME) this.associatedInteraction = null; // the api get-interaction method cannot target IPL
202
202
  if (!this.associatedInteraction) {
203
203
  // 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.
204
- this.associatedInteraction = thisClass.interactionInProgress = new Interaction(thisClass.agentRef, API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi);
204
+ this.associatedInteraction = thisClass.interactionInProgress = new Interaction(API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi);
205
205
  thisClass.domObserver.observe(document.body, {
206
206
  attributes: true,
207
207
  childList: true,
@@ -10,10 +10,11 @@ import { firstContentfulPaint } from '../../../common/vitals/first-contentful-pa
10
10
  import { IPL_TRIGGER_NAME } from '../constants';
11
11
  export class InitialPageLoadInteraction extends Interaction {
12
12
  constructor(agentRef) {
13
- super(agentRef, IPL_TRIGGER_NAME, 0, null);
13
+ super(IPL_TRIGGER_NAME, 0, null);
14
14
  this.queueTime = agentRef.info.queueTime;
15
15
  this.appTime = agentRef.info.applicationTime;
16
- this.oldURL = document.referrer;
16
+ /** @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 */
17
+ this.oldURL = document.referrer || undefined;
17
18
  }
18
19
  get firstPaint() {
19
20
  return firstPaint.current.value;
@@ -30,8 +30,8 @@ export class Interaction extends BelNode {
30
30
  keepOpenUntilEndApi = false;
31
31
  onDone = [];
32
32
  cancellationTimer;
33
- constructor(agentRef, uiEvent, uiEventTimestamp, currentRouteKnown, currentUrl) {
34
- super(agentRef);
33
+ constructor(uiEvent, uiEventTimestamp, currentRouteKnown, currentUrl) {
34
+ super();
35
35
  this.belType = NODE_TYPE.INTERACTION;
36
36
  this.trigger = uiEvent;
37
37
  this.start = uiEventTimestamp;
@@ -72,10 +72,6 @@ export class Interaction extends BelNode {
72
72
  #finish(customEndTime = 0) {
73
73
  clearTimeout(this.cancellationTimer);
74
74
  this.end = Math.max(this.domTimestamp, this.historyTimestamp, customEndTime);
75
- this.customAttributes = {
76
- ...this.info.jsAttributes,
77
- ...this.customAttributes
78
- }; // attrs specific to this interaction should have precedence over the general custom attrs
79
75
  this.status = INTERACTION_STATUS.FIN;
80
76
 
81
77
  // Run all the callbacks awaiting this interaction to finish.
@@ -106,9 +102,16 @@ export class Interaction extends BelNode {
106
102
  get firstPaint() {}
107
103
  get firstContentfulPaint() {}
108
104
  get navTiming() {}
109
- serialize(firstStartTimeOfPayload) {
105
+
106
+ /**
107
+ * Serializes (BEL) the interaction data for transmission.
108
+ * @param {Number} firstStartTimeOfPayload timestamp
109
+ * @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.
110
+ * @returns {String} A string that is the serialized representation of this interaction.
111
+ */
112
+ serialize(firstStartTimeOfPayload, agentRef) {
110
113
  const isFirstIxnOfPayload = firstStartTimeOfPayload === undefined;
111
- const addString = getAddStringContext(this.obfuscator);
114
+ const addString = getAddStringContext(agentRef.runtime.obfuscator);
112
115
  const nodeList = [];
113
116
  let ixnType;
114
117
  if (this.trigger === IPL_TRIGGER_NAME) ixnType = INTERACTION_TYPE.INITIAL_PAGE_LOAD;else if (this.newURL !== this.oldURL) ixnType = INTERACTION_TYPE.ROUTE_CHANGE;else ixnType = INTERACTION_TYPE.UNSPECIFIED;
@@ -125,12 +128,16 @@ export class Interaction extends BelNode {
125
128
  numeric(this.callbackDuration),
126
129
  // not relative
127
130
  addString(this.trigger), addString(cleanURL(this.initialPageURL, true)), addString(cleanURL(this.oldURL, true)), addString(cleanURL(this.newURL, true)), addString(this.customName), ixnType, nullable(this.queueTime, numeric, true) + nullable(this.appTime, numeric, true) + nullable(this.oldRoute, addString, true) + nullable(this.newRoute, addString, true) + addString(this.id), addString(this.nodeId), nullable(this.firstPaint, numeric, true) + nullable(this.firstContentfulPaint, numeric)];
128
- const allAttachedNodes = addCustomAttributes(this.customAttributes || {}, addString); // start with all custom attributes
129
- if (this.info.atts) allAttachedNodes.push('a,' + addString(this.info.atts)); // add apm provided attributes
131
+ const customAttributes = {
132
+ ...agentRef.info.jsAttributes,
133
+ ...this.customAttributes
134
+ }; // attrs specific to this interaction should have precedence over the general custom attrs
135
+ const allAttachedNodes = addCustomAttributes(customAttributes || {}, addString); // start with all custom attributes
136
+ if (agentRef.info.atts) allAttachedNodes.push('a,' + addString(agentRef.info.atts)); // add apm provided attributes
130
137
  /* Querypack encoder+decoder quirkiness:
131
138
  - 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.)
132
139
  - 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. */
133
- 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
140
+ 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
134
141
 
135
142
  fields[1] = numeric(allAttachedNodes.length);
136
143
  nodeList.push(fields);
@@ -27,7 +27,7 @@ export function Interaction(eventName, timestamp, url, routeName, onFinished, ag
27
27
  attrs.initialPageURL = initialLocation;
28
28
  attrs.oldRoute = routeName;
29
29
  attrs.newURL = url;
30
- attrs.oldURL = eventName === 'initialPageLoad' ? document.referrer : url;
30
+ 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.
31
31
  attrs.custom = {};
32
32
  attrs.store = {};
33
33
  }
@@ -8,6 +8,8 @@ import { registerHandler } from '../../common/event-emitter/register-handler';
8
8
  import { SessionEntity } from '../../common/session/session-entity';
9
9
  import { LocalStorage } from '../../common/storage/local-storage.js';
10
10
  import { DEFAULT_KEY } from '../../common/session/constants';
11
+ import { mergeInfo } from '../../common/config/info';
12
+ import { trackObjectAttributeSize } from '../../common/util/attribute-size';
11
13
  export function setupAgentSession(agentRef) {
12
14
  if (agentRef.runtime.session) return agentRef.runtime.session; // already setup
13
15
 
@@ -22,13 +24,19 @@ export function setupAgentSession(agentRef) {
22
24
 
23
25
  // Retrieve & re-add all of the persisted setCustomAttribute|setUserId k-v from previous page load(s), if any was stored.
24
26
  const customSessionData = agentRef.runtime.session.state.custom;
25
- if (customSessionData) {
27
+ if (customSessionData && Object.keys(customSessionData).length) {
26
28
  /** stored attributes from previous page should not take precedence over attributes stored on this page via API before the page load */
27
- agentRef.info.jsAttributes = {
28
- ...customSessionData,
29
- ...agentRef.info.jsAttributes
30
- };
29
+ agentRef.info = mergeInfo({
30
+ ...agentRef.info,
31
+ jsAttributes: {
32
+ ...customSessionData,
33
+ ...agentRef.info.jsAttributes
34
+ }
35
+ });
31
36
  }
37
+
38
+ /** track changes to the jsAttributes field over time for aiding with harvest mechanics */
39
+ agentRef.runtime.jsAttributesMetadata = trackObjectAttributeSize(agentRef.info, 'jsAttributes');
32
40
  const sharedEE = ee.get(agentRef.agentIdentifier);
33
41
 
34
42
  // any calls to newrelic.setCustomAttribute(<persisted>) will need to be added to:
@@ -18,6 +18,7 @@ import { EventBuffer } from './event-buffer';
18
18
  import { handle } from '../../common/event-emitter/handle';
19
19
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../metrics/constants';
20
20
  import { EventAggregator } from '../../common/aggregate/event-aggregator';
21
+ import { IDEAL_PAYLOAD_SIZE } from '../../common/constants/agent-constants';
21
22
  export class AggregateBase extends FeatureBase {
22
23
  /**
23
24
  * Create an AggregateBase instance.
@@ -29,6 +30,12 @@ export class AggregateBase extends FeatureBase {
29
30
  this.agentRef = agentRef;
30
31
  this.checkConfiguration(agentRef);
31
32
  this.doOnceForAllAggregate(agentRef);
33
+
34
+ /** @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 */
35
+ this.customAttributesAreSeparate = false;
36
+ /** @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 */
37
+ this.canHarvestEarly = true; // this is set to false in derived classes that need to block early harvests, like ajax under certain conditions
38
+
32
39
  this.harvestOpts = {}; // features aggregate classes can define custom opts for when their harvest is called
33
40
 
34
41
  const agentEntityGuid = this.agentRef?.runtime?.appMetadata?.agents?.[0]?.entityGuid;
@@ -58,17 +65,33 @@ export class AggregateBase extends FeatureBase {
58
65
  // Jserror and Metric features uses a singleton EventAggregator instead of a regular EventBuffer.
59
66
  case FEATURE_NAMES.jserrors:
60
67
  case FEATURE_NAMES.metrics:
61
- this.events = this.agentRef.sharedAggregator ??= new EventStoreManager(this.agentRef, EventAggregator, entityGuid, 'shared_aggregator');
68
+ this.events = this.agentRef.sharedAggregator ??= new EventStoreManager(this.agentRef, EventAggregator, entityGuid, {
69
+ featureName: 'shared_aggregator'
70
+ });
62
71
  break;
63
72
  /** 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.
64
73
  This was necessary to prevent race cond. issues where the event buffer was checked before the feature could "block" itself.
65
74
  Its easier to just keep an empty event buffer in place. */
66
75
  default:
67
- this.events = new EventStoreManager(this.agentRef, EventBuffer, entityGuid, this.featureName);
76
+ this.events = new EventStoreManager(this.agentRef, EventBuffer, entityGuid, this);
68
77
  break;
69
78
  }
70
79
  }
71
80
 
81
+ /**
82
+ * 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.
83
+ * If conditions are met, a new harvest will be triggered immediately.
84
+ * @returns void
85
+ */
86
+ decideEarlyHarvest() {
87
+ if (!this.canHarvestEarly) return;
88
+ const estimatedSize = this.events.byteSize() + (this.customAttributesAreSeparate ? this.agentRef.runtime.jsAttributesMetadata.bytes : 0);
89
+ if (estimatedSize > IDEAL_PAYLOAD_SIZE) {
90
+ this.agentRef.runtime.harvester.triggerHarvestFor(this);
91
+ this.reportSupportabilityMetric("".concat(this.featureName, "/Harvest/Early/Seen"), estimatedSize);
92
+ }
93
+ }
94
+
72
95
  /**
73
96
  * New handler for waiting for multiple flags. Useful when expecting multiple flags simultaneously (ex. stn vs sr)
74
97
  * @param {string[]} flagNames
@@ -11,10 +11,13 @@ export class EventBuffer {
11
11
  #rawBytesBackup;
12
12
 
13
13
  /**
14
- * @param {number} maxPayloadSize
14
+ * Creates an event buffer that can hold feature-processed events.
15
+ * @param {Number} maxPayloadSize The maximum size of the payload that can be stored in this buffer.
16
+ * @param {Object} [featureAgg] - the feature aggregate instance
15
17
  */
16
- constructor(maxPayloadSize = MAX_PAYLOAD_SIZE) {
18
+ constructor(maxPayloadSize = MAX_PAYLOAD_SIZE, featureAgg) {
17
19
  this.maxPayloadSize = maxPayloadSize;
20
+ this.featureAgg = featureAgg;
18
21
  }
19
22
  isEmpty() {
20
23
  return this.#buffer.length === 0;
@@ -36,9 +39,15 @@ export class EventBuffer {
36
39
  */
37
40
  add(event) {
38
41
  const addSize = stringify(event)?.length || 0; // (estimate) # of bytes a directly stringified event it would take to send
39
- if (this.#rawBytes + addSize > this.maxPayloadSize) return false;
42
+ if (this.#rawBytes + addSize > this.maxPayloadSize) {
43
+ const smTag = inject => "EventBuffer/".concat(inject, "/Dropped/Bytes");
44
+ this.featureAgg?.reportSupportabilityMetric(smTag(this.featureAgg.featureName), addSize); // bytes dropped for this feature will aggregate with this metric tag
45
+ this.featureAgg?.reportSupportabilityMetric(smTag('Combined'), addSize); // all bytes dropped across all features will aggregate with this metric tag
46
+ return false;
47
+ }
40
48
  this.#buffer.push(event);
41
49
  this.#rawBytes += addSize;
50
+ this.featureAgg?.decideEarlyHarvest(); // check if we should harvest early with new data
42
51
  return true;
43
52
  }
44
53
 
@@ -2,7 +2,7 @@
2
2
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
- import { DEFAULT_KEY } from '../../common/constants/agent-constants';
5
+ import { DEFAULT_KEY, MAX_PAYLOAD_SIZE } from '../../common/constants/agent-constants';
6
6
  import { dispatchGlobalEvent } from '../../common/dispatch/global-event';
7
7
  import { activatedFeatures } from '../../common/util/feature-flags';
8
8
  import { isContainerAgentTarget } from '../../common/util/target';
@@ -15,14 +15,14 @@ export class EventStoreManager {
15
15
  * @param {object} agentRef - reference to base agent class
16
16
  * @param {EventBuffer|EventAggregator} storageClass - the type of storage to use in this manager; 'EventBuffer' (1), 'EventAggregator' (2)
17
17
  * @param {string} [defaultEntityGuid] - the entity guid to use as the default storage instance; if not provided, a new one is created
18
- * @param {string} featureName - the name of the feature this manager is for; used for event dispatching
18
+ * @param {Object} featureAgg - the feature aggregate instance that initialized this manager
19
19
  */
20
- constructor(agentRef, storageClass, defaultEntityGuid, featureName) {
20
+ constructor(agentRef, storageClass, defaultEntityGuid, featureAgg) {
21
21
  this.agentRef = agentRef;
22
22
  this.entityManager = agentRef.runtime.entityManager;
23
23
  this.StorageClass = storageClass;
24
- this.appStorageMap = new Map([[DEFAULT_KEY, new this.StorageClass()]]);
25
- this.featureName = featureName;
24
+ this.appStorageMap = new Map([[DEFAULT_KEY, new this.StorageClass(MAX_PAYLOAD_SIZE, featureAgg)]]);
25
+ this.featureAgg = featureAgg;
26
26
  this.setEventStore(defaultEntityGuid);
27
27
  }
28
28
 
@@ -39,7 +39,7 @@ export class EventStoreManager {
39
39
  /** we should already have an event store for the default */
40
40
  if (!targetEntityGuid) return;
41
41
  /** if the target is the container agent, SHARE the default storage -- otherwise create a new event store */
42
- const eventStorage = isContainerAgentTarget(this.entityManager.get(targetEntityGuid), this.agentRef) ? this.appStorageMap.get(DEFAULT_KEY) : new this.StorageClass();
42
+ const eventStorage = isContainerAgentTarget(this.entityManager.get(targetEntityGuid), this.agentRef) ? this.appStorageMap.get(DEFAULT_KEY) : new this.StorageClass(MAX_PAYLOAD_SIZE, this.featureAgg);
43
43
  this.appStorageMap.set(targetEntityGuid, eventStorage);
44
44
  }
45
45
 
@@ -71,7 +71,7 @@ export class EventStoreManager {
71
71
  drained: !!activatedFeatures?.[this.agentRef.agentIdentifier],
72
72
  type: 'data',
73
73
  name: 'buffer',
74
- feature: this.featureName,
74
+ feature: this.featureAgg.featureName,
75
75
  data: event
76
76
  });
77
77
  return this.#getEventStore(targetEntityGuid).add(event);