@newrelic/browser-agent 1.277.0 → 1.278.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 (133) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/common/aggregate/event-aggregator.js +1 -1
  3. package/dist/cjs/common/config/init.js +1 -10
  4. package/dist/cjs/common/config/runtime.js +2 -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/harvest/harvester.js +255 -0
  8. package/dist/cjs/common/harvest/types.js +5 -21
  9. package/dist/cjs/features/ajax/aggregate/index.js +2 -11
  10. package/dist/cjs/features/generic_events/aggregate/index.js +3 -10
  11. package/dist/cjs/features/jserrors/aggregate/index.js +3 -14
  12. package/dist/cjs/features/logging/aggregate/index.js +4 -12
  13. package/dist/cjs/features/metrics/aggregate/index.js +7 -15
  14. package/dist/cjs/features/page_view_event/aggregate/index.js +46 -48
  15. package/dist/cjs/features/page_view_timing/aggregate/index.js +0 -9
  16. package/dist/cjs/features/session_replay/aggregate/index.js +21 -43
  17. package/dist/cjs/features/session_replay/instrument/index.js +2 -1
  18. package/dist/cjs/features/session_replay/shared/recorder.js +6 -6
  19. package/dist/cjs/features/session_trace/aggregate/index.js +9 -24
  20. package/dist/cjs/features/session_trace/aggregate/trace/storage.js +8 -2
  21. package/dist/cjs/features/soft_navigations/aggregate/index.js +4 -11
  22. package/dist/cjs/features/spa/aggregate/index.js +7 -10
  23. package/dist/cjs/features/utils/aggregate-base.js +66 -27
  24. package/dist/cjs/features/utils/event-buffer.js +0 -1
  25. package/dist/cjs/features/utils/event-store-manager.js +109 -0
  26. package/dist/cjs/features/utils/instrument-base.js +1 -10
  27. package/dist/cjs/loaders/features/features.js +16 -10
  28. package/dist/cjs/loaders/micro-agent.js +1 -0
  29. package/dist/esm/common/aggregate/event-aggregator.js +1 -1
  30. package/dist/esm/common/config/init.js +1 -10
  31. package/dist/esm/common/config/runtime.js +2 -1
  32. package/dist/esm/common/constants/env.cdn.js +1 -1
  33. package/dist/esm/common/constants/env.npm.js +1 -1
  34. package/dist/esm/common/harvest/harvester.js +249 -0
  35. package/dist/esm/common/harvest/types.js +5 -21
  36. package/dist/esm/features/ajax/aggregate/index.js +3 -12
  37. package/dist/esm/features/generic_events/aggregate/index.js +3 -10
  38. package/dist/esm/features/jserrors/aggregate/index.js +4 -15
  39. package/dist/esm/features/logging/aggregate/index.js +4 -12
  40. package/dist/esm/features/metrics/aggregate/index.js +7 -15
  41. package/dist/esm/features/page_view_event/aggregate/index.js +46 -48
  42. package/dist/esm/features/page_view_timing/aggregate/index.js +1 -10
  43. package/dist/esm/features/session_replay/aggregate/index.js +22 -44
  44. package/dist/esm/features/session_replay/instrument/index.js +2 -1
  45. package/dist/esm/features/session_replay/shared/recorder.js +6 -6
  46. package/dist/esm/features/session_trace/aggregate/index.js +9 -24
  47. package/dist/esm/features/session_trace/aggregate/trace/storage.js +8 -2
  48. package/dist/esm/features/soft_navigations/aggregate/index.js +5 -12
  49. package/dist/esm/features/spa/aggregate/index.js +8 -11
  50. package/dist/esm/features/utils/aggregate-base.js +66 -27
  51. package/dist/esm/features/utils/event-buffer.js +0 -1
  52. package/dist/esm/features/utils/event-store-manager.js +103 -0
  53. package/dist/esm/features/utils/instrument-base.js +1 -10
  54. package/dist/esm/loaders/features/features.js +15 -9
  55. package/dist/esm/loaders/micro-agent.js +1 -0
  56. package/dist/types/common/aggregate/event-aggregator.d.ts +1 -1
  57. package/dist/types/common/aggregate/event-aggregator.d.ts.map +1 -1
  58. package/dist/types/common/config/init.d.ts.map +1 -1
  59. package/dist/types/common/config/runtime.d.ts.map +1 -1
  60. package/dist/types/common/harvest/harvester.d.ts +16 -0
  61. package/dist/types/common/harvest/harvester.d.ts.map +1 -0
  62. package/dist/types/common/harvest/types.d.ts +8 -45
  63. package/dist/types/common/harvest/types.d.ts.map +1 -1
  64. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  65. package/dist/types/features/generic_events/aggregate/index.d.ts +0 -3
  66. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  67. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  68. package/dist/types/features/logging/aggregate/index.d.ts +0 -3
  69. package/dist/types/features/logging/aggregate/index.d.ts.map +1 -1
  70. package/dist/types/features/metrics/aggregate/index.d.ts +1 -1
  71. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  72. package/dist/types/features/page_view_event/aggregate/index.d.ts +6 -2
  73. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  74. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  75. package/dist/types/features/session_replay/aggregate/index.d.ts +12 -15
  76. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  77. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  78. package/dist/types/features/session_trace/aggregate/index.d.ts +0 -5
  79. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  80. package/dist/types/features/session_trace/aggregate/trace/storage.d.ts +8 -5
  81. package/dist/types/features/session_trace/aggregate/trace/storage.d.ts.map +1 -1
  82. package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
  83. package/dist/types/features/spa/aggregate/index.d.ts +0 -1
  84. package/dist/types/features/spa/aggregate/index.d.ts.map +1 -1
  85. package/dist/types/features/utils/aggregate-base.d.ts +12 -7
  86. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  87. package/dist/types/features/utils/event-buffer.d.ts +1 -2
  88. package/dist/types/features/utils/event-buffer.d.ts.map +1 -1
  89. package/dist/types/features/utils/event-store-manager.d.ts +43 -0
  90. package/dist/types/features/utils/event-store-manager.d.ts.map +1 -0
  91. package/dist/types/features/utils/instrument-base.d.ts +0 -1
  92. package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
  93. package/dist/types/loaders/features/features.d.ts +15 -12
  94. package/dist/types/loaders/features/features.d.ts.map +1 -1
  95. package/dist/types/loaders/micro-agent.d.ts.map +1 -1
  96. package/package.json +6 -6
  97. package/src/common/aggregate/event-aggregator.js +1 -1
  98. package/src/common/config/init.js +9 -10
  99. package/src/common/config/runtime.js +2 -1
  100. package/src/common/harvest/__mocks__/harvester.js +6 -0
  101. package/src/common/harvest/harvester.js +230 -0
  102. package/src/common/harvest/types.js +5 -21
  103. package/src/features/ajax/aggregate/index.js +3 -14
  104. package/src/features/generic_events/aggregate/index.js +3 -13
  105. package/src/features/jserrors/aggregate/index.js +4 -11
  106. package/src/features/logging/aggregate/index.js +4 -12
  107. package/src/features/metrics/aggregate/index.js +5 -12
  108. package/src/features/page_view_event/aggregate/index.js +38 -38
  109. package/src/features/page_view_timing/aggregate/index.js +1 -12
  110. package/src/features/session_replay/aggregate/index.js +19 -42
  111. package/src/features/session_replay/instrument/index.js +1 -1
  112. package/src/features/session_replay/shared/recorder.js +6 -6
  113. package/src/features/session_trace/aggregate/index.js +8 -25
  114. package/src/features/session_trace/aggregate/trace/storage.js +5 -2
  115. package/src/features/soft_navigations/aggregate/index.js +4 -12
  116. package/src/features/spa/aggregate/index.js +8 -11
  117. package/src/features/utils/aggregate-base.js +59 -27
  118. package/src/features/utils/event-buffer.js +0 -1
  119. package/src/features/utils/event-store-manager.js +101 -0
  120. package/src/features/utils/instrument-base.js +2 -8
  121. package/src/loaders/features/features.js +16 -9
  122. package/src/loaders/micro-agent.js +1 -0
  123. package/dist/cjs/common/harvest/harvest-scheduler.js +0 -168
  124. package/dist/cjs/common/harvest/harvest.js +0 -295
  125. package/dist/esm/common/harvest/harvest-scheduler.js +0 -160
  126. package/dist/esm/common/harvest/harvest.js +0 -286
  127. package/dist/types/common/harvest/harvest-scheduler.d.ts +0 -50
  128. package/dist/types/common/harvest/harvest-scheduler.d.ts.map +0 -1
  129. package/dist/types/common/harvest/harvest.d.ts +0 -65
  130. package/dist/types/common/harvest/harvest.d.ts.map +0 -1
  131. package/src/common/harvest/__mocks__/harvest.js +0 -13
  132. package/src/common/harvest/harvest-scheduler.js +0 -166
  133. package/src/common/harvest/harvest.js +0 -282
@@ -1,7 +1,6 @@
1
1
  import { record as recorder } from 'rrweb'
2
2
  import { stringify } from '../../../common/util/stringify'
3
3
  import { AVG_COMPRESSION, CHECKOUT_MS, QUERY_PARAM_PADDING, RRWEB_EVENT_TYPES, SR_EVENT_EMITTER_TYPES } from '../constants'
4
- import { getConfigurationValue } from '../../../common/config/init'
5
4
  import { RecorderEvents } from './recorder-events'
6
5
  import { MODE } from '../../../common/session/constants'
7
6
  import { stylesheetEvaluator } from './stylesheet-evaluator'
@@ -10,6 +9,7 @@ import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
10
9
  import { FEATURE_NAMES } from '../../../loaders/features/features'
11
10
  import { buildNRMetaNode } from './utils'
12
11
  import { IDEAL_PAYLOAD_SIZE } from '../../../common/constants/agent-constants'
12
+ import { AggregateBase } from '../../utils/aggregate-base'
13
13
 
14
14
  export class Recorder {
15
15
  /** Each page mutation or event will be stored (raw) in this array. This array will be cleared on each harvest */
@@ -36,7 +36,7 @@ export class Recorder {
36
36
  /** The parent class that instantiated the recorder */
37
37
  this.parent = parent
38
38
  /** A flag that can be set to false by failing conversions to stop the fetching process */
39
- this.shouldFix = getConfigurationValue(this.parent.agentIdentifier, 'session_replay.fix_stylesheets')
39
+ this.shouldFix = this.parent.agentRef.init.session_replay.fix_stylesheets
40
40
  /** The method to stop recording. This defaults to a noop, but is overwritten once the recording library is imported and initialized */
41
41
  this.stopRecording = () => { /* no-op until set by rrweb initializer */ }
42
42
  }
@@ -73,7 +73,7 @@ export class Recorder {
73
73
  /** Begin recording using configured recording lib */
74
74
  startRecording () {
75
75
  this.recording = true
76
- const { block_class, ignore_class, mask_text_class, block_selector, mask_input_options, mask_text_selector, mask_all_inputs, inline_images, collect_fonts } = getConfigurationValue(this.parent.agentIdentifier, 'session_replay')
76
+ const { block_class, ignore_class, mask_text_class, block_selector, mask_input_options, mask_text_selector, mask_all_inputs, inline_images, collect_fonts } = this.parent.agentRef.init.session_replay
77
77
  const customMasker = (text, element) => {
78
78
  try {
79
79
  if (typeof element?.type === 'string' && element.type.toLowerCase() !== 'password' && (element?.dataset?.nrUnmask !== undefined || element?.classList?.contains('nr-unmask'))) return text
@@ -151,7 +151,7 @@ export class Recorder {
151
151
  store (event, isCheckout) {
152
152
  if (!event) return
153
153
 
154
- if (!this.parent.scheduler && this.#preloaded.length) this.currentBufferTarget = this.#preloaded[this.#preloaded.length - 1]
154
+ if (!(this.parent instanceof AggregateBase) && this.#preloaded.length) this.currentBufferTarget = this.#preloaded[this.#preloaded.length - 1]
155
155
  else this.currentBufferTarget = this.#events
156
156
 
157
157
  if (this.parent.blocked) return
@@ -193,8 +193,8 @@ export class Recorder {
193
193
  // it will send immediately. This often happens on the first snapshot, which can be significantly larger than the other payloads.
194
194
  if (((event.type === RRWEB_EVENT_TYPES.FullSnapshot && this.currentBufferTarget.hasMeta) || payloadSize > IDEAL_PAYLOAD_SIZE) && this.parent.mode === MODE.FULL) {
195
195
  // if we've made it to the ideal size of ~64kb before the interval timer, we should send early.
196
- if (this.parent.scheduler) {
197
- this.parent.scheduler.runHarvest()
196
+ if (this.parent instanceof AggregateBase) {
197
+ this.parent.agentRef.runtime.harvester.triggerHarvestFor(this.parent)
198
198
  } else {
199
199
  // we are still in "preload" and it triggered a "stop point". Make a new set, which will get pointed at on next cycle
200
200
  this.#preloaded.push(new RecorderEvents())
@@ -1,5 +1,4 @@
1
1
  import { registerHandler } from '../../../common/event-emitter/register-handler'
2
- import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
3
2
  import { FEATURE_NAME } from '../constants'
4
3
  import { AggregateBase } from '../../utils/aggregate-base'
5
4
  import { TraceStorage } from './trace/storage'
@@ -7,7 +6,6 @@ import { obj as encodeObj } from '../../../common/url/encode'
7
6
  import { globalScope } from '../../../common/constants/runtime'
8
7
  import { MODE, SESSION_EVENTS } from '../../../common/session/constants'
9
8
  import { applyFnToProps } from '../../../common/util/traverse'
10
- import { FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
11
9
  import { cleanURL } from '../../../common/url/clean-url'
12
10
 
13
11
  const ERROR_MODE_SECONDS_WINDOW = 30 * 1000 // sliding window of nodes to track when simply monitoring (but not harvesting) in error mode
@@ -18,8 +16,8 @@ export class Aggregate extends AggregateBase {
18
16
 
19
17
  constructor (agentRef) {
20
18
  super(agentRef, FEATURE_NAME)
19
+ this.harvestOpts.raw = true
21
20
 
22
- this.harvestTimeSeconds = agentRef.init.session_trace.harvestTimeSeconds || 30
23
21
  /** Tied to the entitlement flag response from BCS. Will short circuit operations of the agg if false */
24
22
  this.entitled = undefined
25
23
  /** A flag used to decide if the 30 node threshold should be ignored on the first harvest to ensure sending on the first payload */
@@ -28,7 +26,8 @@ export class Aggregate extends AggregateBase {
28
26
  this.harvesting = false
29
27
  /** TraceStorage is the mechanism that holds, normalizes and aggregates ST nodes. It will be accessed and purged when harvests occur */
30
28
  this.events = new TraceStorage(this)
31
- /** This agg needs information about sampling (sts) and entitlements (st) to make the appropriate decisions on running */
29
+
30
+ /* This agg needs information about sampling (sts) and entitlements (st) to make the appropriate decisions on running */
32
31
  this.waitForFlags(['sts', 'st'])
33
32
  .then(([stMode, stEntitled]) => this.initialize(stMode, stEntitled))
34
33
  }
@@ -36,7 +35,8 @@ export class Aggregate extends AggregateBase {
36
35
  /** Sets up event listeners, and initializes this module to run in the correct "mode". Can be triggered from a few places, but makes an effort to only set up listeners once */
37
36
  initialize (stMode, stEntitled, ignoreSession) {
38
37
  this.entitled ??= stEntitled
39
- if (this.blocked || !this.entitled) return this.deregisterDrain()
38
+ if (!this.entitled) this.blocked = true
39
+ if (this.blocked) return this.deregisterDrain()
40
40
 
41
41
  if (!this.initialized) {
42
42
  this.initialized = true
@@ -55,7 +55,7 @@ export class Aggregate extends AggregateBase {
55
55
  // this will only have an effect if ST is NOT already in full mode
56
56
  if (this.mode !== MODE.FULL && (sessionState.sessionReplayMode === MODE.FULL || sessionState.sessionTraceMode === MODE.FULL)) this.switchToFull()
57
57
  // if another page's session entity has expired, or another page has transitioned to off and this one hasn't... we can just abort straight away here
58
- if (this.sessionId !== sessionState.value || (eventType === 'cross-tab' && this.scheduler?.started && sessionState.sessionTraceMode === MODE.OFF)) this.abort(2)
58
+ if (this.sessionId !== sessionState.value || (eventType === 'cross-tab' && sessionState.sessionTraceMode === MODE.OFF)) this.abort(2)
59
59
  })
60
60
 
61
61
  if (typeof PerformanceNavigationTiming !== 'undefined') {
@@ -76,13 +76,6 @@ export class Aggregate extends AggregateBase {
76
76
 
77
77
  this.timeKeeper ??= this.agentRef.runtime.timeKeeper
78
78
 
79
- this.scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
80
- onFinished: (result) => this.postHarvestCleanup(result.sent && result.retry),
81
- retryDelay: this.harvestTimeSeconds,
82
- getPayload: (options) => this.makeHarvestPayload(options.retry),
83
- raw: true
84
- }, this)
85
-
86
79
  /** The handlers set up by the Inst file */
87
80
  registerHandler('bst', (...args) => this.events.storeEvent(...args), this.featureName, this.ee)
88
81
  registerHandler('bstResource', (...args) => this.events.storeResources(...args), this.featureName, this.ee)
@@ -92,9 +85,7 @@ export class Aggregate extends AggregateBase {
92
85
  registerHandler('trace-jserror', (...args) => this.events.storeErrorAgg(...args), this.featureName, this.ee)
93
86
  registerHandler('pvtAdded', (...args) => this.events.processPVT(...args), this.featureName, this.ee)
94
87
 
95
- /** Only start actually harvesting if running in full mode at init time */
96
- if (this.mode === MODE.FULL) this.startHarvesting()
97
- else {
88
+ if (this.mode !== MODE.FULL) {
98
89
  /** A separate handler for noticing errors, and switching to "full" mode if running in "error" mode */
99
90
  registerHandler('trace-jserror', () => {
100
91
  if (this.mode === MODE.ERROR) this.switchToFull()
@@ -104,13 +95,6 @@ export class Aggregate extends AggregateBase {
104
95
  this.drain()
105
96
  }
106
97
 
107
- /** This module does not auto harvest by default -- it needs to be kicked off. Once this method is called, it will then harvest on an interval */
108
- startHarvesting () {
109
- if (this.scheduler.started || this.blocked) return
110
- this.scheduler.runHarvest()
111
- this.scheduler.startTimer(this.harvestTimeSeconds)
112
- }
113
-
114
98
  preHarvestChecks () {
115
99
  if (this.mode !== MODE.FULL) return // only allow harvest if running in full mode
116
100
  if (!this.timeKeeper?.ready) return // this should likely never happen, but just to be safe, we should never harvest if we cant correct time
@@ -185,8 +169,8 @@ export class Aggregate extends AggregateBase {
185
169
  if (prevMode === MODE.OFF || !this.initialized) return this.initialize(this.mode, this.entitled)
186
170
  if (this.initialized) {
187
171
  this.events.trimSTNs(ERROR_MODE_SECONDS_WINDOW) // up until now, Trace would've been just buffering nodes up to max, which needs to be trimmed to last X seconds
172
+ this.agentRef.runtime.harvester.triggerHarvestFor(this)
188
173
  }
189
- this.startHarvesting()
190
174
  }
191
175
 
192
176
  /** Stop running for the remainder of the page lifecycle */
@@ -194,7 +178,6 @@ export class Aggregate extends AggregateBase {
194
178
  this.blocked = true
195
179
  this.mode = MODE.OFF
196
180
  this.agentRef.runtime.session.write({ sessionTraceMode: this.mode })
197
- this.scheduler?.stopTimer()
198
181
  this.events.clear()
199
182
  }
200
183
  }
@@ -273,7 +273,8 @@ export class TraceStorage {
273
273
  this.storeSTN(new TraceNode('Ajax', metrics.time, metrics.time + metrics.duration, `${params.status} ${params.method}: ${params.host}${params.pathname}`, 'ajax'))
274
274
  }
275
275
 
276
- /* Below are the interface expected & required of whatever storage is used across all features on an individual basis. This allows a common `.events` property on Trace. */
276
+ /* Below are the interface expected & required of whatever storage is used across all features on an individual basis. This allows a common `.events` property on Trace shared with AggregateBase.
277
+ Note that the usage must be in sync with the EventStoreManager class such that AggregateBase.makeHarvestPayload can run the same regardless of which storage class a feature is using. */
277
278
  isEmpty () {
278
279
  return this.nodeCount === 0
279
280
  }
@@ -282,7 +283,9 @@ export class TraceStorage {
282
283
  this.#backupTrace = this.trace
283
284
  }
284
285
 
285
- get = this.takeSTNs
286
+ get () {
287
+ return [{ targetApp: this.parent.agentRef.mainAppKey, data: this.takeSTNs() }]
288
+ }
286
289
 
287
290
  clear () {
288
291
  this.trace = {}
@@ -1,9 +1,8 @@
1
1
  import { handle } from '../../../common/event-emitter/handle'
2
2
  import { registerHandler } from '../../../common/event-emitter/register-handler'
3
- import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
4
3
  import { single } from '../../../common/util/invoke'
5
4
  import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte'
6
- import { FEATURE_NAMES, FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
5
+ import { FEATURE_NAMES } from '../../../loaders/features/features'
7
6
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
8
7
  import { AggregateBase } from '../../utils/aggregate-base'
9
8
  import { API_TRIGGER_NAME, FEATURE_NAME, INTERACTION_STATUS, INTERACTION_TRIGGERS, IPL_TRIGGER_NAME } from '../constants'
@@ -16,7 +15,6 @@ export class Aggregate extends AggregateBase {
16
15
  constructor (agentRef, { domObserver }) {
17
16
  super(agentRef, FEATURE_NAME)
18
17
 
19
- const harvestTimeSeconds = agentRef.init.soft_navigations.harvestTimeSeconds || 10
20
18
  this.interactionsToHarvest = this.events
21
19
  this.domObserver = domObserver
22
20
 
@@ -36,18 +34,12 @@ export class Aggregate extends AggregateBase {
36
34
  this.latestRouteSetByApi = null
37
35
  this.interactionInProgress = null // aside from the "page load" interaction, there can only ever be 1 ongoing at a time
38
36
  this.latestHistoryUrl = null
37
+ this.harvestOpts.beforeUnload = () => this.interactionInProgress?.done() // return any withheld ajax or jserr events so they can be sent with EoL harvest
39
38
 
40
- this.blocked = false
41
39
  this.waitForFlags(['spa']).then(([spaOn]) => {
42
40
  if (spaOn) {
43
41
  this.drain()
44
- const scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
45
- onFinished: (result) => this.postHarvestCleanup(result.sent && result.retry),
46
- getPayload: (options) => this.makeHarvestPayload(options.retry),
47
- retryDelay: harvestTimeSeconds,
48
- onUnload: () => this.interactionInProgress?.done() // return any held ajax or jserr events so they can be sent with EoL harvest
49
- }, this)
50
- scheduler.startTimer(harvestTimeSeconds, 0)
42
+ setTimeout(() => agentRef.runtime.harvester.triggerHarvestFor(this), 0) // send the IPL ixn on next tick, giving some time for any ajax to finish; we may want to just remove this?
51
43
  } else {
52
44
  this.blocked = true // if rum response determines that customer lacks entitlements for spa endpoint, this feature shouldn't harvest
53
45
  this.deregisterDrain()
@@ -136,7 +128,7 @@ export class Aggregate extends AggregateBase {
136
128
  */
137
129
  if (this.interactionInProgress?.isActiveDuring(timestamp)) return this.interactionInProgress
138
130
  let saveIxn
139
- const interactionsBuffer = this.interactionsToHarvest.get()
131
+ const interactionsBuffer = this.interactionsToHarvest.get(this.agentRef.mainAppKey)[0].data
140
132
  for (let idx = interactionsBuffer.length - 1; idx >= 0; idx--) { // reverse search for the latest completed interaction for efficiency
141
133
  const finishedInteraction = interactionsBuffer[idx]
142
134
  if (finishedInteraction.isActiveDuring(timestamp)) {
@@ -10,11 +10,10 @@ import { navTimingValues as navTiming } from '../../../common/timing/nav-timing'
10
10
  import { generateUuid } from '../../../common/ids/unique-id'
11
11
  import { Interaction } from './interaction'
12
12
  import { eventListenerOpts } from '../../../common/event-listener/event-listener-opts'
13
- import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
14
13
  import { Serializer } from './serializer'
15
14
  import { ee } from '../../../common/event-emitter/contextual-ee'
16
15
  import * as CONSTANTS from '../constants'
17
- import { FEATURE_NAMES, FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
16
+ import { FEATURE_NAMES } from '../../../loaders/features/features'
18
17
  import { AggregateBase } from '../../utils/aggregate-base'
19
18
  import { firstContentfulPaint } from '../../../common/vitals/first-contentful-paint'
20
19
  import { firstPaint } from '../../../common/vitals/first-paint'
@@ -46,14 +45,12 @@ export class Aggregate extends AggregateBase {
46
45
  pageLoaded: false,
47
46
  childTime: 0,
48
47
  depth: 0,
49
- harvestTimeSeconds: agentRef.init.spa.harvestTimeSeconds || 10,
50
48
  // The below feature flag is used to disable the SPA ajax fix for specific customers, see https://new-relic.atlassian.net/browse/NR-172169
51
49
  disableSpaFix: (agentRef.init.feature_flags || []).indexOf('disable-spa-fix') > -1
52
50
  }
53
51
  this.spaSerializerClass = new Serializer(this)
54
52
 
55
53
  const classThis = this
56
- let scheduler
57
54
 
58
55
  const baseEE = ee.get(agentRef.agentIdentifier) // <-- parent baseEE
59
56
  const mutationEE = baseEE.get('mutation')
@@ -98,13 +95,10 @@ export class Aggregate extends AggregateBase {
98
95
  // | click ending: | 65 | 50 | | | |
99
96
  // click fn-end | 70 | 0 | 0 | 70 | 20 |
100
97
 
98
+ let harvester
101
99
  this.waitForFlags((['spa'])).then(([spaFlag]) => {
102
100
  if (spaFlag) {
103
- scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
104
- onFinished: (result) => this.postHarvestCleanup(result.sent && result.retry),
105
- getPayload: (options) => this.makeHarvestPayload(options.retry),
106
- retryDelay: state.harvestTimeSeconds
107
- }, this)
101
+ harvester = agentRef.runtime.harvester // since this is after RUM call, PVE would've initialized harvester by now
108
102
  this.drain()
109
103
  } else {
110
104
  this.blocked = true
@@ -725,8 +719,11 @@ export class Aggregate extends AggregateBase {
725
719
  else smCategory = 'Custom'
726
720
  handle(SUPPORTABILITY_METRIC_CHANNEL, [`Spa/Interaction/${smCategory}/Duration/Ms`, Math.max((interaction.root?.end || 0) - (interaction.root?.start || 0), 0)], undefined, FEATURE_NAMES.metrics, baseEE)
727
721
 
728
- scheduler?.scheduleHarvest(0)
729
- if (!scheduler) warn(19)
722
+ if (!harvester) {
723
+ warn(19)
724
+ return
725
+ }
726
+ harvester.triggerHarvestFor(classThis)
730
727
  }
731
728
  }
732
729
 
@@ -5,19 +5,34 @@ import { gosCDN } from '../../common/window/nreum'
5
5
  import { drain } from '../../common/drain/drain'
6
6
  import { activatedFeatures } from '../../common/util/feature-flags'
7
7
  import { Obfuscator } from '../../common/util/obfuscate'
8
- import { EventBuffer } from './event-buffer'
9
8
  import { FEATURE_NAMES } from '../../loaders/features/features'
9
+ import { EventStoreManager } from './event-store-manager'
10
+ import { Harvester } from '../../common/harvest/harvester'
10
11
 
11
12
  export class AggregateBase extends FeatureBase {
12
13
  constructor (agentRef, featureName) {
13
14
  super(agentRef.agentIdentifier, featureName)
14
15
  this.agentRef = agentRef
15
- // Jserror and Metric features uses a singleton EventAggregator instead of a regular EventBuffer.
16
- if ([FEATURE_NAMES.jserrors, FEATURE_NAMES.metrics].includes(this.featureName)) this.events = agentRef.sharedAggregator
17
- // PVE has no need for eventBuffer, and SessionTrace has its own storage mechanism.
18
- else if (![FEATURE_NAMES.pageViewEvent, FEATURE_NAMES.sessionTrace].includes(this.featureName)) this.events = new EventBuffer()
19
16
  this.checkConfiguration(agentRef)
20
- this.obfuscator = agentRef.runtime.obfuscator
17
+ this.doOnceForAllAggregate(agentRef)
18
+
19
+ // This switch needs to be after doOnceForAllAggregate which may new sharedAggregator and reset mainAppKey.
20
+ switch (this.featureName) {
21
+ // PVE has no need for eventBuffer, and SessionTrace + Replay have their own storage mechanisms.
22
+ case FEATURE_NAMES.pageViewEvent:
23
+ case FEATURE_NAMES.sessionTrace:
24
+ case FEATURE_NAMES.sessionReplay:
25
+ break
26
+ // Jserror and Metric features uses a singleton EventAggregator instead of a regular EventBuffer.
27
+ case FEATURE_NAMES.jserrors:
28
+ case FEATURE_NAMES.metrics:
29
+ this.events = agentRef.sharedAggregator
30
+ break
31
+ default:
32
+ this.events = new EventStoreManager(agentRef.mainAppKey, 1)
33
+ break
34
+ }
35
+ this.harvestOpts = {} // features aggregate classes can define custom opts for when their harvest is called
21
36
  }
22
37
 
23
38
  /**
@@ -56,34 +71,40 @@ export class AggregateBase extends FeatureBase {
56
71
  /**
57
72
  * Return harvest payload. A "serializer" function can be defined on a derived class to format the payload.
58
73
  * @param {Boolean} shouldRetryOnFail - harvester flag to backup payload for retry later if harvest request fails; this should be moved to harvester logic
59
- * @returns final payload, or undefined if there are no pending events
74
+ * @param {object|undefined} opts.target - the target app passed onto the event store manager to determine which app's data to return; if none provided, all apps data will be returned
75
+ * @returns {Array} Final payload tagged with their targeting browser app. The value of `payload` can be undefined if there are no pending events for an app. This should be a minimum length of 1.
60
76
  */
61
77
  makeHarvestPayload (shouldRetryOnFail = false, opts = {}) {
62
- if (this.events.isEmpty(opts)) return
78
+ if (this.events.isEmpty(this.harvestOpts, opts.target)) return
63
79
  // Other conditions and things to do when preparing harvest that is required.
64
- if (this.preHarvestChecks && !this.preHarvestChecks()) return
80
+ if (this.preHarvestChecks && !this.preHarvestChecks(opts)) return
65
81
 
66
- if (shouldRetryOnFail) this.events.save(opts)
67
- const returnedData = this.events.get(opts)
68
- // A serializer or formatter assists in creating the payload `body` from stored events on harvest when defined by derived feature class.
69
- const body = this.serializer ? this.serializer(returnedData) : returnedData
70
- this.events.clear(opts)
82
+ if (shouldRetryOnFail) this.events.save(this.harvestOpts, opts.target)
83
+ const returnedDataArr = this.events.get(this.harvestOpts, opts.target)
84
+ if (!returnedDataArr.length) throw new Error('Unexpected problem encountered. There should be at least one app for harvest!')
85
+ this.events.clear(this.harvestOpts, opts.target)
71
86
 
72
- const payload = {
73
- body
74
- }
75
- // Constructs the payload `qs` for relevant features on harvest.
76
- if (this.queryStringsBuilder) payload.qs = this.queryStringsBuilder(returnedData)
77
- return payload
87
+ return returnedDataArr.map(({ targetApp, data }) => {
88
+ // A serializer or formatter assists in creating the payload `body` from stored events on harvest when defined by derived feature class.
89
+ const body = this.serializer ? this.serializer(data) : data
90
+ const payload = {
91
+ body
92
+ }
93
+ // Constructs the payload `qs` for relevant features on harvest.
94
+ if (this.queryStringsBuilder) payload.qs = this.queryStringsBuilder(data)
95
+
96
+ return { targetApp, payload }
97
+ })
78
98
  }
79
99
 
80
100
  /**
81
101
  * Cleanup task after a harvest.
82
- * @param {Boolean} harvestFailed - harvester flag to restore events in main buffer for retry later if request failed
102
+ * @param {object} result - the cbResult object from the harvester's send method
83
103
  */
84
- postHarvestCleanup (harvestFailed = false, opts = {}) {
85
- if (harvestFailed) this.events.reloadSave(opts)
86
- this.events.clearSave(opts)
104
+ postHarvestCleanup (result = {}) {
105
+ const harvestFailed = result.sent && result.retry
106
+ if (harvestFailed) this.events.reloadSave(this.harvestOpts, result.targetApp)
107
+ this.events.clearSave(this.harvestOpts, result.targetApp)
87
108
  }
88
109
 
89
110
  /**
@@ -112,9 +133,20 @@ export class AggregateBase extends FeatureBase {
112
133
  runtime: existingAgent.runtime
113
134
  })
114
135
  }
136
+ }
115
137
 
116
- if (!existingAgent.runtime.obfuscator) {
117
- existingAgent.runtime.obfuscator = new Obfuscator(this.agentIdentifier)
118
- }
138
+ /**
139
+ * These are actions related to shared resources that should be initialized once by whichever feature Aggregate subclass loads first.
140
+ * This method should run after checkConfiguration, which may reset the agent's info/runtime object that is used here.
141
+ */
142
+ doOnceForAllAggregate (agentRef) {
143
+ if (!agentRef.runtime.obfuscator) agentRef.runtime.obfuscator = new Obfuscator(this.agentIdentifier)
144
+ this.obfuscator = agentRef.runtime.obfuscator
145
+
146
+ if (!agentRef.mainAppKey) agentRef.mainAppKey = { licenseKey: agentRef.info.licenseKey, appId: agentRef.info.applicationID }
147
+ // Create a single Aggregator for this agent if DNE yet; to be used by jserror endpoint features.
148
+ if (!agentRef.sharedAggregator) agentRef.sharedAggregator = new EventStoreManager(agentRef.mainAppKey, 2)
149
+
150
+ if (!agentRef.runtime.harvester) agentRef.runtime.harvester = new Harvester(agentRef)
119
151
  }
120
152
  }
@@ -53,7 +53,6 @@ export class EventBuffer {
53
53
 
54
54
  /**
55
55
  * Backup the buffered data and clear the main buffer
56
- * @returns {Array} the events being backed up
57
56
  */
58
57
  save () {
59
58
  this.#bufferBackup = this.#buffer
@@ -0,0 +1,101 @@
1
+ import { EventAggregator } from '../../common/aggregate/event-aggregator'
2
+ import { EventBuffer } from './event-buffer'
3
+
4
+ /**
5
+ * This layer allows multiple browser entity apps, or "target", to each have their own segregated storage instance.
6
+ * The purpose is so the harvester can send data to different apps within the same agent. Each feature should have a manager if it needs this capability.
7
+ */
8
+ export class EventStoreManager {
9
+ /**
10
+ * @param {object} defaultTarget - should contain licenseKey and appId of the main app from NREUM.info at startup
11
+ * @param {1|2} storageChoice - the type of storage to use in this manager; 'EventBuffer' (1), 'EventAggregator' (2)
12
+ */
13
+ constructor (defaultTarget, storageChoice) {
14
+ this.mainApp = defaultTarget
15
+ this.StorageClass = storageChoice === 1 ? EventBuffer : EventAggregator
16
+ this.appStorageMap = new Map()
17
+ this.appStorageMap.set(defaultTarget, new this.StorageClass())
18
+ }
19
+
20
+ // This class must contain an union of all methods from all supported storage classes and conceptualize away the target app argument.
21
+
22
+ /**
23
+ * @param {object} optsIfPresent - exists if called during harvest interval, @see AggregateBase.makeHarvestPayload
24
+ * @param {object} target - specific app's storage to check; if not provided, this method takes into account all apps recorded by this manager
25
+ * @returns {boolean} True if the target's storage is empty, or target does not exist in map (defaults to all storages)
26
+ */
27
+ isEmpty (optsIfPresent, target) {
28
+ if (target) {
29
+ if (!this.appStorageMap.has(target)) return true
30
+ else return this.appStorageMap.get(target).isEmpty(optsIfPresent)
31
+ }
32
+ for (const eventStore of this.appStorageMap.values()) {
33
+ if (!eventStore.isEmpty(optsIfPresent)) return false
34
+ }
35
+ return true
36
+ }
37
+
38
+ /**
39
+ * @param {string} event - the event element to store
40
+ * @param {object} target - the app to store event under; if not provided, this method adds to the main app from NREUM.info
41
+ * @returns {boolean} True if the event was successfully added
42
+ */
43
+ add (event, target) {
44
+ if (target && !this.appStorageMap.has(target)) this.appStorageMap.set(target, new this.StorageClass())
45
+ return this.appStorageMap.get(target || this.mainApp).add(event)
46
+ }
47
+
48
+ /** This is only used by the Metrics feature which has no need to add metric under a different app atm. */
49
+ addMetric (type, name, params, value) {
50
+ return this.appStorageMap.get(this.mainApp).addMetric(type, name, params, value)
51
+ }
52
+
53
+ /**
54
+ * @param {object} optsIfPresent - exists if called during harvest interval, @see AggregateBase.makeHarvestPayload
55
+ * @param {object} target - specific app to fetch; if not provided, this method fetches from all apps
56
+ * @returns {Array} Objects of `data` labeled with their respective `target` app to be sent to
57
+ */
58
+ get (optsIfPresent, target) {
59
+ if (target) return [{ targetApp: target, data: this.appStorageMap.get(target)?.get(optsIfPresent) }]
60
+ const allPayloads = []
61
+ this.appStorageMap.forEach((eventStore, recordedTarget) => {
62
+ allPayloads.push({ targetApp: recordedTarget, data: eventStore.get(optsIfPresent) })
63
+ })
64
+ return allPayloads
65
+ }
66
+
67
+ byteSize (target) {
68
+ return this.appStorageMap.get(target || this.mainApp).byteSize()
69
+ }
70
+
71
+ wouldExceedMaxSize (incomingSize, target) {
72
+ return this.appStorageMap.get(target || this.mainApp).wouldExceedMaxSize(incomingSize)
73
+ }
74
+
75
+ save (optsIfPresent, target) {
76
+ if (target) return this.appStorageMap.get(target)?.save(optsIfPresent)
77
+ this.appStorageMap.forEach((eventStore) => eventStore.save(optsIfPresent))
78
+ }
79
+
80
+ clear (optsIfPresent, target) {
81
+ if (target) return this.appStorageMap.get(target)?.clear(optsIfPresent)
82
+ this.appStorageMap.forEach((eventStore) => eventStore.clear(optsIfPresent))
83
+ }
84
+
85
+ // Unlike the methods above, the following will have a target as they are called by AggregateBase.postHarvestCleanup callback on harvest finish after getting & sending the data.
86
+ reloadSave (optsIfPresent, target) {
87
+ if (!target) { // -- remove this block once the old harvest.js & harvest-schedule.js are deleted!
88
+ this.appStorageMap.forEach((eventStore) => eventStore.reloadSave(optsIfPresent))
89
+ return
90
+ }
91
+ return this.appStorageMap.get(target)?.reloadSave(optsIfPresent)
92
+ }
93
+
94
+ clearSave (optsIfPresent, target) {
95
+ if (!target) { // -- remove this block once the old harvest.js & harvest-schedule.js are deleted!
96
+ this.appStorageMap.forEach((eventStore) => eventStore.clearSave(optsIfPresent))
97
+ return
98
+ }
99
+ return this.appStorageMap.get(target)?.clearSave(optsIfPresent)
100
+ }
101
+ }
@@ -43,7 +43,6 @@ export class InstrumentBase extends FeatureBase {
43
43
  /**
44
44
  * @type {Promise} Assigned immediately after @see importAggregator runs. Serves as a signal for when the inner async fn finishes execution. Useful for features to await
45
45
  * one another if there are inter-features dependencies.
46
- * TODO: This is only used for the SPA feature component tests and should be refactored out.
47
46
  */
48
47
  this.onAggregateImported = undefined
49
48
 
@@ -95,13 +94,6 @@ export class InstrumentBase extends FeatureBase {
95
94
  * it's only responsible for aborting its one specific feature, rather than all.
96
95
  */
97
96
  try {
98
- // Create a single Aggregator for this agent if DNE yet; to be used by jserror endpoint features.
99
- if (!agentRef.sharedAggregator) {
100
- agentRef.sharedAggregator = import(/* webpackChunkName: "shared-aggregator" */ '../../common/aggregate/event-aggregator')
101
- const { EventAggregator } = await agentRef.sharedAggregator
102
- agentRef.sharedAggregator = new EventAggregator()
103
- } else await agentRef.sharedAggregator // if another feature is already importing the aggregator, wait for it to finish
104
-
105
97
  if (!this.#shouldImportAgg(this.featureName, session)) {
106
98
  drain(this.agentIdentifier, this.featureName)
107
99
  loadedSuccessfully(false) // aggregate module isn't loaded at all
@@ -110,6 +102,8 @@ export class InstrumentBase extends FeatureBase {
110
102
  const { lazyFeatureLoader } = await import(/* webpackChunkName: "lazy-feature-loader" */ './lazy-feature-loader')
111
103
  const { Aggregate } = await lazyFeatureLoader(this.featureName, 'aggregate')
112
104
  this.featAggregate = new Aggregate(agentRef, argsObjFromInstrument)
105
+
106
+ agentRef.runtime.harvester.initializedAggregates.push(this.featAggregate) // "subscribe" the feature to future harvest intervals (PVE will start the timer)
113
107
  loadedSuccessfully(true)
114
108
  } catch (e) {
115
109
  warn(34, e)
@@ -1,7 +1,13 @@
1
+ // To reduce build size a bit:
2
+ export const EVENTS = 'events'
3
+ export const JSERRORS = 'jserrors'
4
+ const BLOBS = 'browser/blobs'
5
+ export const RUM = 'rum'
6
+
1
7
  export const FEATURE_NAMES = {
2
8
  ajax: 'ajax',
3
9
  genericEvents: 'generic_events',
4
- jserrors: 'jserrors',
10
+ jserrors: JSERRORS,
5
11
  logging: 'logging',
6
12
  metrics: 'metrics',
7
13
  /**
@@ -35,14 +41,15 @@ export const featurePriority = {
35
41
  }
36
42
 
37
43
  export const FEATURE_TO_ENDPOINT = {
38
- [FEATURE_NAMES.pageViewTiming]: 'events',
39
- [FEATURE_NAMES.ajax]: 'events',
40
- [FEATURE_NAMES.spa]: 'events',
41
- [FEATURE_NAMES.softNav]: 'events',
42
- [FEATURE_NAMES.metrics]: 'jserrors',
43
- [FEATURE_NAMES.jserrors]: 'jserrors',
44
- [FEATURE_NAMES.sessionTrace]: 'browser/blobs',
45
- [FEATURE_NAMES.sessionReplay]: 'browser/blobs',
44
+ [FEATURE_NAMES.pageViewEvent]: RUM,
45
+ [FEATURE_NAMES.pageViewTiming]: EVENTS,
46
+ [FEATURE_NAMES.ajax]: EVENTS,
47
+ [FEATURE_NAMES.spa]: EVENTS,
48
+ [FEATURE_NAMES.softNav]: EVENTS,
49
+ [FEATURE_NAMES.metrics]: JSERRORS,
50
+ [FEATURE_NAMES.jserrors]: JSERRORS,
51
+ [FEATURE_NAMES.sessionTrace]: BLOBS,
52
+ [FEATURE_NAMES.sessionReplay]: BLOBS,
46
53
  [FEATURE_NAMES.logging]: 'browser/logs',
47
54
  [FEATURE_NAMES.genericEvents]: 'ins'
48
55
  }
@@ -64,6 +64,7 @@ export class MicroAgent extends MicroAgentBase {
64
64
  return lazyFeatureLoader(f, 'aggregate')
65
65
  }).then(({ Aggregate }) => {
66
66
  this.features[f] = new Aggregate(this)
67
+ this.runtime.harvester.initializedAggregates.push(this.features[f]) // so that harvester will poll this feature agg on interval
67
68
  }).catch(err => warn(25, err))
68
69
  }
69
70
  })