@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
@@ -7,6 +7,7 @@ import { onDOMContentLoaded } from '../../../common/window/load'
7
7
  import { windowAddEventListener } from '../../../common/event-listener/event-listener-opts'
8
8
  import { isBrowserScope, isWorkerScope } from '../../../common/constants/runtime'
9
9
  import { AggregateBase } from '../../utils/aggregate-base'
10
+ import { FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
10
11
  import { isIFrameWindow } from '../../../common/dom/iframe'
11
12
  // import { WEBSOCKET_TAG } from '../../../common/wrap/wrap-websocket'
12
13
  // import { handleWebsocketEvents } from './websocket-detection'
@@ -15,13 +16,14 @@ export class Aggregate extends AggregateBase {
15
16
  static featureName = FEATURE_NAME
16
17
  constructor (agentRef) {
17
18
  super(agentRef, FEATURE_NAME)
19
+ const aggregatorTypes = ['cm', 'sm'] // the types in EventAggregator this feature cares about
18
20
 
19
21
  this.waitForFlags(['err']).then(([errFlag]) => {
20
22
  if (errFlag) {
21
23
  // *cli, Mar 23 - Per NR-94597, this feature should only harvest ONCE at the (potential) EoL time of the page.
22
- const scheduler = new HarvestScheduler('jserrors', { onUnload: () => this.unload() }, this)
24
+ const scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], { onUnload: () => this.unload() }, this)
23
25
  // this is needed to ensure EoL is "on" and sent
24
- scheduler.harvest.on('jserrors', () => ({ body: this.agentRef.sharedAggregator.take(['cm', 'sm']) }))
26
+ scheduler.harvest.on(FEATURE_TO_ENDPOINT[this.featureName], () => this.makeHarvestPayload(undefined, { aggregatorTypes }))
25
27
  this.drain()
26
28
  } else {
27
29
  this.blocked = true // if rum response determines that customer lacks entitlements for spa endpoint, this feature shouldn't harvest
@@ -41,14 +43,14 @@ export class Aggregate extends AggregateBase {
41
43
  if (this.blocked) return
42
44
  const type = SUPPORTABILITY_METRIC
43
45
  const params = { name }
44
- this.agentRef.sharedAggregator.storeMetric(type, name, params, value)
46
+ this.events.addMetric(type, name, params, value)
45
47
  }
46
48
 
47
49
  storeEventMetrics (name, metrics) {
48
50
  if (this.blocked) return
49
51
  const type = CUSTOM_METRIC
50
52
  const params = { name }
51
- this.agentRef.sharedAggregator.store(type, name, params, metrics)
53
+ this.events.add(type, name, params, metrics)
52
54
  }
53
55
 
54
56
  singleChecks () {
@@ -8,7 +8,7 @@ import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
8
8
  import { registerHandler } from '../../../common/event-emitter/register-handler'
9
9
  import { handle } from '../../../common/event-emitter/handle'
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 { AggregateBase } from '../../utils/aggregate-base'
13
13
  import { cumulativeLayoutShift } from '../../../common/vitals/cumulative-layout-shift'
14
14
  import { firstContentfulPaint } from '../../../common/vitals/first-contentful-paint'
@@ -19,7 +19,6 @@ import { largestContentfulPaint } from '../../../common/vitals/largest-contentfu
19
19
  import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte'
20
20
  import { subscribeToVisibilityChange } from '../../../common/window/page-visibility'
21
21
  import { VITAL_NAMES } from '../../../common/vitals/constants'
22
- import { EventBuffer } from '../../utils/event-buffer'
23
22
 
24
23
  export class Aggregate extends AggregateBase {
25
24
  static featureName = FEATURE_NAME
@@ -30,8 +29,6 @@ export class Aggregate extends AggregateBase {
30
29
 
31
30
  constructor (agentRef) {
32
31
  super(agentRef, FEATURE_NAME)
33
-
34
- this.timings = new EventBuffer()
35
32
  this.curSessEndRecorded = false
36
33
 
37
34
  registerHandler('docHidden', msTimestamp => this.endCurrentSession(msTimestamp), this.featureName, this.ee)
@@ -60,9 +57,9 @@ export class Aggregate extends AggregateBase {
60
57
  this.addTiming(name, value * 1000, attrs)
61
58
  }, true) // CLS node should only reports on vis change rather than on every change
62
59
 
63
- const scheduler = new HarvestScheduler('events', {
64
- onFinished: (...args) => this.onHarvestFinished(...args),
65
- getPayload: (...args) => this.prepareHarvest(...args)
60
+ const scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
61
+ onFinished: (result) => this.postHarvestCleanup(result.sent && result.retry),
62
+ getPayload: (options) => this.makeHarvestPayload(options.retry)
66
63
  }, this)
67
64
  scheduler.startTimer(harvestTimeSeconds)
68
65
 
@@ -97,7 +94,7 @@ export class Aggregate extends AggregateBase {
97
94
  attrs.cls = cumulativeLayoutShift.current.value
98
95
  }
99
96
 
100
- this.timings.add({
97
+ this.events.add({
101
98
  name,
102
99
  value,
103
100
  attrs
@@ -106,11 +103,6 @@ export class Aggregate extends AggregateBase {
106
103
  handle('pvtAdded', [name, value, attrs], undefined, FEATURE_NAMES.sessionTrace, this.ee)
107
104
  }
108
105
 
109
- onHarvestFinished (result) {
110
- if (result.retry && this.timings.held.hasData) this.timings.unhold()
111
- else this.timings.held.clear()
112
- }
113
-
114
106
  appendGlobalCustomAttributes (timing) {
115
107
  var timingAttributes = timing.attrs || {}
116
108
 
@@ -123,27 +115,14 @@ export class Aggregate extends AggregateBase {
123
115
  })
124
116
  }
125
117
 
126
- // serialize and return current timing data, clear and save current data for retry
127
- prepareHarvest (options) {
128
- if (!this.timings.hasData) return
129
-
130
- var payload = this.getPayload(this.timings.buffer)
131
- if (options.retry) this.timings.hold()
132
- else this.timings.clear()
133
-
134
- return {
135
- body: { e: payload }
136
- }
137
- }
138
-
139
118
  // serialize array of timing data
140
- getPayload (data) {
119
+ serializer (eventBuffer) {
141
120
  var addString = getAddStringContext(this.agentIdentifier)
142
121
 
143
122
  var payload = 'bel.6;'
144
123
 
145
- for (var i = 0; i < data.length; i++) {
146
- var timing = data[i]
124
+ for (var i = 0; i < eventBuffer.length; i++) {
125
+ var timing = eventBuffer[i]
147
126
 
148
127
  payload += 'e,'
149
128
  payload += addString(timing.name) + ','
@@ -156,7 +135,7 @@ export class Aggregate extends AggregateBase {
156
135
  payload += numeric(attrParts.length) + ';' + attrParts.join(';')
157
136
  }
158
137
 
159
- if ((i + 1) < data.length) payload += ';'
138
+ if ((i + 1) < eventBuffer.length) payload += ';'
160
139
  }
161
140
 
162
141
  return payload
@@ -16,7 +16,7 @@ import { warn } from '../../../common/util/console'
16
16
  import { globalScope } from '../../../common/constants/runtime'
17
17
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
18
18
  import { handle } from '../../../common/event-emitter/handle'
19
- import { FEATURE_NAMES } from '../../../loaders/features/features'
19
+ import { FEATURE_NAMES, FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
20
20
  import { RRWEB_VERSION } from '../../../common/constants/env'
21
21
  import { MODE, SESSION_EVENTS, SESSION_EVENT_TYPES } from '../../../common/session/constants'
22
22
  import { stringify } from '../../../common/util/stringify'
@@ -54,14 +54,6 @@ export class Aggregate extends AggregateBase {
54
54
 
55
55
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/Enabled'], undefined, FEATURE_NAMES.metrics, this.ee)
56
56
 
57
- this.ee.on(`cfc.${FEATURE_NAMES.jserrors}`, (crossFeatureData) => {
58
- crossFeatureData.hasReplay = !!(this.scheduler?.started &&
59
- this.recorder &&
60
- this.mode === MODE.FULL &&
61
- !this.blocked &&
62
- this.entitled)
63
- })
64
-
65
57
  // The SessionEntity class can emit a message indicating the session was cleared and reset (expiry, inactivity). This feature must abort and never resume if that occurs.
66
58
  this.ee.on(SESSION_EVENTS.RESET, () => {
67
59
  this.abort(ABORT_REASONS.RESET)
@@ -85,10 +77,10 @@ export class Aggregate extends AggregateBase {
85
77
  })
86
78
 
87
79
  // Bespoke logic for blobs endpoint.
88
- this.scheduler = new HarvestScheduler('browser/blobs', {
89
- onFinished: this.onHarvestFinished.bind(this),
80
+ this.scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
81
+ onFinished: (result) => this.postHarvestCleanup(result),
90
82
  retryDelay: this.harvestTimeSeconds,
91
- getPayload: this.prepareHarvest.bind(this),
83
+ getPayload: ({ retry, ...opts }) => this.makeHarvestPayload(retry, opts),
92
84
  raw: true
93
85
  }, this)
94
86
 
@@ -134,6 +126,10 @@ export class Aggregate extends AggregateBase {
134
126
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/ErrorSamplingRate/Value', error_sampling_rate], undefined, FEATURE_NAMES.metrics, this.ee)
135
127
  }
136
128
 
129
+ replayIsActive () {
130
+ return Boolean(this.scheduler?.started && this.recorder && this.mode === MODE.FULL && !this.blocked && this.entitled)
131
+ }
132
+
137
133
  handleError (e) {
138
134
  if (this.recorder) this.recorder.currentBufferTarget.hasError = true
139
135
  // run once
@@ -236,7 +232,7 @@ export class Aggregate extends AggregateBase {
236
232
  }
237
233
  }
238
234
 
239
- prepareHarvest ({ opts } = {}) {
235
+ makeHarvestPayload (shouldRetryOnFail, opts) {
240
236
  if (!this.recorder || !this.timeKeeper?.ready || !this.recorder.hasSeenSnapshot) return
241
237
  const recorderEvents = this.recorder.getEvents()
242
238
  // get the event type and use that to trigger another harvest if needed
@@ -365,7 +361,7 @@ export class Aggregate extends AggregateBase {
365
361
  }
366
362
  }
367
363
 
368
- onHarvestFinished (result) {
364
+ postHarvestCleanup (result) {
369
365
  // The mutual decision for now is to stop recording and clear buffers if ingest is experiencing 429 rate limiting
370
366
  if (result.status === 429) {
371
367
  this.abort(ABORT_REASONS.TOO_MANY)
@@ -24,11 +24,11 @@ export class RecorderEvents {
24
24
  }
25
25
 
26
26
  get events () {
27
- return this.#events.buffer
27
+ return this.#events.get()
28
28
  }
29
29
 
30
30
  /** A value which increments with every new mutation node reported. Resets after a harvest is sent */
31
31
  get payloadBytesEstimation () {
32
- return this.#events.bytes
32
+ return this.#events.byteSize()
33
33
  }
34
34
  }
@@ -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