@newrelic/browser-agent 1.272.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 (115) hide show
  1. package/CHANGELOG.md +8 -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/constants/env.cdn.js +1 -1
  5. package/dist/cjs/common/constants/env.npm.js +1 -1
  6. package/dist/cjs/common/harvest/harvest-scheduler.js +1 -1
  7. package/dist/cjs/common/harvest/harvest.js +1 -5
  8. package/dist/cjs/common/harvest/types.js +0 -1
  9. package/dist/cjs/features/ajax/aggregate/index.js +52 -62
  10. package/dist/cjs/features/generic_events/aggregate/index.js +18 -36
  11. package/dist/cjs/features/jserrors/aggregate/index.js +23 -69
  12. package/dist/cjs/features/logging/aggregate/index.js +52 -59
  13. package/dist/cjs/features/metrics/aggregate/index.js +8 -5
  14. package/dist/cjs/features/page_view_timing/aggregate/index.js +8 -25
  15. package/dist/cjs/features/session_replay/aggregate/index.js +11 -10
  16. package/dist/cjs/features/session_replay/shared/recorder-events.js +2 -2
  17. package/dist/cjs/features/session_trace/aggregate/index.js +77 -88
  18. package/dist/cjs/features/session_trace/aggregate/trace/storage.js +22 -13
  19. package/dist/cjs/features/soft_navigations/aggregate/index.js +10 -20
  20. package/dist/cjs/features/soft_navigations/instrument/index.js +5 -9
  21. package/dist/cjs/features/spa/aggregate/index.js +10 -26
  22. package/dist/cjs/features/utils/aggregate-base.js +37 -0
  23. package/dist/cjs/features/utils/event-buffer.js +36 -87
  24. package/dist/cjs/features/utils/instrument-base.js +3 -3
  25. package/dist/cjs/loaders/features/features.js +13 -1
  26. package/dist/esm/common/aggregate/aggregator.js +23 -30
  27. package/dist/esm/common/aggregate/event-aggregator.js +78 -0
  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/harvest/harvest-scheduler.js +1 -1
  31. package/dist/esm/common/harvest/harvest.js +1 -5
  32. package/dist/esm/common/harvest/types.js +0 -1
  33. package/dist/esm/features/ajax/aggregate/index.js +53 -62
  34. package/dist/esm/features/generic_events/aggregate/index.js +18 -36
  35. package/dist/esm/features/jserrors/aggregate/index.js +24 -70
  36. package/dist/esm/features/logging/aggregate/index.js +52 -59
  37. package/dist/esm/features/metrics/aggregate/index.js +8 -5
  38. package/dist/esm/features/page_view_timing/aggregate/index.js +9 -26
  39. package/dist/esm/features/session_replay/aggregate/index.js +12 -11
  40. package/dist/esm/features/session_replay/shared/recorder-events.js +2 -2
  41. package/dist/esm/features/session_trace/aggregate/index.js +77 -88
  42. package/dist/esm/features/session_trace/aggregate/trace/storage.js +22 -13
  43. package/dist/esm/features/soft_navigations/aggregate/index.js +11 -21
  44. package/dist/esm/features/soft_navigations/instrument/index.js +5 -9
  45. package/dist/esm/features/spa/aggregate/index.js +11 -27
  46. package/dist/esm/features/utils/aggregate-base.js +37 -0
  47. package/dist/esm/features/utils/event-buffer.js +36 -88
  48. package/dist/esm/features/utils/instrument-base.js +3 -3
  49. package/dist/esm/loaders/features/features.js +12 -0
  50. package/dist/types/common/aggregate/aggregator.d.ts +4 -6
  51. package/dist/types/common/aggregate/aggregator.d.ts.map +1 -1
  52. package/dist/types/common/aggregate/event-aggregator.d.ts +26 -0
  53. package/dist/types/common/aggregate/event-aggregator.d.ts.map +1 -0
  54. package/dist/types/common/harvest/harvest.d.ts.map +1 -1
  55. package/dist/types/common/harvest/types.d.ts +1 -4
  56. package/dist/types/common/harvest/types.d.ts.map +1 -1
  57. package/dist/types/features/ajax/aggregate/index.d.ts +2 -10
  58. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  59. package/dist/types/features/generic_events/aggregate/index.d.ts +5 -11
  60. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  61. package/dist/types/features/jserrors/aggregate/index.d.ts +4 -7
  62. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  63. package/dist/types/features/logging/aggregate/index.d.ts +10 -28
  64. package/dist/types/features/logging/aggregate/index.d.ts.map +1 -1
  65. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  66. package/dist/types/features/page_view_timing/aggregate/index.d.ts +1 -9
  67. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  68. package/dist/types/features/session_replay/aggregate/index.d.ts +3 -4
  69. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  70. package/dist/types/features/session_replay/shared/recorder-events.d.ts +1 -1
  71. package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -1
  72. package/dist/types/features/session_replay/shared/recorder.d.ts +1 -1
  73. package/dist/types/features/session_trace/aggregate/index.d.ts +17 -19
  74. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  75. package/dist/types/features/session_trace/aggregate/trace/storage.d.ts +10 -6
  76. package/dist/types/features/session_trace/aggregate/trace/storage.d.ts.map +1 -1
  77. package/dist/types/features/soft_navigations/aggregate/index.d.ts +3 -9
  78. package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
  79. package/dist/types/features/soft_navigations/instrument/index.d.ts.map +1 -1
  80. package/dist/types/features/spa/aggregate/index.d.ts +2 -3
  81. package/dist/types/features/spa/aggregate/index.d.ts.map +1 -1
  82. package/dist/types/features/utils/aggregate-base.d.ts +14 -0
  83. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  84. package/dist/types/features/utils/event-buffer.d.ts +19 -56
  85. package/dist/types/features/utils/event-buffer.d.ts.map +1 -1
  86. package/dist/types/loaders/features/features.d.ts +3 -0
  87. package/dist/types/loaders/features/features.d.ts.map +1 -1
  88. package/package.json +1 -1
  89. package/src/common/aggregate/aggregator.js +22 -32
  90. package/src/common/aggregate/event-aggregator.js +76 -0
  91. package/src/common/harvest/harvest-scheduler.js +1 -1
  92. package/src/common/harvest/harvest.js +1 -5
  93. package/src/common/harvest/types.js +0 -1
  94. package/src/features/ajax/aggregate/index.js +60 -67
  95. package/src/features/generic_events/aggregate/index.js +14 -39
  96. package/src/features/jserrors/aggregate/index.js +21 -77
  97. package/src/features/logging/aggregate/index.js +46 -60
  98. package/src/features/metrics/aggregate/index.js +6 -4
  99. package/src/features/page_view_timing/aggregate/index.js +9 -30
  100. package/src/features/session_replay/aggregate/index.js +10 -14
  101. package/src/features/session_replay/shared/recorder-events.js +2 -2
  102. package/src/features/session_trace/aggregate/index.js +64 -73
  103. package/src/features/session_trace/aggregate/trace/storage.js +25 -14
  104. package/src/features/soft_navigations/aggregate/index.js +11 -22
  105. package/src/features/soft_navigations/instrument/index.js +6 -9
  106. package/src/features/spa/aggregate/index.js +12 -27
  107. package/src/features/utils/aggregate-base.js +39 -0
  108. package/src/features/utils/event-buffer.js +36 -83
  109. package/src/features/utils/instrument-base.js +3 -3
  110. package/src/loaders/features/features.js +13 -0
  111. package/dist/cjs/features/ajax/aggregate/chunk.js +0 -51
  112. package/dist/esm/features/ajax/aggregate/chunk.js +0 -44
  113. package/dist/types/features/ajax/aggregate/chunk.d.ts +0 -8
  114. package/dist/types/features/ajax/aggregate/chunk.d.ts.map +0 -1
  115. package/src/features/ajax/aggregate/chunk.js +0 -52
@@ -7,6 +7,7 @@ import { obj as encodeObj } from '../../../common/url/encode'
7
7
  import { globalScope } from '../../../common/constants/runtime'
8
8
  import { MODE, SESSION_EVENTS } from '../../../common/session/constants'
9
9
  import { applyFnToProps } from '../../../common/util/traverse'
10
+ import { FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
10
11
  import { cleanURL } from '../../../common/url/clean-url'
11
12
 
12
13
  const ERROR_MODE_SECONDS_WINDOW = 30 * 1000 // sliding window of nodes to track when simply monitoring (but not harvesting) in error mode
@@ -18,8 +19,6 @@ export class Aggregate extends AggregateBase {
18
19
  constructor (agentRef) {
19
20
  super(agentRef, FEATURE_NAME)
20
21
 
21
- /** A buffer to hold on to harvested traces in the case that a retry must be made later */
22
- this.sentTrace = null
23
22
  this.harvestTimeSeconds = agentRef.init.session_trace.harvestTimeSeconds || 30
24
23
  /** Tied to the entitlement flag response from BCS. Will short circuit operations of the agg if false */
25
24
  this.entitled = undefined
@@ -28,7 +27,7 @@ export class Aggregate extends AggregateBase {
28
27
  /** If the harvest module is harvesting */
29
28
  this.harvesting = false
30
29
  /** TraceStorage is the mechanism that holds, normalizes and aggregates ST nodes. It will be accessed and purged when harvests occur */
31
- this.traceStorage = new TraceStorage(this)
30
+ this.events = new TraceStorage(this)
32
31
  /** This agg needs information about sampling (sts) and entitlements (st) to make the appropriate decisions on running */
33
32
  this.waitForFlags(['sts', 'st'])
34
33
  .then(([stMode, stEntitled]) => this.initialize(stMode, stEntitled))
@@ -60,9 +59,9 @@ export class Aggregate extends AggregateBase {
60
59
  })
61
60
 
62
61
  if (typeof PerformanceNavigationTiming !== 'undefined') {
63
- this.traceStorage.storeTiming(globalScope.performance?.getEntriesByType?.('navigation')[0])
62
+ this.events.storeTiming(globalScope.performance?.getEntriesByType?.('navigation')[0])
64
63
  } else {
65
- this.traceStorage.storeTiming(globalScope.performance?.timing, true)
64
+ this.events.storeTiming(globalScope.performance?.timing, true)
66
65
  }
67
66
  }
68
67
 
@@ -77,21 +76,21 @@ export class Aggregate extends AggregateBase {
77
76
 
78
77
  this.timeKeeper ??= this.agentRef.runtime.timeKeeper
79
78
 
80
- this.scheduler = new HarvestScheduler('browser/blobs', {
81
- onFinished: this.onHarvestFinished.bind(this),
79
+ this.scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
80
+ onFinished: (result) => this.postHarvestCleanup(result.sent && result.retry),
82
81
  retryDelay: this.harvestTimeSeconds,
83
- getPayload: this.prepareHarvest.bind(this),
82
+ getPayload: (options) => this.makeHarvestPayload(options.retry),
84
83
  raw: true
85
84
  }, this)
86
85
 
87
86
  /** The handlers set up by the Inst file */
88
- registerHandler('bst', (...args) => this.traceStorage.storeEvent(...args), this.featureName, this.ee)
89
- registerHandler('bstResource', (...args) => this.traceStorage.storeResources(...args), this.featureName, this.ee)
90
- registerHandler('bstHist', (...args) => this.traceStorage.storeHist(...args), this.featureName, this.ee)
91
- registerHandler('bstXhrAgg', (...args) => this.traceStorage.storeXhrAgg(...args), this.featureName, this.ee)
92
- registerHandler('bstApi', (...args) => this.traceStorage.storeSTN(...args), this.featureName, this.ee)
93
- registerHandler('trace-jserror', (...args) => this.traceStorage.storeErrorAgg(...args), this.featureName, this.ee)
94
- registerHandler('pvtAdded', (...args) => this.traceStorage.processPVT(...args), this.featureName, this.ee)
87
+ registerHandler('bst', (...args) => this.events.storeEvent(...args), this.featureName, this.ee)
88
+ registerHandler('bstResource', (...args) => this.events.storeResources(...args), this.featureName, this.ee)
89
+ registerHandler('bstHist', (...args) => this.events.storeHist(...args), this.featureName, this.ee)
90
+ registerHandler('bstXhrAgg', (...args) => this.events.storeXhrAgg(...args), this.featureName, this.ee)
91
+ registerHandler('bstApi', (...args) => this.events.storeSTN(...args), this.featureName, this.ee)
92
+ registerHandler('trace-jserror', (...args) => this.events.storeErrorAgg(...args), this.featureName, this.ee)
93
+ registerHandler('pvtAdded', (...args) => this.events.processPVT(...args), this.featureName, this.ee)
95
94
 
96
95
  /** Only start actually harvesting if running in full mode at init time */
97
96
  if (this.mode === MODE.FULL) this.startHarvesting()
@@ -112,28 +111,32 @@ export class Aggregate extends AggregateBase {
112
111
  this.scheduler.startTimer(this.harvestTimeSeconds)
113
112
  }
114
113
 
115
- /** Called by the harvest scheduler at harvest time to retrieve the payload. This will only actually return a payload if running in full mode */
116
- prepareHarvest (options = {}) {
117
- this.traceStorage.prevStoredEvents.clear() // release references to past events for GC
114
+ preHarvestChecks () {
115
+ if (this.mode !== MODE.FULL) return // only allow harvest if running in full mode
118
116
  if (!this.timeKeeper?.ready) return // this should likely never happen, but just to be safe, we should never harvest if we cant correct time
119
- if (this.blocked || this.mode !== MODE.FULL || this.traceStorage.nodeCount === 0) return
120
- if (this.sessionId !== this.agentRef.runtime.session?.state.value || this.ptid !== this.agentRef.runtime.ptid) return this.abort(3) // if something unexpected happened and we somehow still got to the point of harvesting after a session identifier changed, we should force-exit instead of harvesting
121
- /** Get the ST nodes from the traceStorage buffer. This also returns helpful metadata about the payload. */
122
- const { stns, earliestTimeStamp, latestTimeStamp } = this.traceStorage.takeSTNs()
123
- if (!stns) return // there are no trace nodes
124
- if (options.retry) {
125
- this.sentTrace = stns
117
+ if (!this.agentRef.runtime.session) return // session entity is required for trace to run and continue running
118
+ if (this.sessionId !== this.agentRef.runtime.session.state.value || this.ptid !== this.agentRef.runtime.ptid) {
119
+ // If something unexpected happened and we somehow still got to harvesting after a session identifier changed, we should force-exit instead of harvesting:
120
+ this.abort(3)
121
+ return
126
122
  }
123
+ return true
124
+ }
125
+
126
+ serializer ({ stns }) {
127
+ if (!stns.length) return // there are no processed nodes
128
+ this.everHarvested = true
129
+ return applyFnToProps(stns, this.obfuscator.obfuscateString.bind(this.obfuscator), 'string')
130
+ }
127
131
 
132
+ queryStringsBuilder ({ stns, earliestTimeStamp, latestTimeStamp }) {
128
133
  const firstSessionHarvest = !this.agentRef.runtime.session.state.traceHarvestStarted
129
134
  if (firstSessionHarvest) this.agentRef.runtime.session.write({ traceHarvestStarted: true })
135
+ const hasReplay = this.agentRef.runtime.session.state.sessionReplayMode === 1
136
+ const endUserId = this.agentRef.info.jsAttributes['enduser.id']
137
+ const entityGuid = this.agentRef.runtime.appMetadata.agents?.[0]?.entityGuid
130
138
 
131
- const hasReplay = this.agentRef.runtime.session?.state.sessionReplayMode === 1
132
- const endUserId = this.agentRef.info?.jsAttributes?.['enduser.id']
133
-
134
- this.everHarvested = true
135
-
136
- /** The blob consumer expects the following and will reject if not supplied:
139
+ /* The blob consumer expects the following and will reject if not supplied:
137
140
  * browser_monitoring_key
138
141
  * type
139
142
  * app_id
@@ -142,47 +145,34 @@ export class Aggregate extends AggregateBase {
142
145
  *
143
146
  * For data that does not fit the schema of the above, it should be url-encoded and placed into `attributes`
144
147
  */
145
- const agentMetadata = this.agentRef.runtime.appMetadata?.agents?.[0] || {}
146
- return {
147
- qs: {
148
- browser_monitoring_key: this.agentRef.info.licenseKey,
149
- type: 'BrowserSessionChunk',
150
- app_id: this.agentRef.info.applicationID,
151
- protocol_version: '0',
152
- timestamp: Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
153
- attributes: encodeObj({
154
- ...(agentMetadata.entityGuid && { entityGuid: agentMetadata.entityGuid }),
155
- harvestId: `${this.agentRef.runtime.session?.state.value}_${this.agentRef.runtime.ptid}_${this.agentRef.runtime.harvestCount}`,
156
- // this section of attributes must be controllable and stay below the query param padding limit -- see QUERY_PARAM_PADDING
157
- // if not, data could be lost to truncation at time of sending, potentially breaking parsing / API behavior in NR1
158
- // trace payload metadata
159
- 'trace.firstTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
160
- 'trace.lastTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(latestTimeStamp)),
161
- 'trace.nodes': stns.length,
162
- 'trace.originTimestamp': this.timeKeeper.correctedOriginTime,
163
- // other payload metadata
164
- agentVersion: this.agentRef.runtime.version,
165
- ...(firstSessionHarvest && { firstSessionHarvest }),
166
- ...(hasReplay && { hasReplay }),
167
- ptid: `${this.ptid}`,
168
- session: `${this.sessionId}`,
169
- // customer-defined data should go last so that if it exceeds the query param padding limit it will be truncated instead of important attrs
170
- ...(endUserId && { 'enduser.id': this.obfuscator.obfuscateString(endUserId) }),
171
- currentUrl: this.obfuscator.obfuscateString(cleanURL('' + location))
172
- // The Query Param is being arbitrarily limited in length here. It is also applied when estimating the size of the payload in getPayloadSize()
173
- }, QUERY_PARAM_PADDING).substring(1) // remove the leading '&'
174
- },
175
- body: applyFnToProps(stns, this.obfuscator.obfuscateString.bind(this.obfuscator), 'string')
176
- }
177
- }
178
148
 
179
- /** When the harvest scheduler finishes, this callback is executed. It's main purpose is to determine if the payload needs to be retried
180
- * and if so, it will take all data from the temporary buffer and place it back into the traceStorage module
181
- */
182
- onHarvestFinished (result) {
183
- if (result.sent && result.retry && this.sentTrace) { // merge previous trace back into buffer to retry for next harvest
184
- Object.entries(this.sentTrace).forEach(([name, listOfSTNodes]) => { this.traceStorage.restoreNode(name, listOfSTNodes) })
185
- this.sentTrace = null
149
+ return {
150
+ browser_monitoring_key: this.agentRef.info.licenseKey,
151
+ type: 'BrowserSessionChunk',
152
+ app_id: this.agentRef.info.applicationID,
153
+ protocol_version: '0',
154
+ timestamp: Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
155
+ attributes: encodeObj({
156
+ ...(entityGuid && { entityGuid }),
157
+ harvestId: `${this.agentRef.runtime.session.state.value}_${this.agentRef.runtime.ptid}_${this.agentRef.runtime.harvestCount}`,
158
+ // this section of attributes must be controllable and stay below the query param padding limit -- see QUERY_PARAM_PADDING
159
+ // if not, data could be lost to truncation at time of sending, potentially breaking parsing / API behavior in NR1
160
+ // trace payload metadata
161
+ 'trace.firstTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
162
+ 'trace.lastTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(latestTimeStamp)),
163
+ 'trace.nodes': stns.length,
164
+ 'trace.originTimestamp': this.timeKeeper.correctedOriginTime,
165
+ // other payload metadata
166
+ agentVersion: this.agentRef.runtime.version,
167
+ ...(firstSessionHarvest && { firstSessionHarvest }),
168
+ ...(hasReplay && { hasReplay }),
169
+ ptid: `${this.ptid}`,
170
+ session: `${this.sessionId}`,
171
+ // customer-defined data should go last so that if it exceeds the query param padding limit it will be truncated instead of important attrs
172
+ ...(endUserId && { 'enduser.id': this.obfuscator.obfuscateString(endUserId) }),
173
+ currentUrl: this.obfuscator.obfuscateString(cleanURL('' + location))
174
+ // The Query Param is being arbitrarily limited in length here. It is also applied when estimating the size of the payload in getPayloadSize()
175
+ }, QUERY_PARAM_PADDING).substring(1) // remove the leading '&'
186
176
  }
187
177
  }
188
178
 
@@ -194,16 +184,17 @@ export class Aggregate extends AggregateBase {
194
184
  this.agentRef.runtime.session.write({ sessionTraceMode: this.mode })
195
185
  if (prevMode === MODE.OFF || !this.initialized) return this.initialize(this.mode, this.entitled)
196
186
  if (this.initialized) {
197
- this.traceStorage.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
187
+ 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
198
188
  }
199
189
  this.startHarvesting()
200
190
  }
201
191
 
202
192
  /** Stop running for the remainder of the page lifecycle */
203
- abort (reason) {
193
+ abort () {
204
194
  this.blocked = true
205
195
  this.mode = MODE.OFF
206
196
  this.agentRef.runtime.session.write({ sessionTraceMode: this.mode })
207
197
  this.scheduler?.stopTimer()
198
+ this.events.clear()
208
199
  }
209
200
  }
@@ -29,8 +29,8 @@ export class TraceStorage {
29
29
  trace = {}
30
30
  earliestTimeStamp = Infinity
31
31
  latestTimeStamp = 0
32
- tempStorage = []
33
32
  prevStoredEvents = new Set()
33
+ #backupTrace
34
34
 
35
35
  constructor (parent) {
36
36
  this.parent = parent
@@ -44,9 +44,6 @@ export class TraceStorage {
44
44
  const openedSpace = this.trimSTNs(ERROR_MODE_SECONDS_WINDOW) // but maybe we could make some space by discarding irrelevant nodes if we're in sessioned Error mode
45
45
  if (openedSpace === 0) return
46
46
  }
47
- while (this.tempStorage.length) {
48
- this.storeSTN(this.tempStorage.shift())
49
- }
50
47
 
51
48
  if (this.trace[stn.n]) this.trace[stn.n].push(stn)
52
49
  else this.trace[stn.n] = [stn]
@@ -96,15 +93,9 @@ export class TraceStorage {
96
93
  const partitionListByOriginMap = listOfSTNodes.sort((a, b) => a.s - b.s).reduce(reindexByOriginFn, {})
97
94
  return Object.values(partitionListByOriginMap).flat() // join the partitions back into 1-D, now ordered by origin then start time
98
95
  }, this)
99
- if (stns.length === 0) return {}
100
96
 
101
- this.trace = {}
102
- this.nodeCount = 0
103
97
  const earliestTimeStamp = this.earliestTimeStamp
104
- this.earliestTimeStamp = Infinity
105
98
  const latestTimeStamp = this.latestTimeStamp
106
- this.latestTimeStamp = 0
107
-
108
99
  return { stns, earliestTimeStamp, latestTimeStamp }
109
100
  }
110
101
 
@@ -282,10 +273,30 @@ export class TraceStorage {
282
273
  this.storeSTN(new TraceNode('Ajax', metrics.time, metrics.time + metrics.duration, `${params.status} ${params.method}: ${params.host}${params.pathname}`, 'ajax'))
283
274
  }
284
275
 
285
- restoreNode (name, listOfSTNodes) {
286
- if (this.nodeCount >= MAX_NODES_PER_HARVEST) return
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. */
277
+ isEmpty () {
278
+ return this.nodeCount === 0
279
+ }
280
+
281
+ save () {
282
+ this.#backupTrace = this.trace
283
+ }
284
+
285
+ get = this.takeSTNs
286
+
287
+ clear () {
288
+ this.trace = {}
289
+ this.nodeCount = 0
290
+ this.prevStoredEvents.clear() // release references to past events for GC
291
+ this.earliestTimeStamp = Infinity
292
+ this.latestTimeStamp = 0
293
+ }
294
+
295
+ reloadSave () {
296
+ Object.values(this.#backupTrace).forEach(stnsArray => stnsArray.forEach(stn => this.storeSTN(stn)))
297
+ }
287
298
 
288
- this.nodeCount += listOfSTNodes.length
289
- this.trace[name] = this.trace[name] ? listOfSTNodes.concat(this.trace[name]) : listOfSTNodes
299
+ clearSave () {
300
+ this.#backupTrace = undefined
290
301
  }
291
302
  }
@@ -3,10 +3,9 @@ import { registerHandler } from '../../../common/event-emitter/register-handler'
3
3
  import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
4
4
  import { single } from '../../../common/util/invoke'
5
5
  import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte'
6
- import { FEATURE_NAMES } from '../../../loaders/features/features'
6
+ import { FEATURE_NAMES, FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
7
7
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
8
8
  import { AggregateBase } from '../../utils/aggregate-base'
9
- import { EventBuffer } from '../../utils/event-buffer'
10
9
  import { API_TRIGGER_NAME, FEATURE_NAME, INTERACTION_STATUS } from '../constants'
11
10
  import { AjaxNode } from './ajax-node'
12
11
  import { InitialPageLoadInteraction } from './initial-page-load-interaction'
@@ -18,7 +17,7 @@ export class Aggregate extends AggregateBase {
18
17
  super(agentRef, FEATURE_NAME)
19
18
 
20
19
  const harvestTimeSeconds = agentRef.init.soft_navigations.harvestTimeSeconds || 10
21
- this.interactionsToHarvest = new EventBuffer()
20
+ this.interactionsToHarvest = this.events
22
21
  this.domObserver = domObserver
23
22
 
24
23
  this.initialPageLoadInteraction = new InitialPageLoadInteraction(agentRef.agentIdentifier)
@@ -39,12 +38,12 @@ export class Aggregate extends AggregateBase {
39
38
  this.waitForFlags(['spa']).then(([spaOn]) => {
40
39
  if (spaOn) {
41
40
  this.drain()
42
- const scheduler = new HarvestScheduler('events', {
43
- onFinished: this.onHarvestFinished.bind(this),
41
+ const scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
42
+ onFinished: (result) => this.postHarvestCleanup(result.sent && result.retry),
43
+ getPayload: (options) => this.makeHarvestPayload(options.retry),
44
44
  retryDelay: harvestTimeSeconds,
45
45
  onUnload: () => this.interactionInProgress?.done() // return any held ajax or jserr events so they can be sent with EoL harvest
46
46
  }, this)
47
- scheduler.harvest.on('events', this.onHarvestStarted.bind(this))
48
47
  scheduler.startTimer(harvestTimeSeconds, 0)
49
48
  } else {
50
49
  this.blocked = true // if rum response determines that customer lacks entitlements for spa endpoint, this feature shouldn't harvest
@@ -66,27 +65,16 @@ export class Aggregate extends AggregateBase {
66
65
  registerHandler('jserror', this.#handleJserror.bind(this), this.featureName, this.ee)
67
66
  }
68
67
 
69
- onHarvestStarted (options) {
70
- if (!this.interactionsToHarvest.hasData || this.blocked) return
68
+ serializer (eventBuffer) {
71
69
  // The payload depacker takes the first ixn of a payload (if there are multiple ixns) and positively offset the subsequent ixns timestamps by that amount.
72
70
  // In order to accurately portray the real start & end times of the 2nd & onward ixns, we hence need to negatively offset their start timestamps with that of the 1st ixn.
73
71
  let firstIxnStartTime = 0 // the very 1st ixn does not require any offsetting
74
72
  const serializedIxnList = []
75
- for (const interaction of this.interactionsToHarvest.buffer) {
73
+ for (const interaction of eventBuffer) {
76
74
  serializedIxnList.push(interaction.serialize(firstIxnStartTime))
77
75
  if (!firstIxnStartTime) firstIxnStartTime = Math.floor(interaction.start)
78
76
  }
79
- const payload = `bel.7;${serializedIxnList.join(';')}`
80
-
81
- if (options.retry) this.interactionsToHarvest.hold()
82
- else this.interactionsToHarvest.clear()
83
-
84
- return { body: { e: payload } }
85
- }
86
-
87
- onHarvestFinished (result) {
88
- if (result.sent && result.retry && this.interactionsToHarvest.held.hasData) this.interactionsToHarvest.unhold()
89
- else this.interactionsToHarvest.held.clear()
77
+ return `bel.7;${serializedIxnList.join(';')}`
90
78
  }
91
79
 
92
80
  startUIInteraction (eventName, startedAt, sourceElem) { // this is throttled by instrumentation so that it isn't excessively called
@@ -139,8 +127,9 @@ export class Aggregate extends AggregateBase {
139
127
  */
140
128
  if (this.interactionInProgress?.isActiveDuring(timestamp)) return this.interactionInProgress
141
129
  let saveIxn
142
- for (let idx = this.interactionsToHarvest.buffer.length - 1; idx >= 0; idx--) { // reverse search for the latest completed interaction for efficiency
143
- const finishedInteraction = this.interactionsToHarvest.buffer[idx]
130
+ const interactionsBuffer = this.interactionsToHarvest.get()
131
+ for (let idx = interactionsBuffer.length - 1; idx >= 0; idx--) { // reverse search for the latest completed interaction for efficiency
132
+ const finishedInteraction = interactionsBuffer[idx]
144
133
  if (finishedInteraction.isActiveDuring(timestamp)) {
145
134
  if (finishedInteraction.trigger !== 'initialPageLoad') return finishedInteraction
146
135
  // It's possible that a complete interaction occurs before page is fully loaded, so we need to consider if a route-change ixn may have overlapped this iPL
@@ -3,7 +3,6 @@ import { isBrowserScope } from '../../../common/constants/runtime'
3
3
  import { handle } from '../../../common/event-emitter/handle'
4
4
  import { windowAddEventListener } from '../../../common/event-listener/event-listener-opts'
5
5
  import { debounce } from '../../../common/util/invoke'
6
- import { wrapEvents } from '../../../common/wrap/wrap-events'
7
6
  import { wrapHistory } from '../../../common/wrap/wrap-history'
8
7
  import { InstrumentBase } from '../../utils/instrument-base'
9
8
  import { FEATURE_NAME, INTERACTION_TRIGGERS } from '../constants'
@@ -23,7 +22,12 @@ export class Instrument extends InstrumentBase {
23
22
  if (!isBrowserScope || !gosNREUMOriginals().o.MO) return // soft navigations is not supported outside web env or browsers without the mutation observer API
24
23
 
25
24
  const historyEE = wrapHistory(this.ee)
26
- const eventsEE = wrapEvents(this.ee)
25
+
26
+ INTERACTION_TRIGGERS.forEach((trigger) => {
27
+ windowAddEventListener(trigger, (evt) => {
28
+ processUserInteraction(evt)
29
+ }, true)
30
+ })
27
31
 
28
32
  const trackURLChange = () => handle('newURL', [now(), '' + window.location], undefined, this.featureName, this.ee)
29
33
  historyEE.on('pushState-end', trackURLChange)
@@ -50,13 +54,6 @@ export class Instrument extends InstrumentBase {
50
54
  domObserver.observe(document.body, { attributes: true, childList: true, subtree: true, characterData: true })
51
55
  }, UI_WAIT_INTERVAL, { leading: true })
52
56
 
53
- eventsEE.on('fn-start', ([evt]) => { // set up a new user ixn before the callback for the triggering event executes
54
- if (INTERACTION_TRIGGERS.includes(evt?.type)) {
55
- processUserInteraction(evt)
56
- }
57
- })
58
- for (let eventType of INTERACTION_TRIGGERS) document.addEventListener(eventType, () => { /* no-op, this ensures the UI events are monitored by our callback above */ })
59
-
60
57
  this.abortHandler = abort
61
58
  this.importAggregator(agentRef, { domObserver })
62
59
 
@@ -14,7 +14,7 @@ import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
14
14
  import { Serializer } from './serializer'
15
15
  import { ee } from '../../../common/event-emitter/contextual-ee'
16
16
  import * as CONSTANTS from '../constants'
17
- import { FEATURE_NAMES } from '../../../loaders/features/features'
17
+ import { FEATURE_NAMES, FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
18
18
  import { AggregateBase } from '../../utils/aggregate-base'
19
19
  import { firstContentfulPaint } from '../../../common/vitals/first-contentful-paint'
20
20
  import { firstPaint } from '../../../common/vitals/first-paint'
@@ -23,7 +23,6 @@ import { initialLocation, loadedAsDeferredBrowserScript } from '../../../common/
23
23
  import { handle } from '../../../common/event-emitter/handle'
24
24
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
25
25
  import { warn } from '../../../common/util/console'
26
- import { EventBuffer } from '../../utils/event-buffer'
27
26
 
28
27
  const {
29
28
  FEATURE_NAME, INTERACTION_EVENTS, MAX_TIMER_BUDGET, FN_START, FN_END, CB_START, INTERACTION_API, REMAINING,
@@ -34,7 +33,7 @@ export class Aggregate extends AggregateBase {
34
33
  constructor (agentRef) {
35
34
  super(agentRef, FEATURE_NAME)
36
35
 
37
- this.state = {
36
+ const state = this.state = {
38
37
  initialPageURL: initialLocation,
39
38
  lastSeenUrl: initialLocation,
40
39
  lastSeenRouteName: null,
@@ -48,15 +47,13 @@ export class Aggregate extends AggregateBase {
48
47
  childTime: 0,
49
48
  depth: 0,
50
49
  harvestTimeSeconds: agentRef.init.spa.harvestTimeSeconds || 10,
51
- interactionsToHarvest: new EventBuffer(),
52
50
  // The below feature flag is used to disable the SPA ajax fix for specific customers, see https://new-relic.atlassian.net/browse/NR-172169
53
51
  disableSpaFix: (agentRef.init.feature_flags || []).indexOf('disable-spa-fix') > -1
54
52
  }
53
+ this.spaSerializerClass = new Serializer(this)
55
54
 
55
+ const classThis = this
56
56
  let scheduler
57
- this.serializer = new Serializer(this)
58
-
59
- const { state, serializer } = this
60
57
 
61
58
  const baseEE = ee.get(agentRef.agentIdentifier) // <-- parent baseEE
62
59
  const mutationEE = baseEE.get('mutation')
@@ -103,11 +100,11 @@ export class Aggregate extends AggregateBase {
103
100
 
104
101
  this.waitForFlags((['spa'])).then(([spaFlag]) => {
105
102
  if (spaFlag) {
106
- scheduler = new HarvestScheduler('events', {
107
- onFinished: onHarvestFinished,
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),
108
106
  retryDelay: state.harvestTimeSeconds
109
107
  }, this)
110
- scheduler.harvest.on('events', onHarvestStarted)
111
108
  this.drain()
112
109
  } else {
113
110
  this.blocked = true
@@ -670,22 +667,6 @@ export class Aggregate extends AggregateBase {
670
667
  setCurrentNode(null)
671
668
  }
672
669
 
673
- const classThis = this
674
- function onHarvestStarted (options) {
675
- if (!state.interactionsToHarvest.hasData || classThis.blocked) return {}
676
- var payload = serializer.serializeMultiple(state.interactionsToHarvest.buffer, 0, navTiming)
677
-
678
- if (options.retry) state.interactionsToHarvest.hold()
679
- else state.interactionsToHarvest.clear()
680
-
681
- return { body: { e: payload } }
682
- }
683
-
684
- function onHarvestFinished (result) {
685
- if (result.sent && result.retry && state.interactionsToHarvest.held.hasData) state.interactionsToHarvest.unhold()
686
- else state.interactionsToHarvest.held.clear()
687
- }
688
-
689
670
  baseEE.on('spa-jserror', function (type, name, params, metrics) {
690
671
  if (!state.currentNode) return
691
672
  params._interactionId = state.currentNode.interaction.id
@@ -736,7 +717,7 @@ export class Aggregate extends AggregateBase {
736
717
  interaction.root.attrs.firstContentfulPaint = firstContentfulPaint.current.value
737
718
  }
738
719
  baseEE.emit('interactionDone', [interaction, true])
739
- state.interactionsToHarvest.add(interaction)
720
+ classThis.events.add(interaction)
740
721
 
741
722
  let smCategory
742
723
  if (interaction.root?.attrs?.trigger === 'initialPageLoad') smCategory = 'InitialPageLoad'
@@ -748,4 +729,8 @@ export class Aggregate extends AggregateBase {
748
729
  if (!scheduler) warn(19)
749
730
  }
750
731
  }
732
+
733
+ serializer (eventBuffer) {
734
+ return this.spaSerializerClass.serializeMultiple(eventBuffer, 0, navTiming)
735
+ }
751
736
  }
@@ -5,11 +5,17 @@ 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
+ import { FEATURE_NAMES } from '../../loaders/features/features'
8
10
 
9
11
  export class AggregateBase extends FeatureBase {
10
12
  constructor (agentRef, featureName) {
11
13
  super(agentRef.agentIdentifier, featureName)
12
14
  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()
13
19
  this.checkConfiguration(agentRef)
14
20
  this.obfuscator = agentRef.runtime.obfuscator
15
21
  }
@@ -47,6 +53,39 @@ export class AggregateBase extends FeatureBase {
47
53
  this.drained = true
48
54
  }
49
55
 
56
+ /**
57
+ * Return harvest payload. A "serializer" function can be defined on a derived class to format the payload.
58
+ * @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
60
+ */
61
+ makeHarvestPayload (shouldRetryOnFail = false, opts = {}) {
62
+ if (this.events.isEmpty(opts)) return
63
+ // Other conditions and things to do when preparing harvest that is required.
64
+ if (this.preHarvestChecks && !this.preHarvestChecks()) return
65
+
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)
71
+
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
78
+ }
79
+
80
+ /**
81
+ * Cleanup task after a harvest.
82
+ * @param {Boolean} harvestFailed - harvester flag to restore events in main buffer for retry later if request failed
83
+ */
84
+ postHarvestCleanup (harvestFailed = false, opts = {}) {
85
+ if (harvestFailed) this.events.reloadSave(opts)
86
+ this.events.clearSave(opts)
87
+ }
88
+
50
89
  /**
51
90
  * Checks for additional `jsAttributes` items to support backward compatibility with implementations of the agent where
52
91
  * loader configurations may appear after the loader code is executed.