@newrelic/browser-agent 1.293.0 → 1.294.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 (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
@@ -7,8 +7,8 @@ import { NODE_TYPE } from '../constants'
7
7
  import { BelNode } from './bel-node'
8
8
 
9
9
  export class AjaxNode extends BelNode {
10
- constructor (agentRef, ajaxEvent) {
11
- super(agentRef)
10
+ constructor (ajaxEvent) {
11
+ super()
12
12
  this.belType = NODE_TYPE.AJAX
13
13
  this.method = ajaxEvent.method
14
14
  this.status = ajaxEvent.status
@@ -26,8 +26,8 @@ export class AjaxNode extends BelNode {
26
26
  this.end = ajaxEvent.endTime
27
27
  }
28
28
 
29
- serialize (parentStartTimestamp) {
30
- const addString = getAddStringContext(this.obfuscator)
29
+ serialize (parentStartTimestamp, agentRef) {
30
+ const addString = getAddStringContext(agentRef.runtime.obfuscator)
31
31
  const nodeList = []
32
32
 
33
33
  // 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.
@@ -14,11 +14,6 @@ export class BelNode {
14
14
  callbackDuration = 0
15
15
  nodeId = ++nodesSeen
16
16
 
17
- constructor (agentRef) {
18
- this.obfuscator = agentRef.runtime.obfuscator
19
- this.info = agentRef.info
20
- }
21
-
22
17
  addChild (child) {
23
18
  this.children.push(child)
24
19
  }
@@ -18,6 +18,8 @@ export class Aggregate extends AggregateBase {
18
18
  constructor (agentRef, { domObserver }) {
19
19
  super(agentRef, FEATURE_NAME)
20
20
 
21
+ super.customAttributesAreSeparate = true
22
+
21
23
  this.interactionsToHarvest = this.events
22
24
  this.domObserver = domObserver
23
25
 
@@ -26,7 +28,7 @@ export class Aggregate extends AggregateBase {
26
28
  if (agentRef.runtime.session?.isNew) this.initialPageLoadInteraction.customAttributes.isFirstOfSession = true // mark the hard page load as first of its session
27
29
  this.initialPageLoadInteraction.forceSave = true // unless forcibly ignored, iPL always finish by default
28
30
  const ixn = this.initialPageLoadInteraction
29
- this.interactionsToHarvest.add(ixn)
31
+ this.events.add(ixn) // add the iPL ixn to the buffer for harvest
30
32
  this.initialPageLoadInteraction = null
31
33
  })
32
34
  timeToFirstByte.subscribe(({ attrs }) => {
@@ -75,7 +77,7 @@ export class Aggregate extends AggregateBase {
75
77
  let firstIxnStartTime
76
78
  const serializedIxnList = []
77
79
  for (const interaction of eventBuffer) {
78
- serializedIxnList.push(interaction.serialize(firstIxnStartTime))
80
+ serializedIxnList.push(interaction.serialize(firstIxnStartTime, this.agentRef))
79
81
  if (firstIxnStartTime === undefined) firstIxnStartTime = Math.floor(interaction.start) // careful not to match or overwrite on 0 value!
80
82
  }
81
83
  return `bel.7;${serializedIxnList.join(';')}`
@@ -86,7 +88,7 @@ export class Aggregate extends AggregateBase {
86
88
  if (this.interactionInProgress?.done() === false) return // current in-progress is blocked from closing, e.g. by 'waitForEnd' api option
87
89
 
88
90
  const oldURL = eventName === INTERACTION_TRIGGERS[3] ? this.latestHistoryUrl : undefined // see related comment in 'newURL' handler above, 'popstate'
89
- this.interactionInProgress = new Interaction(this.agentRef, eventName, startedAt, this.latestRouteSetByApi, oldURL)
91
+ this.interactionInProgress = new Interaction(eventName, startedAt, this.latestRouteSetByApi, oldURL)
90
92
 
91
93
  if (eventName === INTERACTION_TRIGGERS[0]) { // 'click'
92
94
  const sourceElemText = getActionText(sourceElem)
@@ -103,7 +105,7 @@ export class Aggregate extends AggregateBase {
103
105
  setClosureHandlers () {
104
106
  this.interactionInProgress.on('finished', () => {
105
107
  const ref = this.interactionInProgress
106
- this.interactionsToHarvest.add(this.interactionInProgress)
108
+ this.events.add(this.interactionInProgress) // add the ixn to the buffer for harvest
107
109
  this.interactionInProgress = null
108
110
  this.domObserver.disconnect() // can stop observing whenever our interaction logic completes a cycle
109
111
 
@@ -133,7 +135,8 @@ export class Aggregate extends AggregateBase {
133
135
  */
134
136
  if (this.interactionInProgress?.isActiveDuring(timestamp)) return this.interactionInProgress
135
137
  let saveIxn
136
- const [{ data: interactionsBuffer }] = 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)
137
140
  for (let idx = interactionsBuffer.length - 1; idx >= 0; idx--) { // reverse search for the latest completed interaction for efficiency
138
141
  const finishedInteraction = interactionsBuffer[idx]
139
142
  if (finishedInteraction.isActiveDuring(timestamp)) {
@@ -156,15 +159,15 @@ export class Aggregate extends AggregateBase {
156
159
  if (!associatedInteraction) { // no interaction was happening when this ajax started, so give it back to Ajax feature for processing
157
160
  handle('returnAjax', [event], undefined, FEATURE_NAMES.ajax, this.ee)
158
161
  } else {
159
- if (associatedInteraction.status === INTERACTION_STATUS.FIN) processAjax(this.agentRef, event, associatedInteraction) // tack ajax onto the ixn object awaiting harvest
162
+ if (associatedInteraction.status === INTERACTION_STATUS.FIN) processAjax(event, associatedInteraction) // tack ajax onto the ixn object awaiting harvest
160
163
  else { // 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
161
- associatedInteraction.on('finished', () => processAjax(this.agentRef, event, associatedInteraction))
164
+ associatedInteraction.on('finished', () => processAjax(event, associatedInteraction))
162
165
  associatedInteraction.on('cancelled', () => handle('returnAjax', [event], undefined, FEATURE_NAMES.ajax, this.ee))
163
166
  }
164
167
  }
165
168
 
166
- function processAjax (agent, event, parentInteraction) {
167
- const newNode = new AjaxNode(agent, event)
169
+ function processAjax (event, parentInteraction) {
170
+ const newNode = new AjaxNode(event)
168
171
  parentInteraction.addChild(newNode)
169
172
  }
170
173
  }
@@ -205,7 +208,7 @@ export class Aggregate extends AggregateBase {
205
208
  if (this.associatedInteraction?.trigger === IPL_TRIGGER_NAME) this.associatedInteraction = null // the api get-interaction method cannot target IPL
206
209
  if (!this.associatedInteraction) {
207
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.
208
- this.associatedInteraction = thisClass.interactionInProgress = new Interaction(thisClass.agentRef, API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi)
211
+ this.associatedInteraction = thisClass.interactionInProgress = new Interaction(API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi)
209
212
  thisClass.domObserver.observe(document.body, { attributes: true, childList: true, subtree: true, characterData: true }) // start observing for DOM changes like a regular UI-driven interaction
210
213
  thisClass.setClosureHandlers()
211
214
  }
@@ -11,10 +11,11 @@ import { IPL_TRIGGER_NAME } from '../constants'
11
11
 
12
12
  export class InitialPageLoadInteraction extends Interaction {
13
13
  constructor (agentRef) {
14
- super(agentRef, IPL_TRIGGER_NAME, 0, null)
14
+ super(IPL_TRIGGER_NAME, 0, null)
15
15
  this.queueTime = agentRef.info.queueTime
16
16
  this.appTime = agentRef.info.applicationTime
17
- this.oldURL = document.referrer
17
+ /** @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 */
18
+ this.oldURL = document.referrer || undefined
18
19
  }
19
20
 
20
21
  get firstPaint () { return firstPaint.current.value }
@@ -31,8 +31,8 @@ export class Interaction extends BelNode {
31
31
  onDone = []
32
32
  cancellationTimer
33
33
 
34
- constructor (agentRef, uiEvent, uiEventTimestamp, currentRouteKnown, currentUrl) {
35
- super(agentRef)
34
+ constructor (uiEvent, uiEventTimestamp, currentRouteKnown, currentUrl) {
35
+ super()
36
36
  this.belType = NODE_TYPE.INTERACTION
37
37
  this.trigger = uiEvent
38
38
  this.start = uiEventTimestamp
@@ -83,7 +83,6 @@ export class Interaction extends BelNode {
83
83
  #finish (customEndTime = 0) {
84
84
  clearTimeout(this.cancellationTimer)
85
85
  this.end = Math.max(this.domTimestamp, this.historyTimestamp, customEndTime)
86
- this.customAttributes = { ...this.info.jsAttributes, ...this.customAttributes } // attrs specific to this interaction should have precedence over the general custom attrs
87
86
  this.status = INTERACTION_STATUS.FIN
88
87
 
89
88
  // Run all the callbacks awaiting this interaction to finish.
@@ -116,9 +115,15 @@ export class Interaction extends BelNode {
116
115
  get firstContentfulPaint () {}
117
116
  get navTiming () {}
118
117
 
119
- serialize (firstStartTimeOfPayload) {
118
+ /**
119
+ * Serializes (BEL) the interaction data for transmission.
120
+ * @param {Number} firstStartTimeOfPayload timestamp
121
+ * @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.
122
+ * @returns {String} A string that is the serialized representation of this interaction.
123
+ */
124
+ serialize (firstStartTimeOfPayload, agentRef) {
120
125
  const isFirstIxnOfPayload = firstStartTimeOfPayload === undefined
121
- const addString = getAddStringContext(this.obfuscator)
126
+ const addString = getAddStringContext(agentRef.runtime.obfuscator)
122
127
  const nodeList = []
123
128
  let ixnType
124
129
  if (this.trigger === IPL_TRIGGER_NAME) ixnType = INTERACTION_TYPE.INITIAL_PAGE_LOAD
@@ -145,12 +150,13 @@ export class Interaction extends BelNode {
145
150
  addString(this.nodeId),
146
151
  nullable(this.firstPaint, numeric, true) + nullable(this.firstContentfulPaint, numeric)
147
152
  ]
148
- const allAttachedNodes = addCustomAttributes(this.customAttributes || {}, addString) // start with all custom attributes
149
- if (this.info.atts) allAttachedNodes.push('a,' + addString(this.info.atts)) // add apm provided attributes
153
+ const customAttributes = { ...agentRef.info.jsAttributes, ...this.customAttributes } // attrs specific to this interaction should have precedence over the general custom attrs
154
+ const allAttachedNodes = addCustomAttributes(customAttributes || {}, addString) // start with all custom attributes
155
+ if (agentRef.info.atts) allAttachedNodes.push('a,' + addString(agentRef.info.atts)) // add apm provided attributes
150
156
  /* Querypack encoder+decoder quirkiness:
151
157
  - 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.)
152
158
  - 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. */
153
- 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
159
+ 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
154
160
 
155
161
  fields[1] = numeric(allAttachedNodes.length)
156
162
  nodeList.push(fields)
@@ -33,7 +33,7 @@ export function Interaction (eventName, timestamp, url, routeName, onFinished, a
33
33
  attrs.initialPageURL = initialLocation
34
34
  attrs.oldRoute = routeName
35
35
  attrs.newURL = url
36
- attrs.oldURL = eventName === 'initialPageLoad' ? document.referrer : url
36
+ 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.
37
37
  attrs.custom = {}
38
38
  attrs.store = {}
39
39
  }
@@ -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
 
12
14
  export function setupAgentSession (agentRef) {
13
15
  if (agentRef.runtime.session) return agentRef.runtime.session // already setup
@@ -24,11 +26,20 @@ export function setupAgentSession (agentRef) {
24
26
 
25
27
  // Retrieve & re-add all of the persisted setCustomAttribute|setUserId k-v from previous page load(s), if any was stored.
26
28
  const customSessionData = agentRef.runtime.session.state.custom
27
- if (customSessionData) {
29
+ if (customSessionData && Object.keys(customSessionData).length) {
28
30
  /** stored attributes from previous page should not take precedence over attributes stored on this page via API before the page load */
29
- agentRef.info.jsAttributes = { ...customSessionData, ...agentRef.info.jsAttributes }
31
+ agentRef.info = mergeInfo({
32
+ ...agentRef.info,
33
+ jsAttributes: {
34
+ ...customSessionData,
35
+ ...agentRef.info.jsAttributes
36
+ }
37
+ })
30
38
  }
31
39
 
40
+ /** track changes to the jsAttributes field over time for aiding with harvest mechanics */
41
+ agentRef.runtime.jsAttributesMetadata = trackObjectAttributeSize(agentRef.info, 'jsAttributes')
42
+
32
43
  const sharedEE = ee.get(agentRef.agentIdentifier)
33
44
 
34
45
  // 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
 
22
23
  export class AggregateBase extends FeatureBase {
23
24
  /**
@@ -31,6 +32,11 @@ export class AggregateBase extends FeatureBase {
31
32
  this.checkConfiguration(agentRef)
32
33
  this.doOnceForAllAggregate(agentRef)
33
34
 
35
+ /** @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 */
36
+ this.customAttributesAreSeparate = false
37
+ /** @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 */
38
+ this.canHarvestEarly = true // this is set to false in derived classes that need to block early harvests, like ajax under certain conditions
39
+
34
40
  this.harvestOpts = {} // features aggregate classes can define custom opts for when their harvest is called
35
41
 
36
42
  const agentEntityGuid = this.agentRef?.runtime?.appMetadata?.agents?.[0]?.entityGuid
@@ -60,17 +66,31 @@ export class AggregateBase extends FeatureBase {
60
66
  // Jserror and Metric features uses a singleton EventAggregator instead of a regular EventBuffer.
61
67
  case FEATURE_NAMES.jserrors:
62
68
  case FEATURE_NAMES.metrics:
63
- this.events = this.agentRef.sharedAggregator ??= new EventStoreManager(this.agentRef, EventAggregator, entityGuid, 'shared_aggregator')
69
+ this.events = this.agentRef.sharedAggregator ??= new EventStoreManager(this.agentRef, EventAggregator, entityGuid, { featureName: 'shared_aggregator' })
64
70
  break
65
71
  /** 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.
66
72
  This was necessary to prevent race cond. issues where the event buffer was checked before the feature could "block" itself.
67
73
  Its easier to just keep an empty event buffer in place. */
68
74
  default:
69
- this.events = new EventStoreManager(this.agentRef, EventBuffer, entityGuid, this.featureName)
75
+ this.events = new EventStoreManager(this.agentRef, EventBuffer, entityGuid, this)
70
76
  break
71
77
  }
72
78
  }
73
79
 
80
+ /**
81
+ * 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.
82
+ * If conditions are met, a new harvest will be triggered immediately.
83
+ * @returns void
84
+ */
85
+ decideEarlyHarvest () {
86
+ if (!this.canHarvestEarly) return
87
+ const estimatedSize = this.events.byteSize() + (this.customAttributesAreSeparate ? this.agentRef.runtime.jsAttributesMetadata.bytes : 0)
88
+ if (estimatedSize > IDEAL_PAYLOAD_SIZE) {
89
+ this.agentRef.runtime.harvester.triggerHarvestFor(this)
90
+ this.reportSupportabilityMetric(`${this.featureName}/Harvest/Early/Seen`, estimatedSize)
91
+ }
92
+ }
93
+
74
94
  /**
75
95
  * New handler for waiting for multiple flags. Useful when expecting multiple flags simultaneously (ex. stn vs sr)
76
96
  * @param {string[]} flagNames
@@ -12,10 +12,13 @@ export class EventBuffer {
12
12
  #rawBytesBackup
13
13
 
14
14
  /**
15
- * @param {number} maxPayloadSize
15
+ * Creates an event buffer that can hold feature-processed events.
16
+ * @param {Number} maxPayloadSize The maximum size of the payload that can be stored in this buffer.
17
+ * @param {Object} [featureAgg] - the feature aggregate instance
16
18
  */
17
- constructor (maxPayloadSize = MAX_PAYLOAD_SIZE) {
19
+ constructor (maxPayloadSize = MAX_PAYLOAD_SIZE, featureAgg) {
18
20
  this.maxPayloadSize = maxPayloadSize
21
+ this.featureAgg = featureAgg
19
22
  }
20
23
 
21
24
  isEmpty () {
@@ -41,9 +44,15 @@ export class EventBuffer {
41
44
  */
42
45
  add (event) {
43
46
  const addSize = stringify(event)?.length || 0 // (estimate) # of bytes a directly stringified event it would take to send
44
- if (this.#rawBytes + addSize > this.maxPayloadSize) return false
47
+ if (this.#rawBytes + addSize > this.maxPayloadSize) {
48
+ const smTag = inject => `EventBuffer/${inject}/Dropped/Bytes`
49
+ this.featureAgg?.reportSupportabilityMetric(smTag(this.featureAgg.featureName), addSize) // bytes dropped for this feature will aggregate with this metric tag
50
+ this.featureAgg?.reportSupportabilityMetric(smTag('Combined'), addSize) // all bytes dropped across all features will aggregate with this metric tag
51
+ return false
52
+ }
45
53
  this.#buffer.push(event)
46
54
  this.#rawBytes += addSize
55
+ this.featureAgg?.decideEarlyHarvest() // check if we should harvest early with new data
47
56
  return true
48
57
  }
49
58
 
@@ -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
 
@@ -42,7 +42,7 @@ export class EventStoreManager {
42
42
  /** if the target is the container agent, SHARE the default storage -- otherwise create a new event store */
43
43
  const eventStorage = (isContainerAgentTarget(this.entityManager.get(targetEntityGuid), this.agentRef))
44
44
  ? this.appStorageMap.get(DEFAULT_KEY)
45
- : new this.StorageClass()
45
+ : new this.StorageClass(MAX_PAYLOAD_SIZE, this.featureAgg)
46
46
  this.appStorageMap.set(targetEntityGuid, eventStorage)
47
47
  }
48
48
 
@@ -75,7 +75,7 @@ export class EventStoreManager {
75
75
  drained: !!activatedFeatures?.[this.agentRef.agentIdentifier],
76
76
  type: 'data',
77
77
  name: 'buffer',
78
- feature: this.featureName,
78
+ feature: this.featureAgg.featureName,
79
79
  data: event
80
80
  })
81
81
  return this.#getEventStore(targetEntityGuid).add(event)