@newrelic/browser-agent 1.271.0 → 1.273.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 (123) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cjs/common/aggregate/aggregator.js +23 -30
  3. package/dist/cjs/common/aggregate/event-aggregator.js +84 -0
  4. package/dist/cjs/common/config/init.js +8 -4
  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/harvest/harvest-scheduler.js +1 -1
  8. package/dist/cjs/common/harvest/harvest.js +1 -5
  9. package/dist/cjs/common/harvest/types.js +0 -1
  10. package/dist/cjs/features/ajax/aggregate/index.js +52 -62
  11. package/dist/cjs/features/generic_events/aggregate/index.js +57 -36
  12. package/dist/cjs/features/generic_events/instrument/index.js +1 -1
  13. package/dist/cjs/features/jserrors/aggregate/index.js +23 -69
  14. package/dist/cjs/features/logging/aggregate/index.js +52 -59
  15. package/dist/cjs/features/metrics/aggregate/index.js +8 -5
  16. package/dist/cjs/features/page_view_timing/aggregate/index.js +8 -25
  17. package/dist/cjs/features/session_replay/aggregate/index.js +11 -10
  18. package/dist/cjs/features/session_replay/shared/recorder-events.js +2 -2
  19. package/dist/cjs/features/session_trace/aggregate/index.js +77 -88
  20. package/dist/cjs/features/session_trace/aggregate/trace/storage.js +22 -13
  21. package/dist/cjs/features/soft_navigations/aggregate/index.js +10 -20
  22. package/dist/cjs/features/soft_navigations/instrument/index.js +5 -9
  23. package/dist/cjs/features/spa/aggregate/index.js +10 -26
  24. package/dist/cjs/features/utils/aggregate-base.js +37 -0
  25. package/dist/cjs/features/utils/event-buffer.js +36 -87
  26. package/dist/cjs/features/utils/instrument-base.js +3 -3
  27. package/dist/cjs/loaders/features/features.js +13 -1
  28. package/dist/esm/common/aggregate/aggregator.js +23 -30
  29. package/dist/esm/common/aggregate/event-aggregator.js +78 -0
  30. package/dist/esm/common/config/init.js +8 -4
  31. package/dist/esm/common/constants/env.cdn.js +1 -1
  32. package/dist/esm/common/constants/env.npm.js +1 -1
  33. package/dist/esm/common/harvest/harvest-scheduler.js +1 -1
  34. package/dist/esm/common/harvest/harvest.js +1 -5
  35. package/dist/esm/common/harvest/types.js +0 -1
  36. package/dist/esm/features/ajax/aggregate/index.js +53 -62
  37. package/dist/esm/features/generic_events/aggregate/index.js +57 -36
  38. package/dist/esm/features/generic_events/instrument/index.js +1 -1
  39. package/dist/esm/features/jserrors/aggregate/index.js +24 -70
  40. package/dist/esm/features/logging/aggregate/index.js +52 -59
  41. package/dist/esm/features/metrics/aggregate/index.js +8 -5
  42. package/dist/esm/features/page_view_timing/aggregate/index.js +9 -26
  43. package/dist/esm/features/session_replay/aggregate/index.js +12 -11
  44. package/dist/esm/features/session_replay/shared/recorder-events.js +2 -2
  45. package/dist/esm/features/session_trace/aggregate/index.js +77 -88
  46. package/dist/esm/features/session_trace/aggregate/trace/storage.js +22 -13
  47. package/dist/esm/features/soft_navigations/aggregate/index.js +11 -21
  48. package/dist/esm/features/soft_navigations/instrument/index.js +5 -9
  49. package/dist/esm/features/spa/aggregate/index.js +11 -27
  50. package/dist/esm/features/utils/aggregate-base.js +37 -0
  51. package/dist/esm/features/utils/event-buffer.js +36 -88
  52. package/dist/esm/features/utils/instrument-base.js +3 -3
  53. package/dist/esm/loaders/features/features.js +12 -0
  54. package/dist/types/common/aggregate/aggregator.d.ts +4 -6
  55. package/dist/types/common/aggregate/aggregator.d.ts.map +1 -1
  56. package/dist/types/common/aggregate/event-aggregator.d.ts +26 -0
  57. package/dist/types/common/aggregate/event-aggregator.d.ts.map +1 -0
  58. package/dist/types/common/config/init.d.ts.map +1 -1
  59. package/dist/types/common/harvest/harvest.d.ts.map +1 -1
  60. package/dist/types/common/harvest/types.d.ts +1 -4
  61. package/dist/types/common/harvest/types.d.ts.map +1 -1
  62. package/dist/types/features/ajax/aggregate/index.d.ts +2 -10
  63. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  64. package/dist/types/features/generic_events/aggregate/index.d.ts +5 -11
  65. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  66. package/dist/types/features/generic_events/instrument/index.d.ts.map +1 -1
  67. package/dist/types/features/jserrors/aggregate/index.d.ts +4 -7
  68. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  69. package/dist/types/features/logging/aggregate/index.d.ts +10 -28
  70. package/dist/types/features/logging/aggregate/index.d.ts.map +1 -1
  71. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  72. package/dist/types/features/page_view_timing/aggregate/index.d.ts +1 -9
  73. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  74. package/dist/types/features/session_replay/aggregate/index.d.ts +3 -4
  75. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  76. package/dist/types/features/session_replay/shared/recorder-events.d.ts +1 -1
  77. package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -1
  78. package/dist/types/features/session_replay/shared/recorder.d.ts +1 -1
  79. package/dist/types/features/session_trace/aggregate/index.d.ts +17 -19
  80. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  81. package/dist/types/features/session_trace/aggregate/trace/storage.d.ts +10 -6
  82. package/dist/types/features/session_trace/aggregate/trace/storage.d.ts.map +1 -1
  83. package/dist/types/features/soft_navigations/aggregate/index.d.ts +3 -9
  84. package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
  85. package/dist/types/features/soft_navigations/instrument/index.d.ts.map +1 -1
  86. package/dist/types/features/spa/aggregate/index.d.ts +2 -3
  87. package/dist/types/features/spa/aggregate/index.d.ts.map +1 -1
  88. package/dist/types/features/utils/aggregate-base.d.ts +14 -0
  89. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  90. package/dist/types/features/utils/event-buffer.d.ts +19 -56
  91. package/dist/types/features/utils/event-buffer.d.ts.map +1 -1
  92. package/dist/types/loaders/features/features.d.ts +3 -0
  93. package/dist/types/loaders/features/features.d.ts.map +1 -1
  94. package/package.json +3 -2
  95. package/src/common/aggregate/aggregator.js +22 -32
  96. package/src/common/aggregate/event-aggregator.js +76 -0
  97. package/src/common/config/init.js +6 -2
  98. package/src/common/harvest/harvest-scheduler.js +1 -1
  99. package/src/common/harvest/harvest.js +1 -5
  100. package/src/common/harvest/types.js +0 -1
  101. package/src/features/ajax/aggregate/index.js +60 -67
  102. package/src/features/generic_events/aggregate/index.js +48 -38
  103. package/src/features/generic_events/instrument/index.js +2 -0
  104. package/src/features/jserrors/aggregate/index.js +21 -77
  105. package/src/features/logging/aggregate/index.js +46 -60
  106. package/src/features/metrics/aggregate/index.js +6 -4
  107. package/src/features/page_view_timing/aggregate/index.js +9 -30
  108. package/src/features/session_replay/aggregate/index.js +10 -14
  109. package/src/features/session_replay/shared/recorder-events.js +2 -2
  110. package/src/features/session_trace/aggregate/index.js +64 -73
  111. package/src/features/session_trace/aggregate/trace/storage.js +25 -14
  112. package/src/features/soft_navigations/aggregate/index.js +11 -22
  113. package/src/features/soft_navigations/instrument/index.js +6 -9
  114. package/src/features/spa/aggregate/index.js +12 -27
  115. package/src/features/utils/aggregate-base.js +39 -0
  116. package/src/features/utils/event-buffer.js +36 -83
  117. package/src/features/utils/instrument-base.js +3 -3
  118. package/src/loaders/features/features.js +13 -0
  119. package/dist/cjs/features/ajax/aggregate/chunk.js +0 -51
  120. package/dist/esm/features/ajax/aggregate/chunk.js +0 -44
  121. package/dist/types/features/ajax/aggregate/chunk.d.ts +0 -8
  122. package/dist/types/features/ajax/aggregate/chunk.d.ts.map +0 -1
  123. package/src/features/ajax/aggregate/chunk.js +0 -52
@@ -8,13 +8,11 @@ import { handle } from '../../../common/event-emitter/handle'
8
8
  import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
9
9
  import { setDenyList, shouldCollectEvent } from '../../../common/deny-list/deny-list'
10
10
  import { FEATURE_NAME } from '../constants'
11
- import { FEATURE_NAMES } from '../../../loaders/features/features'
11
+ import { FEATURE_NAMES, FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
12
12
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
13
13
  import { AggregateBase } from '../../utils/aggregate-base'
14
14
  import { parseGQL } from './gql'
15
- import { getNREUMInitializedAgent } from '../../../common/window/nreum'
16
- import Chunk from './chunk'
17
- import { EventBuffer } from '../../utils/event-buffer'
15
+ import { nullable, numeric, getAddStringContext, addCustomAttributes } from '../../../common/serialize/bel-serializer'
18
16
 
19
17
  export class Aggregate extends AggregateBase {
20
18
  static featureName = FEATURE_NAME
@@ -25,31 +23,30 @@ export class Aggregate extends AggregateBase {
25
23
  const harvestTimeSeconds = agentRef.init.ajax.harvestTimeSeconds || 10
26
24
  setDenyList(agentRef.runtime.denyList)
27
25
 
28
- this.ajaxEvents = new EventBuffer()
29
- this.spaAjaxEvents = {}
26
+ this.underSpaEvents = {}
30
27
  const classThis = this
31
28
 
32
29
  // --- v Used by old spa feature
33
30
  this.ee.on('interactionDone', (interaction, wasSaved) => {
34
- if (!this.spaAjaxEvents[interaction.id]?.hasData) return
31
+ if (!this.underSpaEvents[interaction.id]) return
35
32
 
36
33
  if (!wasSaved) { // if the ixn was saved, then its ajax reqs are part of the payload whereas if it was discarded, it should still be harvested in the ajax feature itself
37
- this.ajaxEvents.merge(this.spaAjaxEvents[interaction.id])
34
+ this.underSpaEvents[interaction.id].forEach((item) => this.events.add(item))
38
35
  }
39
- delete this.spaAjaxEvents[interaction.id]
36
+ delete this.underSpaEvents[interaction.id]
40
37
  })
41
38
  // --- ^
42
39
  // --- v Used by new soft nav
43
- registerHandler('returnAjax', event => this.ajaxEvents.add(event), this.featureName, this.ee)
40
+ registerHandler('returnAjax', event => this.events.add(event), this.featureName, this.ee)
44
41
  // --- ^
45
42
  registerHandler('xhr', function () { // the EE-drain system not only switches "this" but also passes a new EventContext with info. Should consider platform refactor to another system which passes a mutable context around separately and predictably to avoid problems like this.
46
43
  classThis.storeXhr(...arguments, this) // this switches the context back to the class instance while passing the NR context as an argument -- see "ctx" in storeXhr
47
44
  }, this.featureName, this.ee)
48
45
 
49
46
  this.waitForFlags(([])).then(() => {
50
- const scheduler = new HarvestScheduler('events', {
51
- onFinished: this.onEventsHarvestFinished.bind(this),
52
- getPayload: this.prepareHarvest.bind(this)
47
+ const scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
48
+ onFinished: (result) => this.postHarvestCleanup(result.sent && result.retry),
49
+ getPayload: (options) => this.makeHarvestPayload(options.retry)
53
50
  }, this)
54
51
  scheduler.startTimer(harvestTimeSeconds)
55
52
  this.drain()
@@ -69,10 +66,11 @@ export class Aggregate extends AggregateBase {
69
66
 
70
67
  const shouldCollect = shouldCollectEvent(params)
71
68
  const shouldOmitAjaxMetrics = this.agentRef.init.feature_flags?.includes('ajax_metrics_deny_list')
69
+ const jserrorsInUse = Boolean(this.agentRef.features?.[FEATURE_NAMES.jserrors])
72
70
 
73
- // store for timeslice metric (harvested by jserrors feature)
74
- if (shouldCollect || !shouldOmitAjaxMetrics) {
75
- this.agentRef.sharedAggregator.store('xhr', hash, params, metrics)
71
+ // Report ajax timeslice metric (to be harvested by jserrors feature, but only if it's running).
72
+ if (jserrorsInUse && (shouldCollect || !shouldOmitAjaxMetrics)) {
73
+ this.agentRef.sharedAggregator.add('xhr', hash, params, metrics)
76
74
  }
77
75
 
78
76
  if (!shouldCollect) {
@@ -119,66 +117,61 @@ export class Aggregate extends AggregateBase {
119
117
  })
120
118
  if (event.gql) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length], undefined, FEATURE_NAMES.metrics, this.ee)
121
119
 
122
- const softNavInUse = Boolean(getNREUMInitializedAgent(this.agentIdentifier)?.features?.[FEATURE_NAMES.softNav])
120
+ const softNavInUse = Boolean(this.agentRef.features?.[FEATURE_NAMES.softNav])
123
121
  if (softNavInUse) { // For newer soft nav (when running), pass the event to it for evaluation -- either part of an interaction or is given back
124
122
  handle('ajax', [event], undefined, FEATURE_NAMES.softNav, this.ee)
125
123
  } else if (ctx.spaNode) { // For old spa (when running), if the ajax happened inside an interaction, hold it until the interaction finishes
126
124
  const interactionId = ctx.spaNode.interaction.id
127
- this.spaAjaxEvents[interactionId] ??= new EventBuffer()
128
- this.spaAjaxEvents[interactionId].add(event)
125
+ this.underSpaEvents[interactionId] ??= []
126
+ this.underSpaEvents[interactionId].push(event)
129
127
  } else {
130
- this.ajaxEvents.add(event)
128
+ this.events.add(event)
131
129
  }
132
130
  }
133
131
 
134
- prepareHarvest (options) {
135
- options = options || {}
136
- if (this.ajaxEvents.buffer.length === 0) return null
137
-
138
- const payload = this.#getPayload(this.ajaxEvents.buffer)
139
- const payloadObjs = []
140
-
141
- for (let i = 0; i < payload.length; i++) payloadObjs.push({ body: { e: payload[i] } })
142
-
143
- if (options.retry) this.ajaxEvents.hold()
144
- else this.ajaxEvents.clear()
145
-
146
- return payloadObjs
147
- }
148
-
149
- onEventsHarvestFinished (result) {
150
- if (result.retry && this.ajaxEvents.held.hasData) this.ajaxEvents.unhold()
151
- else this.ajaxEvents.held.clear()
152
- }
153
-
154
- #getPayload (events, numberOfChunks) {
155
- numberOfChunks = numberOfChunks || 1
156
- const payload = []
157
- const chunkSize = events.length / numberOfChunks
158
- const eventChunks = splitChunks.call(this, events, chunkSize)
159
- let tooBig = false
160
- for (let i = 0; i < eventChunks.length; i++) {
161
- const currentChunk = eventChunks[i]
162
- if (currentChunk.tooBig) {
163
- if (currentChunk.events.length > 1) {
164
- tooBig = true
165
- break // if the payload is too big BUT is made of more than 1 event, we can split it down again
166
- }
167
- // Otherwise, if it consists of one sole event, we do not send it (discarded) since we cannot break it apart any further.
168
- } else {
169
- payload.push(currentChunk.payload)
170
- }
171
- }
172
- // Check if the current payload string is too big, if so then run getPayload again with more buckets.
173
- return tooBig ? this.#getPayload(events, ++numberOfChunks) : payload
174
-
175
- function splitChunks (arr, chunkSize) {
176
- chunkSize = chunkSize || arr.length
177
- const chunks = []
178
- for (let i = 0, len = arr.length; i < len; i += chunkSize) {
179
- chunks.push(new Chunk(arr.slice(i, i + chunkSize), this))
132
+ serializer (eventBuffer) {
133
+ const addString = getAddStringContext(this.agentIdentifier)
134
+ let payload = 'bel.7;'
135
+
136
+ for (let i = 0; i < eventBuffer.length; i++) {
137
+ const event = eventBuffer[i]
138
+ const fields = [
139
+ numeric(event.startTime),
140
+ numeric(event.endTime - event.startTime),
141
+ numeric(0), // callbackEnd
142
+ numeric(0), // no callbackDuration for non-SPA events
143
+ addString(event.method),
144
+ numeric(event.status),
145
+ addString(event.domain),
146
+ addString(event.path),
147
+ numeric(event.requestSize),
148
+ numeric(event.responseSize),
149
+ event.type === 'fetch' ? 1 : '',
150
+ addString(0), // nodeId
151
+ nullable(event.spanId, addString, true) + // guid
152
+ nullable(event.traceId, addString, true) + // traceId
153
+ nullable(event.spanTimestamp, numeric, false) // timestamp
154
+ ]
155
+
156
+ let insert = '2,'
157
+
158
+ // Since configuration objects (like info) are created new each time they are set, we have to grab the current pointer to the attr object here.
159
+ const jsAttributes = this.agentRef.info.jsAttributes
160
+
161
+ // add custom attributes
162
+ // gql decorators are added as custom attributes to alleviate need for new BEL schema
163
+ const attrParts = addCustomAttributes({ ...(jsAttributes || {}), ...(event.gql || {}) }, addString)
164
+ fields.unshift(numeric(attrParts.length))
165
+
166
+ insert += fields.join(',')
167
+ if (attrParts && attrParts.length > 0) {
168
+ insert += ';' + attrParts.join(';')
180
169
  }
181
- return chunks
170
+ if ((i + 1) < eventBuffer.length) insert += ';'
171
+
172
+ payload += insert
182
173
  }
174
+
175
+ return payload
183
176
  }
184
177
  }
@@ -12,9 +12,9 @@ import { warn } from '../../../common/util/console'
12
12
  import { now } from '../../../common/timing/now'
13
13
  import { registerHandler } from '../../../common/event-emitter/register-handler'
14
14
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
15
- import { EventBuffer } from '../../utils/event-buffer'
16
15
  import { applyFnToProps } from '../../../common/util/traverse'
17
16
  import { IDEAL_PAYLOAD_SIZE } from '../../../common/constants/agent-constants'
17
+ import { FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
18
18
  import { UserActionsAggregator } from './user-actions/user-actions-aggregator'
19
19
  import { isIFrameWindow } from '../../../common/dom/iframe'
20
20
 
@@ -27,7 +27,6 @@ export class Aggregate extends AggregateBase {
27
27
  this.harvestTimeSeconds = agentRef.init.generic_events.harvestTimeSeconds
28
28
 
29
29
  this.referrerUrl = (isBrowserScope && document.referrer) ? cleanURL(document.referrer) : undefined
30
- this.events = new EventBuffer()
31
30
 
32
31
  this.waitForFlags(['ins']).then(([ins]) => {
33
32
  if (!ins) {
@@ -36,8 +35,6 @@ export class Aggregate extends AggregateBase {
36
35
  return
37
36
  }
38
37
 
39
- const preHarvestMethods = []
40
-
41
38
  if (agentRef.init.page_action.enabled) {
42
39
  registerHandler('api-addPageAction', (timestamp, name, attributes) => {
43
40
  this.addEvent({
@@ -55,10 +52,11 @@ export class Aggregate extends AggregateBase {
55
52
  }, this.featureName, this.ee)
56
53
  }
57
54
 
55
+ let addUserAction
58
56
  if (isBrowserScope && agentRef.init.user_actions.enabled) {
59
57
  this.userActionAggregator = new UserActionsAggregator()
60
58
 
61
- this.addUserAction = (aggregatedUserAction) => {
59
+ addUserAction = (aggregatedUserAction) => {
62
60
  try {
63
61
  /** The aggregator process only returns an event when it is "done" aggregating -
64
62
  * so we still need to validate that an event was given to this method before we try to add */
@@ -87,21 +85,50 @@ export class Aggregate extends AggregateBase {
87
85
 
88
86
  registerHandler('ua', (evt) => {
89
87
  /** the processor will return the previously aggregated event if it has been completed by processing the current event */
90
- this.addUserAction(this.userActionAggregator.process(evt))
88
+ addUserAction(this.userActionAggregator.process(evt))
91
89
  }, this.featureName, this.ee)
90
+ }
92
91
 
93
- preHarvestMethods.push((options = {}) => {
94
- /** send whatever UserActions have been aggregated up to this point
95
- * if we are in a final harvest. By accessing the aggregationEvent, the aggregation is then force-cleared */
96
- if (options.isFinalHarvest) this.addUserAction(this.userActionAggregator.aggregationEvent)
97
- })
92
+ /**
93
+ * is it worth complicating the agent and skipping the POs for single repeating queries? maybe,
94
+ * but right now it was less desirable simply because it is a nice benefit of populating the event buffer
95
+ * immediately as events happen for payload evaluation purposes and that becomes a little more chaotic
96
+ * with an arbitrary query method. note: eventTypes: [...types] does not support the 'buffered' flag so we have
97
+ * to create up to two PO's here.
98
+ */
99
+ const performanceTypesToCapture = [...(agentRef.init.performance.capture_marks ? ['mark'] : []), ...(agentRef.init.performance.capture_measures ? ['measure'] : [])]
100
+ if (performanceTypesToCapture.length) {
101
+ try {
102
+ performanceTypesToCapture.forEach(type => {
103
+ if (PerformanceObserver.supportedEntryTypes.includes(type)) {
104
+ const observer = new PerformanceObserver((list) => {
105
+ list.getEntries().forEach(entry => {
106
+ try {
107
+ this.addEvent({
108
+ eventType: 'BrowserPerformance',
109
+ timestamp: Math.floor(agentRef.runtime.timeKeeper.correctRelativeTimestamp(entry.startTime)),
110
+ entryName: entry.name,
111
+ entryDuration: entry.duration,
112
+ entryType: type,
113
+ ...(entry.detail && { entryDetail: entry.detail })
114
+ })
115
+ } catch (err) {
116
+ }
117
+ })
118
+ })
119
+ observer.observe({ buffered: true, type })
120
+ }
121
+ })
122
+ } catch (err) {
123
+ // Something failed in our set up, likely the browser does not support PO's... do nothing
124
+ }
98
125
  }
99
126
 
100
- this.harvestScheduler = new HarvestScheduler('ins', { onFinished: (...args) => this.onHarvestFinished(...args) }, this)
101
- this.harvestScheduler.harvest.on('ins', (...args) => {
102
- preHarvestMethods.forEach(fn => fn(...args))
103
- return this.onHarvestStarted(...args)
104
- })
127
+ this.harvestScheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
128
+ onFinished: (result) => this.postHarvestCleanup(result.sent && result.retry),
129
+ onUnload: () => addUserAction?.(this.userActionAggregator.aggregationEvent)
130
+ }, this)
131
+ this.harvestScheduler.harvest.on(FEATURE_TO_ENDPOINT[this.featureName], (options) => this.makeHarvestPayload(options.retry))
105
132
  this.harvestScheduler.startTimer(this.harvestTimeSeconds, 0)
106
133
 
107
134
  this.drain()
@@ -155,34 +182,17 @@ export class Aggregate extends AggregateBase {
155
182
  this.checkEventLimits()
156
183
  }
157
184
 
158
- onHarvestStarted (options) {
159
- const { userAttributes, atts } = this.agentRef.info
160
- if (!this.events.hasData) return
161
- var payload = ({
162
- qs: {
163
- ua: userAttributes,
164
- at: atts
165
- },
166
- body: applyFnToProps(
167
- { ins: this.events.buffer },
168
- this.obfuscator.obfuscateString.bind(this.obfuscator), 'string'
169
- )
170
- })
171
-
172
- if (options.retry) this.events.hold()
173
- else this.events.clear()
174
-
175
- return payload
185
+ serializer (eventBuffer) {
186
+ return applyFnToProps({ ins: eventBuffer }, this.obfuscator.obfuscateString.bind(this.obfuscator), 'string')
176
187
  }
177
188
 
178
- onHarvestFinished (result) {
179
- if (result && result?.sent && result?.retry && this.events.held.hasData) this.events.unhold()
180
- else this.events.held.clear()
189
+ queryStringsBuilder () {
190
+ return { ua: this.agentRef.info.userAttributes, at: this.agentRef.info.atts }
181
191
  }
182
192
 
183
193
  checkEventLimits () {
184
194
  // check if we've reached any harvest limits...
185
- if (this.events.bytes > IDEAL_PAYLOAD_SIZE) {
195
+ if (this.events.byteSize() > IDEAL_PAYLOAD_SIZE) {
186
196
  this.ee.emit(SUPPORTABILITY_METRIC_CHANNEL, ['GenericEvents/Harvest/Max/Seen'])
187
197
  this.harvestScheduler.runHarvest()
188
198
  }
@@ -14,6 +14,8 @@ export class Instrument extends InstrumentBase {
14
14
  super(agentRef, FEATURE_NAME, auto)
15
15
  const genericEventSourceConfigs = [
16
16
  agentRef.init.page_action.enabled,
17
+ agentRef.init.performance.capture_marks,
18
+ agentRef.init.performance.capture_measures,
17
19
  agentRef.init.user_actions.enabled
18
20
  // other future generic event source configs to go here, like M&Ms, PageResouce, etc.
19
21
  ]
@@ -15,9 +15,8 @@ import { handle } from '../../../common/event-emitter/handle'
15
15
  import { globalScope } from '../../../common/constants/runtime'
16
16
 
17
17
  import { FEATURE_NAME } from '../constants'
18
- import { FEATURE_NAMES } from '../../../loaders/features/features'
18
+ import { FEATURE_NAMES, FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
19
19
  import { AggregateBase } from '../../utils/aggregate-base'
20
- import { getNREUMInitializedAgent } from '../../../common/window/nreum'
21
20
  import { now } from '../../../common/timing/now'
22
21
  import { applyFnToProps } from '../../../common/util/traverse'
23
22
  import { evaluateInternalError } from './internal-errors'
@@ -36,7 +35,6 @@ export class Aggregate extends AggregateBase {
36
35
  this.observedAt = {}
37
36
  this.pageviewReported = {}
38
37
  this.bufferedErrorsUnderSpa = {}
39
- this.currentBody = undefined
40
38
  this.errorOnPage = false
41
39
 
42
40
  // this will need to change to match whatever ee we use in the instrument
@@ -48,12 +46,15 @@ export class Aggregate extends AggregateBase {
48
46
  this.onSoftNavNotification(interactionId, wasFinished, softNavAttrs), this.featureName, this.ee) // when an ixn is done or cancelled
49
47
 
50
48
  const harvestTimeSeconds = agentRef.init.jserrors.harvestTimeSeconds || 10
49
+ const aggregatorTypes = ['err', 'ierr', 'xhr'] // the types in EventAggregator this feature cares about
51
50
 
52
51
  // 0 == off, 1 == on
53
52
  this.waitForFlags(['err']).then(([errFlag]) => {
54
53
  if (errFlag) {
55
- const scheduler = new HarvestScheduler('jserrors', { onFinished: (...args) => this.onHarvestFinished(...args) }, this)
56
- scheduler.harvest.on('jserrors', (...args) => this.onHarvestStarted(...args))
54
+ const scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
55
+ onFinished: (result) => this.postHarvestCleanup(result.sent && result.retry, { aggregatorTypes })
56
+ }, this)
57
+ scheduler.harvest.on(FEATURE_TO_ENDPOINT[this.featureName], (options) => this.makeHarvestPayload(options.retry, { aggregatorTypes }))
57
58
  scheduler.startTimer(harvestTimeSeconds)
58
59
  this.drain()
59
60
  } else {
@@ -63,58 +64,24 @@ export class Aggregate extends AggregateBase {
63
64
  })
64
65
  }
65
66
 
66
- onHarvestStarted (options) {
67
- // this gets rid of dependency in AJAX module
68
- var body = applyFnToProps(
69
- this.agentRef.sharedAggregator.take(['err', 'ierr', 'xhr']),
70
- this.obfuscator.obfuscateString.bind(this.obfuscator), 'string'
71
- )
72
-
73
- if (options.retry) {
74
- this.currentBody = body
75
- }
76
-
77
- var payload = { body, qs: {} }
78
- var releaseIds = stringify(this.agentRef.runtime.releaseIds)
67
+ serializer (aggregatorTypeToBucketsMap) {
68
+ return applyFnToProps(aggregatorTypeToBucketsMap, this.obfuscator.obfuscateString.bind(this.obfuscator), 'string')
69
+ }
79
70
 
80
- if (releaseIds !== '{}') {
81
- payload.qs.ri = releaseIds
82
- }
71
+ queryStringsBuilder (aggregatorTakeReturnedData) {
72
+ const qs = {}
73
+ const releaseIds = stringify(this.agentRef.runtime.releaseIds)
74
+ if (releaseIds !== '{}') qs.ri = releaseIds
83
75
 
84
- if (body && body.err && body.err.length) {
85
- this.#runCrossFeatureChecks(body.err)
76
+ if (aggregatorTakeReturnedData?.err?.length) {
86
77
  if (!this.errorOnPage) {
87
- payload.qs.pve = '1'
78
+ qs.pve = '1'
88
79
  this.errorOnPage = true
89
80
  }
81
+ // For assurance, erase any `hasReplay` flag from all errors if replay is not recording, not-yet imported, or not running at all.
82
+ if (!this.agentRef.features?.[FEATURE_NAMES.sessionReplay]?.featAggregate?.replayIsActive()) aggregatorTakeReturnedData.err.forEach(error => delete error.params.hasReplay)
90
83
  }
91
-
92
- return payload
93
- }
94
-
95
- onHarvestFinished (result) {
96
- if (result.retry && this.currentBody) {
97
- Object.entries(this.currentBody || {}).forEach(([key, value]) => {
98
- for (var i = 0; i < value.length; i++) {
99
- var bucket = value[i]
100
- var name = this.getBucketName(key, bucket.params, bucket.custom)
101
- this.agentRef.sharedAggregator.merge(key, name, bucket.metrics, bucket.params, bucket.custom)
102
- }
103
- })
104
- this.currentBody = null
105
- }
106
- }
107
-
108
- nameHash (params) {
109
- return stringHashCode(`${params.exceptionClass}_${params.message}_${params.stack_trace || params.browser_stack_hash}`)
110
- }
111
-
112
- getBucketName (objType, params, customParams) {
113
- if (objType === 'xhr') {
114
- return stringHashCode(stringify(params)) + ':' + stringHashCode(stringify(customParams))
115
- }
116
-
117
- return this.nameHash(params) + ':' + stringHashCode(stringify(customParams))
84
+ return qs
118
85
  }
119
86
 
120
87
  /**
@@ -230,7 +197,7 @@ export class Aggregate extends AggregateBase {
230
197
  params._interactionNodeId = err.__newrelic[this.agentIdentifier].interactionNodeId
231
198
  }
232
199
 
233
- const softNavInUse = Boolean(getNREUMInitializedAgent(this.agentIdentifier)?.features[FEATURE_NAMES.softNav])
200
+ const softNavInUse = Boolean(this.agentRef.features?.[FEATURE_NAMES.softNav])
234
201
  // Note: the following are subject to potential race cond wherein if the other feature aren't fully initialized, it'll be treated as there being no associated interaction.
235
202
  // They each will also tack on their respective properties to the params object as part of the decision flow.
236
203
  if (softNavInUse) handle('jserror', [params, time], undefined, FEATURE_NAMES.softNav, this.ee)
@@ -267,7 +234,7 @@ export class Aggregate extends AggregateBase {
267
234
 
268
235
  const jsAttributesHash = stringHashCode(stringify(allCustomAttrs))
269
236
  const aggregateHash = bucketHash + ':' + jsAttributesHash
270
- this.agentRef.sharedAggregator.store(type, aggregateHash, params, newMetrics, allCustomAttrs)
237
+ this.events.add(type, aggregateHash, params, newMetrics, allCustomAttrs)
271
238
 
272
239
  function setCustom (key, val) {
273
240
  allCustomAttrs[key] = (val && typeof val === 'object' ? stringify(val) : val)
@@ -297,7 +264,7 @@ export class Aggregate extends AggregateBase {
297
264
  var jsAttributesHash = stringHashCode(stringify(allCustomAttrs))
298
265
  var aggregateHash = hash + ':' + jsAttributesHash
299
266
 
300
- this.agentRef.sharedAggregator.store(item[0], aggregateHash, params, item[3], allCustomAttrs)
267
+ this.events.add(item[0], aggregateHash, params, item[3], allCustomAttrs)
301
268
 
302
269
  function setCustom ([key, val]) {
303
270
  allCustomAttrs[key] = (val && typeof val === 'object' ? stringify(val) : val)
@@ -314,27 +281,4 @@ export class Aggregate extends AggregateBase {
314
281
  )
315
282
  delete this.bufferedErrorsUnderSpa[interactionId] // wipe the list of jserrors so they aren't duplicated by another call to the same id
316
283
  }
317
-
318
- /**
319
- * Dispatches a cross-feature communication event to allow other
320
- * features to provide flags and data that can be used to mutation
321
- * to the payload and to allow features to know about a feature
322
- * harvest happening.
323
- * @param {any[]} errors Array of errors from the payload body
324
- */
325
- #runCrossFeatureChecks (errors) {
326
- const errorHashes = errors.map(error => error.params.stackHash)
327
- const crossFeatureData = {
328
- errorHashes
329
- }
330
- this.ee.emit(`cfc.${this.featureName}`, [crossFeatureData])
331
-
332
- let hasReplayFlag = errors.find(err => err.params.hasReplay)
333
- if (hasReplayFlag && !crossFeatureData.hasReplay) {
334
- // Some errors have `hasReplay` and a replay is not being recorded
335
- errors.forEach(error => {
336
- delete error.params.hasReplay
337
- })
338
- }
339
- }
340
284
  }
@@ -10,23 +10,19 @@ import { Log } from '../shared/log'
10
10
  import { isValidLogLevel } from '../shared/utils'
11
11
  import { applyFnToProps } from '../../../common/util/traverse'
12
12
  import { MAX_PAYLOAD_SIZE } from '../../../common/constants/agent-constants'
13
- import { EventBuffer } from '../../utils/event-buffer'
13
+ import { FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
14
14
 
15
15
  export class Aggregate extends AggregateBase {
16
16
  static featureName = FEATURE_NAME
17
17
  constructor (agentRef) {
18
18
  super(agentRef, FEATURE_NAME)
19
-
20
- /** held logs before sending */
21
- this.bufferedLogs = new EventBuffer()
22
-
23
19
  this.harvestTimeSeconds = agentRef.init.logging.harvestTimeSeconds
24
20
 
25
21
  this.waitForFlags([]).then(() => {
26
- this.scheduler = new HarvestScheduler('browser/logs', {
27
- onFinished: this.onHarvestFinished.bind(this),
22
+ this.scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
23
+ onFinished: (result) => this.postHarvestCleanup(result.sent && result.retry),
28
24
  retryDelay: this.harvestTimeSeconds,
29
- getPayload: this.prepareHarvest.bind(this),
25
+ getPayload: (options) => this.makeHarvestPayload(options.retry),
30
26
  raw: true
31
27
  }, this)
32
28
  /** emitted by instrument class (wrapped loggers) or the api methods directly */
@@ -69,65 +65,55 @@ export class Aggregate extends AggregateBase {
69
65
  )
70
66
  const logBytes = log.message.length + stringify(log.attributes).length + log.level.length + 10 // timestamp == 10 chars
71
67
 
72
- if (!this.bufferedLogs.canMerge(logBytes)) {
73
- if (this.bufferedLogs.hasData) {
74
- handle(SUPPORTABILITY_METRIC_CHANNEL, ['Logging/Harvest/Early/Seen', this.bufferedLogs.bytes + logBytes])
75
- this.scheduler.runHarvest({})
76
- if (logBytes < MAX_PAYLOAD_SIZE) this.bufferedLogs.add(log)
77
- } else {
78
- handle(SUPPORTABILITY_METRIC_CHANNEL, ['Logging/Harvest/Failed/Seen', logBytes])
79
- warn(31, log.message.slice(0, 25) + '...')
80
- }
68
+ const failToHarvestMessage = 'Logging/Harvest/Failed/Seen'
69
+ if (logBytes > MAX_PAYLOAD_SIZE) { // cannot possibly send this, even with an empty buffer
70
+ handle(SUPPORTABILITY_METRIC_CHANNEL, [failToHarvestMessage, logBytes])
71
+ warn(31, log.message.slice(0, 25) + '...')
81
72
  return
82
73
  }
83
74
 
84
- this.bufferedLogs.add(log)
85
- }
86
-
87
- prepareHarvest (options = {}) {
88
- if (this.blocked || !this.bufferedLogs.hasData) return
89
- /** These attributes are evaluated and dropped at ingest processing time and do not get stored on NRDB */
90
- const unbilledAttributes = {
91
- 'instrumentation.provider': 'browser',
92
- 'instrumentation.version': this.agentRef.runtime.version,
93
- 'instrumentation.name': this.agentRef.runtime.loaderType
94
- }
95
- /** see https://source.datanerd.us/agents/rum-specs/blob/main/browser/Log for logging spec */
96
- const payload = {
97
- qs: {
98
- browser_monitoring_key: this.agentRef.info.licenseKey
99
- },
100
- body: [{
101
- common: {
102
- /** Attributes in the `common` section are added to `all` logs generated in the payload */
103
- attributes: {
104
- 'entity.guid': this.agentRef.runtime.appMetadata?.agents?.[0]?.entityGuid, // browser entity guid as provided from RUM response
105
- session: this.agentRef.runtime.session?.state.value || '0', // The session ID that we generate and keep across page loads
106
- hasReplay: this.agentRef.runtime.session?.state.sessionReplayMode === 1, // True if a session replay recording is running
107
- hasTrace: this.agentRef.runtime.session?.state.sessionTraceMode === 1, // True if a session trace recording is running
108
- ptid: this.agentRef.runtime.ptid, // page trace id
109
- appId: this.agentRef.info.applicationID, // Application ID from info object,
110
- standalone: Boolean(this.agentRef.info.sa), // copy paste (true) vs APM (false)
111
- agentVersion: this.agentRef.runtime.version, // browser agent version
112
- ...unbilledAttributes
113
- }
114
- },
115
- /** logs section contains individual unique log entries */
116
- logs: applyFnToProps(
117
- this.bufferedLogs.buffer,
118
- this.obfuscator.obfuscateString.bind(this.obfuscator), 'string'
119
- )
120
- }]
75
+ if (this.events.wouldExceedMaxSize(logBytes)) {
76
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['Logging/Harvest/Early/Seen', this.events.bytes + logBytes])
77
+ this.scheduler.runHarvest() // force a harvest to try adding again
121
78
  }
122
79
 
123
- if (options.retry) this.bufferedLogs.hold()
124
- else this.bufferedLogs.clear()
80
+ if (!this.events.add(log)) { // still failed after a harvest attempt despite not being too large would mean harvest failed with options.retry
81
+ handle(SUPPORTABILITY_METRIC_CHANNEL, [failToHarvestMessage, logBytes])
82
+ warn(31, log.message.slice(0, 25) + '...')
83
+ }
84
+ }
125
85
 
126
- return payload
86
+ serializer (eventBuffer) {
87
+ const sessionEntity = this.agentRef.runtime.session
88
+ return [{
89
+ common: {
90
+ /** Attributes in the `common` section are added to `all` logs generated in the payload */
91
+ attributes: {
92
+ 'entity.guid': this.agentRef.runtime.appMetadata?.agents?.[0]?.entityGuid, // browser entity guid as provided from RUM response
93
+ ...(sessionEntity && {
94
+ session: sessionEntity.state.value || '0', // The session ID that we generate and keep across page loads
95
+ hasReplay: sessionEntity.state.sessionReplayMode === 1, // True if a session replay recording is running
96
+ hasTrace: sessionEntity.state.sessionTraceMode === 1 // True if a session trace recording is running
97
+ }),
98
+ ptid: this.agentRef.runtime.ptid, // page trace id
99
+ appId: this.agentRef.info.applicationID, // Application ID from info object,
100
+ standalone: Boolean(this.agentRef.info.sa), // copy paste (true) vs APM (false)
101
+ agentVersion: this.agentRef.runtime.version, // browser agent version
102
+ // The following 3 attributes are evaluated and dropped at ingest processing time and do not get stored on NRDB:
103
+ 'instrumentation.provider': 'browser',
104
+ 'instrumentation.version': this.agentRef.runtime.version,
105
+ 'instrumentation.name': this.agentRef.runtime.loaderType
106
+ }
107
+ },
108
+ /** logs section contains individual unique log entries */
109
+ logs: applyFnToProps(
110
+ eventBuffer,
111
+ this.obfuscator.obfuscateString.bind(this.obfuscator), 'string'
112
+ )
113
+ }]
127
114
  }
128
115
 
129
- onHarvestFinished (result) {
130
- if (result.retry) this.bufferedLogs.unhold()
131
- else this.bufferedLogs.held.clear()
116
+ queryStringsBuilder () {
117
+ return { browser_monitoring_key: this.agentRef.info.licenseKey }
132
118
  }
133
119
  }