@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
@@ -21,11 +21,11 @@ export class RecorderEvents {
21
21
  this.#events.add(event);
22
22
  }
23
23
  get events() {
24
- return this.#events.buffer;
24
+ return this.#events.get();
25
25
  }
26
26
 
27
27
  /** A value which increments with every new mutation node reported. Resets after a harvest is sent */
28
28
  get payloadBytesEstimation() {
29
- return this.#events.bytes;
29
+ return this.#events.byteSize();
30
30
  }
31
31
  }
@@ -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
  const ERROR_MODE_SECONDS_WINDOW = 30 * 1000; // sliding window of nodes to track when simply monitoring (but not harvesting) in error mode
12
13
  /** Reserved room for query param attrs */
@@ -15,9 +16,6 @@ export class Aggregate extends AggregateBase {
15
16
  static featureName = FEATURE_NAME;
16
17
  constructor(agentRef) {
17
18
  super(agentRef, FEATURE_NAME);
18
-
19
- /** A buffer to hold on to harvested traces in the case that a retry must be made later */
20
- this.sentTrace = null;
21
19
  this.harvestTimeSeconds = agentRef.init.session_trace.harvestTimeSeconds || 30;
22
20
  /** Tied to the entitlement flag response from BCS. Will short circuit operations of the agg if false */
23
21
  this.entitled = undefined;
@@ -26,7 +24,7 @@ export class Aggregate extends AggregateBase {
26
24
  /** If the harvest module is harvesting */
27
25
  this.harvesting = false;
28
26
  /** TraceStorage is the mechanism that holds, normalizes and aggregates ST nodes. It will be accessed and purged when harvests occur */
29
- this.traceStorage = new TraceStorage(this);
27
+ this.events = new TraceStorage(this);
30
28
  /** This agg needs information about sampling (sts) and entitlements (st) to make the appropriate decisions on running */
31
29
  this.waitForFlags(['sts', 'st']).then(([stMode, stEntitled]) => this.initialize(stMode, stEntitled));
32
30
  }
@@ -55,9 +53,9 @@ export class Aggregate extends AggregateBase {
55
53
  if (this.sessionId !== sessionState.value || eventType === 'cross-tab' && this.scheduler?.started && sessionState.sessionTraceMode === MODE.OFF) this.abort(2);
56
54
  });
57
55
  if (typeof PerformanceNavigationTiming !== 'undefined') {
58
- this.traceStorage.storeTiming(globalScope.performance?.getEntriesByType?.('navigation')[0]);
56
+ this.events.storeTiming(globalScope.performance?.getEntriesByType?.('navigation')[0]);
59
57
  } else {
60
- this.traceStorage.storeTiming(globalScope.performance?.timing, true);
58
+ this.events.storeTiming(globalScope.performance?.timing, true);
61
59
  }
62
60
  }
63
61
 
@@ -69,21 +67,21 @@ export class Aggregate extends AggregateBase {
69
67
  * If it drains later (due to a mode change), data and handlers will instantly drain instead of waiting for the registry. */
70
68
  if (this.mode === MODE.OFF) return this.deregisterDrain();
71
69
  this.timeKeeper ??= this.agentRef.runtime.timeKeeper;
72
- this.scheduler = new HarvestScheduler('browser/blobs', {
73
- onFinished: this.onHarvestFinished.bind(this),
70
+ this.scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
71
+ onFinished: result => this.postHarvestCleanup(result.sent && result.retry),
74
72
  retryDelay: this.harvestTimeSeconds,
75
- getPayload: this.prepareHarvest.bind(this),
73
+ getPayload: options => this.makeHarvestPayload(options.retry),
76
74
  raw: true
77
75
  }, this);
78
76
 
79
77
  /** The handlers set up by the Inst file */
80
- registerHandler('bst', (...args) => this.traceStorage.storeEvent(...args), this.featureName, this.ee);
81
- registerHandler('bstResource', (...args) => this.traceStorage.storeResources(...args), this.featureName, this.ee);
82
- registerHandler('bstHist', (...args) => this.traceStorage.storeHist(...args), this.featureName, this.ee);
83
- registerHandler('bstXhrAgg', (...args) => this.traceStorage.storeXhrAgg(...args), this.featureName, this.ee);
84
- registerHandler('bstApi', (...args) => this.traceStorage.storeSTN(...args), this.featureName, this.ee);
85
- registerHandler('trace-jserror', (...args) => this.traceStorage.storeErrorAgg(...args), this.featureName, this.ee);
86
- registerHandler('pvtAdded', (...args) => this.traceStorage.processPVT(...args), this.featureName, this.ee);
78
+ registerHandler('bst', (...args) => this.events.storeEvent(...args), this.featureName, this.ee);
79
+ registerHandler('bstResource', (...args) => this.events.storeResources(...args), this.featureName, this.ee);
80
+ registerHandler('bstHist', (...args) => this.events.storeHist(...args), this.featureName, this.ee);
81
+ registerHandler('bstXhrAgg', (...args) => this.events.storeXhrAgg(...args), this.featureName, this.ee);
82
+ registerHandler('bstApi', (...args) => this.events.storeSTN(...args), this.featureName, this.ee);
83
+ registerHandler('trace-jserror', (...args) => this.events.storeErrorAgg(...args), this.featureName, this.ee);
84
+ registerHandler('pvtAdded', (...args) => this.events.processPVT(...args), this.featureName, this.ee);
87
85
 
88
86
  /** Only start actually harvesting if running in full mode at init time */
89
87
  if (this.mode === MODE.FULL) this.startHarvesting();else {
@@ -104,32 +102,38 @@ export class Aggregate extends AggregateBase {
104
102
  this.scheduler.runHarvest();
105
103
  this.scheduler.startTimer(this.harvestTimeSeconds);
106
104
  }
107
-
108
- /** Called by the harvest scheduler at harvest time to retrieve the payload. This will only actually return a payload if running in full mode */
109
- prepareHarvest(options = {}) {
110
- this.traceStorage.prevStoredEvents.clear(); // release references to past events for GC
105
+ preHarvestChecks() {
106
+ if (this.mode !== MODE.FULL) return; // only allow harvest if running in full mode
111
107
  if (!this.timeKeeper?.ready) return; // this should likely never happen, but just to be safe, we should never harvest if we cant correct time
112
- if (this.blocked || this.mode !== MODE.FULL || this.traceStorage.nodeCount === 0) return;
113
- 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
114
- /** Get the ST nodes from the traceStorage buffer. This also returns helpful metadata about the payload. */
115
- const {
116
- stns,
117
- earliestTimeStamp,
118
- latestTimeStamp
119
- } = this.traceStorage.takeSTNs();
120
- if (!stns) return; // there are no trace nodes
121
- if (options.retry) {
122
- this.sentTrace = stns;
108
+ if (!this.agentRef.runtime.session) return; // session entity is required for trace to run and continue running
109
+ if (this.sessionId !== this.agentRef.runtime.session.state.value || this.ptid !== this.agentRef.runtime.ptid) {
110
+ // If something unexpected happened and we somehow still got to harvesting after a session identifier changed, we should force-exit instead of harvesting:
111
+ this.abort(3);
112
+ return;
123
113
  }
114
+ return true;
115
+ }
116
+ serializer({
117
+ stns
118
+ }) {
119
+ if (!stns.length) return; // there are no processed nodes
120
+ this.everHarvested = true;
121
+ return applyFnToProps(stns, this.obfuscator.obfuscateString.bind(this.obfuscator), 'string');
122
+ }
123
+ queryStringsBuilder({
124
+ stns,
125
+ earliestTimeStamp,
126
+ latestTimeStamp
127
+ }) {
124
128
  const firstSessionHarvest = !this.agentRef.runtime.session.state.traceHarvestStarted;
125
129
  if (firstSessionHarvest) this.agentRef.runtime.session.write({
126
130
  traceHarvestStarted: true
127
131
  });
128
- const hasReplay = this.agentRef.runtime.session?.state.sessionReplayMode === 1;
129
- const endUserId = this.agentRef.info?.jsAttributes?.['enduser.id'];
130
- this.everHarvested = true;
132
+ const hasReplay = this.agentRef.runtime.session.state.sessionReplayMode === 1;
133
+ const endUserId = this.agentRef.info.jsAttributes['enduser.id'];
134
+ const entityGuid = this.agentRef.runtime.appMetadata.agents?.[0]?.entityGuid;
131
135
 
132
- /** The blob consumer expects the following and will reject if not supplied:
136
+ /* The blob consumer expects the following and will reject if not supplied:
133
137
  * browser_monitoring_key
134
138
  * type
135
139
  * app_id
@@ -138,61 +142,45 @@ export class Aggregate extends AggregateBase {
138
142
  *
139
143
  * For data that does not fit the schema of the above, it should be url-encoded and placed into `attributes`
140
144
  */
141
- const agentMetadata = this.agentRef.runtime.appMetadata?.agents?.[0] || {};
145
+
142
146
  return {
143
- qs: {
144
- browser_monitoring_key: this.agentRef.info.licenseKey,
145
- type: 'BrowserSessionChunk',
146
- app_id: this.agentRef.info.applicationID,
147
- protocol_version: '0',
148
- timestamp: Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
149
- attributes: encodeObj({
150
- ...(agentMetadata.entityGuid && {
151
- entityGuid: agentMetadata.entityGuid
152
- }),
153
- harvestId: "".concat(this.agentRef.runtime.session?.state.value, "_").concat(this.agentRef.runtime.ptid, "_").concat(this.agentRef.runtime.harvestCount),
154
- // this section of attributes must be controllable and stay below the query param padding limit -- see QUERY_PARAM_PADDING
155
- // if not, data could be lost to truncation at time of sending, potentially breaking parsing / API behavior in NR1
156
- // trace payload metadata
157
- 'trace.firstTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
158
- 'trace.lastTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(latestTimeStamp)),
159
- 'trace.nodes': stns.length,
160
- 'trace.originTimestamp': this.timeKeeper.correctedOriginTime,
161
- // other payload metadata
162
- agentVersion: this.agentRef.runtime.version,
163
- ...(firstSessionHarvest && {
164
- firstSessionHarvest
165
- }),
166
- ...(hasReplay && {
167
- hasReplay
168
- }),
169
- ptid: "".concat(this.ptid),
170
- session: "".concat(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 && {
173
- 'enduser.id': this.obfuscator.obfuscateString(endUserId)
174
- }),
175
- currentUrl: this.obfuscator.obfuscateString(cleanURL('' + location))
176
- // The Query Param is being arbitrarily limited in length here. It is also applied when estimating the size of the payload in getPayloadSize()
177
- }, QUERY_PARAM_PADDING).substring(1) // remove the leading '&'
178
- },
179
- body: applyFnToProps(stns, this.obfuscator.obfuscateString.bind(this.obfuscator), 'string')
147
+ browser_monitoring_key: this.agentRef.info.licenseKey,
148
+ type: 'BrowserSessionChunk',
149
+ app_id: this.agentRef.info.applicationID,
150
+ protocol_version: '0',
151
+ timestamp: Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
152
+ attributes: encodeObj({
153
+ ...(entityGuid && {
154
+ entityGuid
155
+ }),
156
+ harvestId: "".concat(this.agentRef.runtime.session.state.value, "_").concat(this.agentRef.runtime.ptid, "_").concat(this.agentRef.runtime.harvestCount),
157
+ // this section of attributes must be controllable and stay below the query param padding limit -- see QUERY_PARAM_PADDING
158
+ // if not, data could be lost to truncation at time of sending, potentially breaking parsing / API behavior in NR1
159
+ // trace payload metadata
160
+ 'trace.firstTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
161
+ 'trace.lastTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(latestTimeStamp)),
162
+ 'trace.nodes': stns.length,
163
+ 'trace.originTimestamp': this.timeKeeper.correctedOriginTime,
164
+ // other payload metadata
165
+ agentVersion: this.agentRef.runtime.version,
166
+ ...(firstSessionHarvest && {
167
+ firstSessionHarvest
168
+ }),
169
+ ...(hasReplay && {
170
+ hasReplay
171
+ }),
172
+ ptid: "".concat(this.ptid),
173
+ session: "".concat(this.sessionId),
174
+ // customer-defined data should go last so that if it exceeds the query param padding limit it will be truncated instead of important attrs
175
+ ...(endUserId && {
176
+ 'enduser.id': this.obfuscator.obfuscateString(endUserId)
177
+ }),
178
+ currentUrl: this.obfuscator.obfuscateString(cleanURL('' + location))
179
+ // The Query Param is being arbitrarily limited in length here. It is also applied when estimating the size of the payload in getPayloadSize()
180
+ }, QUERY_PARAM_PADDING).substring(1) // remove the leading '&'
180
181
  };
181
182
  }
182
183
 
183
- /** When the harvest scheduler finishes, this callback is executed. It's main purpose is to determine if the payload needs to be retried
184
- * and if so, it will take all data from the temporary buffer and place it back into the traceStorage module
185
- */
186
- onHarvestFinished(result) {
187
- if (result.sent && result.retry && this.sentTrace) {
188
- // merge previous trace back into buffer to retry for next harvest
189
- Object.entries(this.sentTrace).forEach(([name, listOfSTNodes]) => {
190
- this.traceStorage.restoreNode(name, listOfSTNodes);
191
- });
192
- this.sentTrace = null;
193
- }
194
- }
195
-
196
184
  /** Switch from "off" or "error" to full mode (if entitled) */
197
185
  switchToFull() {
198
186
  if (this.mode === MODE.FULL || !this.entitled || this.blocked) return;
@@ -203,18 +191,19 @@ export class Aggregate extends AggregateBase {
203
191
  });
204
192
  if (prevMode === MODE.OFF || !this.initialized) return this.initialize(this.mode, this.entitled);
205
193
  if (this.initialized) {
206
- 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
194
+ 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
207
195
  }
208
196
  this.startHarvesting();
209
197
  }
210
198
 
211
199
  /** Stop running for the remainder of the page lifecycle */
212
- abort(reason) {
200
+ abort() {
213
201
  this.blocked = true;
214
202
  this.mode = MODE.OFF;
215
203
  this.agentRef.runtime.session.write({
216
204
  sessionTraceMode: this.mode
217
205
  });
218
206
  this.scheduler?.stopTimer();
207
+ this.events.clear();
219
208
  }
220
209
  }
@@ -35,8 +35,8 @@ export class TraceStorage {
35
35
  trace = {};
36
36
  earliestTimeStamp = Infinity;
37
37
  latestTimeStamp = 0;
38
- tempStorage = [];
39
38
  prevStoredEvents = new Set();
39
+ #backupTrace;
40
40
  constructor(parent) {
41
41
  this.parent = parent;
42
42
  }
@@ -50,9 +50,6 @@ export class TraceStorage {
50
50
  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
51
51
  if (openedSpace === 0) return;
52
52
  }
53
- while (this.tempStorage.length) {
54
- this.storeSTN(this.tempStorage.shift());
55
- }
56
53
  if (this.trace[stn.n]) this.trace[stn.n].push(stn);else this.trace[stn.n] = [stn];
57
54
  if (stn.s < this.earliestTimeStamp) this.earliestTimeStamp = stn.s;
58
55
  if (stn.s > this.latestTimeStamp) this.latestTimeStamp = stn.s;
@@ -99,13 +96,8 @@ export class TraceStorage {
99
96
  const partitionListByOriginMap = listOfSTNodes.sort((a, b) => a.s - b.s).reduce(reindexByOriginFn, {});
100
97
  return Object.values(partitionListByOriginMap).flat(); // join the partitions back into 1-D, now ordered by origin then start time
101
98
  }, this);
102
- if (stns.length === 0) return {};
103
- this.trace = {};
104
- this.nodeCount = 0;
105
99
  const earliestTimeStamp = this.earliestTimeStamp;
106
- this.earliestTimeStamp = Infinity;
107
100
  const latestTimeStamp = this.latestTimeStamp;
108
- this.latestTimeStamp = 0;
109
101
  return {
110
102
  stns,
111
103
  earliestTimeStamp,
@@ -276,9 +268,26 @@ export class TraceStorage {
276
268
  if (type !== 'xhr') return;
277
269
  this.storeSTN(new TraceNode('Ajax', metrics.time, metrics.time + metrics.duration, "".concat(params.status, " ").concat(params.method, ": ").concat(params.host).concat(params.pathname), 'ajax'));
278
270
  }
279
- restoreNode(name, listOfSTNodes) {
280
- if (this.nodeCount >= MAX_NODES_PER_HARVEST) return;
281
- this.nodeCount += listOfSTNodes.length;
282
- this.trace[name] = this.trace[name] ? listOfSTNodes.concat(this.trace[name]) : listOfSTNodes;
271
+
272
+ /* 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. */
273
+ isEmpty() {
274
+ return this.nodeCount === 0;
275
+ }
276
+ save() {
277
+ this.#backupTrace = this.trace;
278
+ }
279
+ get = this.takeSTNs;
280
+ clear() {
281
+ this.trace = {};
282
+ this.nodeCount = 0;
283
+ this.prevStoredEvents.clear(); // release references to past events for GC
284
+ this.earliestTimeStamp = Infinity;
285
+ this.latestTimeStamp = 0;
286
+ }
287
+ reloadSave() {
288
+ Object.values(this.#backupTrace).forEach(stnsArray => stnsArray.forEach(stn => this.storeSTN(stn)));
289
+ }
290
+ clearSave() {
291
+ this.#backupTrace = undefined;
283
292
  }
284
293
  }
@@ -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
  }) {
19
18
  super(agentRef, FEATURE_NAME);
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
  this.initialPageLoadInteraction = new InitialPageLoadInteraction(agentRef.agentIdentifier);
24
23
  timeToFirstByte.subscribe(({
@@ -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
@@ -63,26 +62,16 @@ export class Aggregate extends AggregateBase {
63
62
  registerHandler('ajax', this.#handleAjaxEvent.bind(this), this.featureName, this.ee);
64
63
  registerHandler('jserror', this.#handleJserror.bind(this), this.featureName, this.ee);
65
64
  }
66
- onHarvestStarted(options) {
67
- if (!this.interactionsToHarvest.hasData || this.blocked) return;
65
+ serializer(eventBuffer) {
68
66
  // 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.
69
67
  // 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.
70
68
  let firstIxnStartTime = 0; // the very 1st ixn does not require any offsetting
71
69
  const serializedIxnList = [];
72
- for (const interaction of this.interactionsToHarvest.buffer) {
70
+ for (const interaction of eventBuffer) {
73
71
  serializedIxnList.push(interaction.serialize(firstIxnStartTime));
74
72
  if (!firstIxnStartTime) firstIxnStartTime = Math.floor(interaction.start);
75
73
  }
76
- const payload = "bel.7;".concat(serializedIxnList.join(';'));
77
- if (options.retry) this.interactionsToHarvest.hold();else this.interactionsToHarvest.clear();
78
- return {
79
- body: {
80
- e: payload
81
- }
82
- };
83
- }
84
- onHarvestFinished(result) {
85
- if (result.sent && result.retry && this.interactionsToHarvest.held.hasData) this.interactionsToHarvest.unhold();else this.interactionsToHarvest.held.clear();
74
+ return "bel.7;".concat(serializedIxnList.join(';'));
86
75
  }
87
76
  startUIInteraction(eventName, startedAt, sourceElem) {
88
77
  // this is throttled by instrumentation so that it isn't excessively called
@@ -130,9 +119,10 @@ export class Aggregate extends AggregateBase {
130
119
  */
131
120
  if (this.interactionInProgress?.isActiveDuring(timestamp)) return this.interactionInProgress;
132
121
  let saveIxn;
133
- for (let idx = this.interactionsToHarvest.buffer.length - 1; idx >= 0; idx--) {
122
+ const interactionsBuffer = this.interactionsToHarvest.get();
123
+ for (let idx = interactionsBuffer.length - 1; idx >= 0; idx--) {
134
124
  // reverse search for the latest completed interaction for efficiency
135
- const finishedInteraction = this.interactionsToHarvest.buffer[idx];
125
+ const finishedInteraction = interactionsBuffer[idx];
136
126
  if (finishedInteraction.isActiveDuring(timestamp)) {
137
127
  if (finishedInteraction.trigger !== 'initialPageLoad') return finishedInteraction;
138
128
  // 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,11 @@ 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
+ INTERACTION_TRIGGERS.forEach(trigger => {
26
+ windowAddEventListener(trigger, evt => {
27
+ processUserInteraction(evt);
28
+ }, true);
29
+ });
27
30
  const trackURLChange = () => handle('newURL', [now(), '' + window.location], undefined, this.featureName, this.ee);
28
31
  historyEE.on('pushState-end', trackURLChange);
29
32
  historyEE.on('replaceState-end', trackURLChange);
@@ -53,13 +56,6 @@ export class Instrument extends InstrumentBase {
53
56
  }, UI_WAIT_INTERVAL, {
54
57
  leading: true
55
58
  });
56
- eventsEE.on('fn-start', ([evt]) => {
57
- // set up a new user ixn before the callback for the triggering event executes
58
- if (INTERACTION_TRIGGERS.includes(evt?.type)) {
59
- processUserInteraction(evt);
60
- }
61
- });
62
- for (let eventType of INTERACTION_TRIGGERS) document.addEventListener(eventType, () => {/* no-op, this ensures the UI events are monitored by our callback above */});
63
59
  this.abortHandler = abort;
64
60
  this.importAggregator(agentRef, {
65
61
  domObserver
@@ -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
  const {
28
27
  FEATURE_NAME,
29
28
  INTERACTION_EVENTS,
@@ -46,7 +45,7 @@ export class Aggregate extends AggregateBase {
46
45
  static featureName = FEATURE_NAME;
47
46
  constructor(agentRef) {
48
47
  super(agentRef, FEATURE_NAME);
49
- this.state = {
48
+ const state = this.state = {
50
49
  initialPageURL: initialLocation,
51
50
  lastSeenUrl: initialLocation,
52
51
  lastSeenRouteName: null,
@@ -60,16 +59,12 @@ export class Aggregate extends AggregateBase {
60
59
  childTime: 0,
61
60
  depth: 0,
62
61
  harvestTimeSeconds: agentRef.init.spa.harvestTimeSeconds || 10,
63
- interactionsToHarvest: new EventBuffer(),
64
62
  // The below feature flag is used to disable the SPA ajax fix for specific customers, see https://new-relic.atlassian.net/browse/NR-172169
65
63
  disableSpaFix: (agentRef.init.feature_flags || []).indexOf('disable-spa-fix') > -1
66
64
  };
65
+ this.spaSerializerClass = new Serializer(this);
66
+ const classThis = this;
67
67
  let scheduler;
68
- this.serializer = new Serializer(this);
69
- const {
70
- state,
71
- serializer
72
- } = this;
73
68
  const baseEE = ee.get(agentRef.agentIdentifier); // <-- parent baseEE
74
69
  const mutationEE = baseEE.get('mutation');
75
70
  const promiseEE = baseEE.get('promise');
@@ -115,11 +110,11 @@ export class Aggregate extends AggregateBase {
115
110
 
116
111
  this.waitForFlags(['spa']).then(([spaFlag]) => {
117
112
  if (spaFlag) {
118
- scheduler = new HarvestScheduler('events', {
119
- onFinished: onHarvestFinished,
113
+ scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
114
+ onFinished: result => this.postHarvestCleanup(result.sent && result.retry),
115
+ getPayload: options => this.makeHarvestPayload(options.retry),
120
116
  retryDelay: state.harvestTimeSeconds
121
117
  }, this);
122
- scheduler.harvest.on('events', onHarvestStarted);
123
118
  this.drain();
124
119
  } else {
125
120
  this.blocked = true;
@@ -610,20 +605,6 @@ export class Aggregate extends AggregateBase {
610
605
  });
611
606
  setCurrentNode(null);
612
607
  }
613
- const classThis = this;
614
- function onHarvestStarted(options) {
615
- if (!state.interactionsToHarvest.hasData || classThis.blocked) return {};
616
- var payload = serializer.serializeMultiple(state.interactionsToHarvest.buffer, 0, navTiming);
617
- if (options.retry) state.interactionsToHarvest.hold();else state.interactionsToHarvest.clear();
618
- return {
619
- body: {
620
- e: payload
621
- }
622
- };
623
- }
624
- function onHarvestFinished(result) {
625
- if (result.sent && result.retry && state.interactionsToHarvest.held.hasData) state.interactionsToHarvest.unhold();else state.interactionsToHarvest.held.clear();
626
- }
627
608
  baseEE.on('spa-jserror', function (type, name, params, metrics) {
628
609
  if (!state.currentNode) return;
629
610
  params._interactionId = state.currentNode.interaction.id;
@@ -670,7 +651,7 @@ export class Aggregate extends AggregateBase {
670
651
  interaction.root.attrs.firstContentfulPaint = firstContentfulPaint.current.value;
671
652
  }
672
653
  baseEE.emit('interactionDone', [interaction, true]);
673
- state.interactionsToHarvest.add(interaction);
654
+ classThis.events.add(interaction);
674
655
  let smCategory;
675
656
  if (interaction.root?.attrs?.trigger === 'initialPageLoad') smCategory = 'InitialPageLoad';else if (interaction.routeChange) smCategory = 'RouteChange';else smCategory = 'Custom';
676
657
  handle(SUPPORTABILITY_METRIC_CHANNEL, ["Spa/Interaction/".concat(smCategory, "/Duration/Ms"), Math.max((interaction.root?.end || 0) - (interaction.root?.start || 0), 0)], undefined, FEATURE_NAMES.metrics, baseEE);
@@ -678,4 +659,7 @@ export class Aggregate extends AggregateBase {
678
659
  if (!scheduler) warn(19);
679
660
  }
680
661
  }
662
+ serializer(eventBuffer) {
663
+ return this.spaSerializerClass.serializeMultiple(eventBuffer, 0, navTiming);
664
+ }
681
665
  }
@@ -5,10 +5,16 @@ 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
  export class AggregateBase extends FeatureBase {
9
11
  constructor(agentRef, featureName) {
10
12
  super(agentRef.agentIdentifier, featureName);
11
13
  this.agentRef = agentRef;
14
+ // Jserror and Metric features uses a singleton EventAggregator instead of a regular EventBuffer.
15
+ if ([FEATURE_NAMES.jserrors, FEATURE_NAMES.metrics].includes(this.featureName)) this.events = agentRef.sharedAggregator;
16
+ // PVE has no need for eventBuffer, and SessionTrace has its own storage mechanism.
17
+ else if (![FEATURE_NAMES.pageViewEvent, FEATURE_NAMES.sessionTrace].includes(this.featureName)) this.events = new EventBuffer();
12
18
  this.checkConfiguration(agentRef);
13
19
  this.obfuscator = agentRef.runtime.obfuscator;
14
20
  }
@@ -45,6 +51,37 @@ export class AggregateBase extends FeatureBase {
45
51
  this.drained = true;
46
52
  }
47
53
 
54
+ /**
55
+ * Return harvest payload. A "serializer" function can be defined on a derived class to format the payload.
56
+ * @param {Boolean} shouldRetryOnFail - harvester flag to backup payload for retry later if harvest request fails; this should be moved to harvester logic
57
+ * @returns final payload, or undefined if there are no pending events
58
+ */
59
+ makeHarvestPayload(shouldRetryOnFail = false, opts = {}) {
60
+ if (this.events.isEmpty(opts)) return;
61
+ // Other conditions and things to do when preparing harvest that is required.
62
+ if (this.preHarvestChecks && !this.preHarvestChecks()) return;
63
+ if (shouldRetryOnFail) this.events.save(opts);
64
+ const returnedData = this.events.get(opts);
65
+ // A serializer or formatter assists in creating the payload `body` from stored events on harvest when defined by derived feature class.
66
+ const body = this.serializer ? this.serializer(returnedData) : returnedData;
67
+ this.events.clear(opts);
68
+ const payload = {
69
+ body
70
+ };
71
+ // Constructs the payload `qs` for relevant features on harvest.
72
+ if (this.queryStringsBuilder) payload.qs = this.queryStringsBuilder(returnedData);
73
+ return payload;
74
+ }
75
+
76
+ /**
77
+ * Cleanup task after a harvest.
78
+ * @param {Boolean} harvestFailed - harvester flag to restore events in main buffer for retry later if request failed
79
+ */
80
+ postHarvestCleanup(harvestFailed = false, opts = {}) {
81
+ if (harvestFailed) this.events.reloadSave(opts);
82
+ this.events.clearSave(opts);
83
+ }
84
+
48
85
  /**
49
86
  * Checks for additional `jsAttributes` items to support backward compatibility with implementations of the agent where
50
87
  * loader configurations may appear after the loader code is executed.