@newrelic/browser-agent 1.295.0-rc.4 → 1.295.0-rc.6

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 (44) hide show
  1. package/dist/cjs/common/constants/env.cdn.js +1 -1
  2. package/dist/cjs/common/constants/env.npm.js +1 -1
  3. package/dist/cjs/common/wrap/wrap-events.js +2 -1
  4. package/dist/cjs/features/session_trace/aggregate/index.js +20 -21
  5. package/dist/cjs/features/session_trace/aggregate/trace/storage.js +198 -185
  6. package/dist/cjs/features/session_trace/aggregate/trace/utils.js +41 -0
  7. package/dist/cjs/features/session_trace/constants.js +3 -2
  8. package/dist/cjs/features/utils/aggregate-base.js +1 -2
  9. package/dist/cjs/features/utils/event-buffer.js +34 -3
  10. package/dist/cjs/features/utils/event-store-manager.js +18 -1
  11. package/dist/esm/common/constants/env.cdn.js +1 -1
  12. package/dist/esm/common/constants/env.npm.js +1 -1
  13. package/dist/esm/common/wrap/wrap-events.js +2 -1
  14. package/dist/esm/features/session_trace/aggregate/index.js +21 -21
  15. package/dist/esm/features/session_trace/aggregate/trace/storage.js +199 -186
  16. package/dist/esm/features/session_trace/aggregate/trace/utils.js +34 -0
  17. package/dist/esm/features/session_trace/constants.js +2 -1
  18. package/dist/esm/features/utils/aggregate-base.js +1 -2
  19. package/dist/esm/features/utils/event-buffer.js +34 -3
  20. package/dist/esm/features/utils/event-store-manager.js +18 -1
  21. package/dist/tsconfig.tsbuildinfo +1 -1
  22. package/dist/types/common/wrap/wrap-events.d.ts.map +1 -1
  23. package/dist/types/features/session_trace/aggregate/index.d.ts +5 -10
  24. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  25. package/dist/types/features/session_trace/aggregate/trace/storage.d.ts +81 -39
  26. package/dist/types/features/session_trace/aggregate/trace/storage.d.ts.map +1 -1
  27. package/dist/types/features/session_trace/aggregate/trace/utils.d.ts +7 -0
  28. package/dist/types/features/session_trace/aggregate/trace/utils.d.ts.map +1 -0
  29. package/dist/types/features/session_trace/constants.d.ts +1 -0
  30. package/dist/types/features/session_trace/constants.d.ts.map +1 -1
  31. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  32. package/dist/types/features/utils/event-buffer.d.ts +18 -1
  33. package/dist/types/features/utils/event-buffer.d.ts.map +1 -1
  34. package/dist/types/features/utils/event-store-manager.d.ts +12 -0
  35. package/dist/types/features/utils/event-store-manager.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/common/wrap/wrap-events.js +2 -1
  38. package/src/features/session_trace/aggregate/index.js +23 -15
  39. package/src/features/session_trace/aggregate/trace/storage.js +186 -189
  40. package/src/features/session_trace/aggregate/trace/utils.js +35 -0
  41. package/src/features/session_trace/constants.js +1 -0
  42. package/src/features/utils/aggregate-base.js +1 -2
  43. package/src/features/utils/event-buffer.js +35 -3
  44. package/src/features/utils/event-store-manager.js +18 -1
@@ -2,21 +2,19 @@
2
2
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
- import { globalScope } from '../../../../common/constants/runtime';
6
5
  import { MODE } from '../../../../common/session/constants';
7
6
  import { now } from '../../../../common/timing/now';
8
7
  import { parseUrl } from '../../../../common/url/parse-url';
9
8
  import { eventOrigin } from '../../../../common/util/event-origin';
10
- import { MAX_NODES_PER_HARVEST } from '../../constants';
9
+ import { ERROR_MODE_SECONDS_WINDOW, MAX_NODES_PER_HARVEST } from '../../constants';
11
10
  import { TraceNode } from './node';
12
- const ERROR_MODE_SECONDS_WINDOW = 30 * 1000; // sliding window of nodes to track when simply monitoring (but not harvesting) in error mode
13
- const SUPPORTS_PERFORMANCE_OBSERVER = typeof globalScope.PerformanceObserver === 'function';
11
+ import { evtName } from './utils';
14
12
  const ignoredEvents = {
15
- // we find that certain events make the data too noisy to be useful
13
+ // we find that certain events are noisy (and not easily smearable like mousemove) and/or duplicative (like with click vs mousedown/mouseup).
14
+ // These would ONLY ever be tracked in ST if the application has event listeners defined for these events... however, just in case - ignore these anyway.
16
15
  global: {
17
16
  mouseup: true,
18
- mousedown: true,
19
- mousemove: true
17
+ mousedown: true
20
18
  },
21
19
  // certain events are present both in the window and in PVT metrics. PVT metrics are prefered so the window events should be ignored
22
20
  window: {
@@ -28,128 +26,146 @@ const ignoredEvents = {
28
26
  ignoreAll: true
29
27
  }
30
28
  };
31
- const toAggregate = {
32
- typing: [1000, 2000],
33
- scrolling: [100, 1000],
34
- mousing: [1000, 2000],
35
- touching: [1000, 2000]
29
+ const SMEARABLES = {
30
+ typing: 'typing',
31
+ scrolling: 'scrolling',
32
+ mousing: 'mousing',
33
+ touching: 'touching'
34
+ };
35
+ const GAPS = {
36
+ [SMEARABLES.typing]: 1000,
37
+ // 1 second gap between typing events
38
+ [SMEARABLES.scrolling]: 100,
39
+ // 100ms gap between scrolling events
40
+ [SMEARABLES.mousing]: 1000,
41
+ // 1 second gap between mousing events
42
+ [SMEARABLES.touching]: 1000 // 1 second gap between touching events
43
+ };
44
+ const LENGTHS = {
45
+ [SMEARABLES.typing]: 2000,
46
+ // 2 seconds max length for typing events
47
+ [SMEARABLES.scrolling]: 1000,
48
+ // 1 second max length for scrolling events
49
+ [SMEARABLES.mousing]: 2000,
50
+ // 2 seconds max length for mousing events
51
+ [SMEARABLES.touching]: 2000 // 2 seconds max length for touching events
36
52
  };
37
53
 
38
- /** The purpose of this class is to manage, normalize, and retrieve ST nodes as needed without polluting the main ST modules */
54
+ /** The purpose of this class is to manage, normalize, and drop various ST nodes as needed without polluting the main ST modules */
39
55
  export class TraceStorage {
40
- nodeCount = 0;
41
- trace = {};
42
- earliestTimeStamp = Infinity;
43
- latestTimeStamp = 0;
56
+ /** prevents duplication of event nodes by keeping a reference of each one seen per harvest cycle */
44
57
  prevStoredEvents = new Set();
45
- #backupTrace;
46
58
  constructor(parent) {
47
59
  this.parent = parent;
48
60
  }
61
+
62
+ /**
63
+ * Checks if a trace node is smearable with previously stored nodes.
64
+ * @param {TraceNode} stn
65
+ * @returns {boolean} true if the node is smearable, false otherwise
66
+ */
67
+ #isSmearable(stn) {
68
+ return stn.n in SMEARABLES;
69
+ }
70
+
71
+ /**
72
+ * Attempts to smear the current trace node with the last stored event in the event buffer.
73
+ * If the last stored event is smearable and matches the current node's origin and type, it will merge the two nodes and return true.
74
+ * If not, it will return false.
75
+ * This is used to reduce the number of smearable trace nodes created for events that occur in quick succession.
76
+ * @param {TraceNode} stn
77
+ * @returns {boolean} true if the node was successfully smeared, false otherwise
78
+ */
79
+ #smear(stn) {
80
+ /**
81
+ * The matcher function to be executed by the event buffer merge method. It must be the same origin and node type,
82
+ * the start time of the new node must be within a certain length of the last seen node's start time,
83
+ * and the end time of the last seen node must be within a certain gap of the new node's start time.
84
+ * If all these conditions are met, we can merge the last seen node's end time with the new one.
85
+ * @param {TraceNode} storedEvent - the event already stored in the event buffer to potentially be merged with
86
+ */
87
+ const matcher = storedEvent => {
88
+ return !(storedEvent.o !== stn.o || storedEvent.n !== stn.n || stn.s - storedEvent.s < LENGTHS[stn.o] || storedEvent.e > stn.s - GAPS[stn.o]);
89
+ };
90
+
91
+ /** the data to be smeared together with a matching event -- if one is found in the event buffer using the matcher defined above */
92
+ const smearableData = {
93
+ e: stn.e
94
+ };
95
+ return this.parent.events.merge(matcher, smearableData);
96
+ }
97
+
98
+ /**
99
+ * Checks if the event should be ignored based on rules around its type and/or origin.
100
+ * @param {TraceNode} stn
101
+ * @returns {boolean} true if the event should be ignored, false otherwise
102
+ */
103
+ #shouldIgnoreEvent(stn) {
104
+ if (stn.n in ignoredEvents.global) return true; // ignore noisy global events or window events that are already captured by PVT metrics
105
+ const origin = stn.o;
106
+ if (ignoredEvents[origin]?.ignoreAll || ignoredEvents[origin]?.[stn.n]) return true;
107
+ return origin === 'xhrOriginMissing' && stn.n === 'Ajax'; // ignore XHR events when the origin is missing
108
+ }
109
+
110
+ /**
111
+ * Checks if a new node can be stored based on the current state of the trace storage class itself as well as the parent class.
112
+ * @returns {boolean} true if a new node can be stored, false otherwise
113
+ */
49
114
  #canStoreNewNode() {
50
115
  if (this.parent.blocked) return false;
51
- if (this.nodeCount >= MAX_NODES_PER_HARVEST) {
116
+ if (this.parent.events.length >= MAX_NODES_PER_HARVEST) {
52
117
  // limit the amount of pending data awaiting next harvest
53
118
  if (this.parent.mode !== MODE.ERROR) return false;
54
- 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
55
- if (openedSpace === 0) return false;
119
+ this.trimSTNsByTime(); // if we're in error mode, we can try to clear the trace storage to make room for new nodes
120
+ if (this.parent.events.length >= MAX_NODES_PER_HARVEST) this.trimSTNsByIndex(1); // if we still can't store new nodes, trim before index 1 to make room for new nodes
56
121
  }
57
122
  return true;
58
123
  }
59
124
 
60
- /** Central internal function called by all the other store__ & addToTrace API to append a trace node. They MUST all have checked #canStoreNewNode before calling this func!! */
125
+ /**
126
+ * Attempts to store a new trace node in the event buffer.
127
+ * @param {TraceNode} stn
128
+ * @returns {boolean} true if the node was successfully stored, false otherwise
129
+ */
61
130
  #storeSTN(stn) {
62
- if (this.trace[stn.n]) this.trace[stn.n].push(stn);else this.trace[stn.n] = [stn];
63
- if (stn.s < this.earliestTimeStamp) this.earliestTimeStamp = stn.s;
64
- if (stn.s > this.latestTimeStamp) this.latestTimeStamp = stn.s;
65
- this.nodeCount++;
131
+ if (this.#shouldIgnoreEvent(stn) || !this.#canStoreNewNode()) return false;
132
+
133
+ /** attempt to smear -- if not possible or it doesnt find a match -- just add it directly to the event buffer */
134
+ if (!this.#isSmearable(stn) || !this.#smear(stn)) this.parent.events.add(stn);
135
+ return true;
66
136
  }
67
137
 
68
138
  /**
69
- * Trim the collection of nodes awaiting harvest such that those seen outside a certain span of time are discarded.
70
- * @param {number} lookbackDuration Past length of time until now for which we care about nodes, in milliseconds
71
- * @returns {number} However many nodes were discarded after trimming.
139
+ * Stores a new trace node in the event buffer.
140
+ * @param {TraceNode} node
141
+ * @returns {boolean} true if the node was successfully stored, false otherwise
72
142
  */
73
- trimSTNs(lookbackDuration) {
74
- let prunedNodes = 0;
75
- const cutoffHighResTime = Math.max(now() - lookbackDuration, 0);
76
- Object.keys(this.trace).forEach(nameCategory => {
77
- const nodeList = this.trace[nameCategory];
78
- /* Notice nodes are appending under their name's list as they end and are stored. This means each list is already (roughly) sorted in chronological order by end time.
79
- * This isn't exact since nodes go through some processing & EE handlers chain, but it's close enough as we still capture nodes whose duration overlaps the lookback window.
80
- * ASSUMPTION: all 'end' timings stored are relative to timeOrigin (DOMHighResTimeStamp) and not Unix epoch based. */
81
- let cutoffIdx = nodeList.findIndex(node => cutoffHighResTime <= node.e);
82
- if (cutoffIdx === 0) return;else if (cutoffIdx < 0) {
83
- // whole list falls outside lookback window and is irrelevant
84
- cutoffIdx = nodeList.length;
85
- delete this.trace[nameCategory];
86
- } else nodeList.splice(0, cutoffIdx); // chop off everything outside our window i.e. before the last <lookbackDuration> timeframe
87
-
88
- this.nodeCount -= cutoffIdx;
89
- prunedNodes += cutoffIdx;
90
- });
91
- return prunedNodes;
92
- }
93
-
94
- /** Used by session trace's harvester to create the payload body. */
95
- takeSTNs() {
96
- if (!SUPPORTS_PERFORMANCE_OBSERVER) {
97
- // if PO isn't supported, this checks resourcetiming buffer every harvest.
98
- this.storeResources(globalScope.performance?.getEntriesByType?.('resource'));
99
- }
100
- const stns = Object.entries(this.trace).flatMap(([name, listOfSTNodes]) => {
101
- // basically take the "this.trace" map-obj and concat all the list-type values
102
- if (!(name in toAggregate)) return listOfSTNodes;
103
- // Special processing for event nodes dealing with user inputs:
104
- const reindexByOriginFn = this.smearEvtsByOrigin(name);
105
- const partitionListByOriginMap = listOfSTNodes.sort((a, b) => a.s - b.s).reduce(reindexByOriginFn, {});
106
- return Object.values(partitionListByOriginMap).flat(); // join the partitions back into 1-D, now ordered by origin then start time
107
- }, this);
108
- const earliestTimeStamp = this.earliestTimeStamp;
109
- const latestTimeStamp = this.latestTimeStamp;
110
- return {
111
- stns,
112
- earliestTimeStamp,
113
- latestTimeStamp
114
- };
115
- }
116
- smearEvtsByOrigin(name) {
117
- const maxGap = toAggregate[name][0];
118
- const maxLen = toAggregate[name][1];
119
- const lastO = {};
120
- return (byOrigin, evtNode) => {
121
- let lastArr = byOrigin[evtNode.o];
122
- if (!lastArr) lastArr = byOrigin[evtNode.o] = [];
123
- const last = lastO[evtNode.o];
124
- if (name === 'scrolling' && !trivial(evtNode)) {
125
- lastO[evtNode.o] = null;
126
- evtNode.n = 'scroll';
127
- lastArr.push(evtNode);
128
- } else if (last && evtNode.s - last.s < maxLen && last.e > evtNode.s - maxGap) {
129
- last.e = evtNode.e;
130
- } else {
131
- lastO[evtNode.o] = evtNode;
132
- lastArr.push(evtNode);
133
- }
134
- return byOrigin;
135
- };
136
- function trivial(node) {
137
- const limit = 4;
138
- return !!(node && typeof node.e === 'number' && typeof node.s === 'number' && node.e - node.s < limit);
139
- }
140
- }
141
143
  storeNode(node) {
142
- if (!this.#canStoreNewNode()) return;
143
- this.#storeSTN(node);
144
+ return this.#storeSTN(node);
144
145
  }
146
+
147
+ /**
148
+ * Processes a PVT (Page Visibility Timing) entry.
149
+ * @param {*} name
150
+ * @param {*} value
151
+ * @param {*} attrs
152
+ * @returns {boolean} true if the node was successfully stored, false otherwise
153
+ */
145
154
  processPVT(name, value, attrs) {
146
- this.storeTiming({
155
+ return this.storeTiming({
147
156
  [name]: value
148
157
  });
149
158
  }
150
- storeTiming(timingEntry, isAbsoluteTimestamp = false) {
151
- if (!timingEntry) return;
152
159
 
160
+ /**
161
+ * Stores a timing entry in the event buffer.
162
+ * @param {*} timingEntry
163
+ * @param {*} isAbsoluteTimestamp
164
+ * @returns {boolean} true if ALL possible nodes were successfully stored, false otherwise
165
+ */
166
+ storeTiming(timingEntry, isAbsoluteTimestamp = false) {
167
+ if (!timingEntry) return false;
168
+ let allStored = true;
153
169
  // loop iterates through prototype also (for FF)
154
170
  for (let key in timingEntry) {
155
171
  let val = timingEntry[key];
@@ -165,19 +181,27 @@ export class TraceStorage {
165
181
  if (this.parent.timeKeeper && this.parent.timeKeeper.ready && isAbsoluteTimestamp) {
166
182
  val = this.parent.timeKeeper.convertAbsoluteTimestamp(Math.floor(this.parent.timeKeeper.correctAbsoluteTimestamp(val)));
167
183
  }
168
- if (!this.#canStoreNewNode()) return; // at any point when no new nodes can be stored, there's no point in processing the rest of the timing entries
169
- this.#storeSTN(new TraceNode(key, val, val, 'document', 'timing'));
184
+ if (!this.#storeSTN(new TraceNode(key, val, val, 'document', 'timing'))) allStored = false;
170
185
  }
186
+ return allStored;
171
187
  }
172
188
 
173
- // Tracks the events and their listener's duration on objects wrapped by wrap-events.
189
+ /**
190
+ * Tracks the events and their listener's duration on objects wrapped by wrap-events.
191
+ * @param {*} currentEvent - the event to be stored
192
+ * @param {*} target - the target of the event
193
+ * @param {*} start - the start time of the event
194
+ * @param {*} end - the end time of the event
195
+ * @returns {boolean} true if the event was successfully stored, false otherwise
196
+ */
174
197
  storeEvent(currentEvent, target, start, end) {
175
- if (this.shouldIgnoreEvent(currentEvent, target)) return;
176
- if (!this.#canStoreNewNode()) return; // need to check if adding node will succeed BEFORE storing event ref below (*cli Jun'25 - addressing memory leak in aborted ST issue #NR-420780)
177
-
178
- if (this.prevStoredEvents.has(currentEvent)) return; // prevent multiple listeners of an event from creating duplicate trace nodes per occurrence. Cleared every harvest. near-zero chance for re-duplication after clearing per harvest since the timestamps of the event are considered for uniqueness.
198
+ /**
199
+ * Important! -- This check NEEDS to be done directly in this handler, before converting to a TraceNode so that the
200
+ * reference pointer to the Event node itself is the object being checked for duplication
201
+ * **/
202
+ if (this.prevStoredEvents.has(currentEvent) || !this.#canStoreNewNode()) return false;
179
203
  this.prevStoredEvents.add(currentEvent);
180
- const evt = new TraceNode(this.evtName(currentEvent.type), start, end, undefined, 'event');
204
+ const evt = new TraceNode(evtName(currentEvent.type), start, end, undefined, 'event');
181
205
  try {
182
206
  // webcomponents-lite.js can trigger an exception on currentEvent.target getter because
183
207
  // it does not check currentEvent.currentTarget before calling getRootNode() on it
@@ -185,52 +209,30 @@ export class TraceStorage {
185
209
  } catch (e) {
186
210
  evt.o = eventOrigin(null, target, this.parent.ee);
187
211
  }
188
- this.#storeSTN(evt);
189
- }
190
- shouldIgnoreEvent(event, target) {
191
- if (event.type in ignoredEvents.global) return true;
192
- const origin = eventOrigin(event.target, target, this.parent.ee);
193
- if (!!ignoredEvents[origin] && ignoredEvents[origin].ignoreAll) return true;
194
- return !!(!!ignoredEvents[origin] && event.type in ignoredEvents[origin]);
195
- }
196
- evtName(type) {
197
- switch (type) {
198
- case 'keydown':
199
- case 'keyup':
200
- case 'keypress':
201
- return 'typing';
202
- case 'mousemove':
203
- case 'mouseenter':
204
- case 'mouseleave':
205
- case 'mouseover':
206
- case 'mouseout':
207
- return 'mousing';
208
- case 'scroll':
209
- return 'scrolling';
210
- case 'touchstart':
211
- case 'touchmove':
212
- case 'touchend':
213
- case 'touchcancel':
214
- case 'touchenter':
215
- case 'touchleave':
216
- return 'touching';
217
- default:
218
- return type;
219
- }
212
+ return this.#storeSTN(evt);
220
213
  }
221
214
 
222
- // Tracks when the window history API specified by wrap-history is used.
215
+ /**
216
+ * Tracks when the window history API specified by wrap-history is used.
217
+ * @param {*} path
218
+ * @param {*} old
219
+ * @param {*} time
220
+ * @returns {boolean} true if the history node was successfully stored, false otherwise
221
+ */
223
222
  storeHist(path, old, time) {
224
- if (!this.#canStoreNewNode()) return;
225
- this.#storeSTN(new TraceNode('history.pushState', time, time, path, old));
223
+ return this.#storeSTN(new TraceNode('history.pushState', time, time, path, old));
226
224
  }
227
- #laststart = 0;
228
- // Processes all the PerformanceResourceTiming entries captured (by observer).
225
+
226
+ /**
227
+ * Processes all the PerformanceResourceTiming entries captured (by observer).
228
+ * @param {*[]} resources
229
+ * @returns {boolean} true if all resource nodes were successfully stored, false otherwise
230
+ */
229
231
  storeResources(resources) {
230
- if (!resources || resources.length === 0) return;
232
+ if (!resources || resources.length === 0) return false;
233
+ let allStored = true;
231
234
  for (let i = 0; i < resources.length; i++) {
232
235
  const currentResource = resources[i];
233
- if ((currentResource.fetchStart | 0) <= this.#laststart) continue; // don't recollect already-seen resources
234
236
  if (!this.#canStoreNewNode()) break; // stop processing if we can't store any more resource nodes anyways
235
237
 
236
238
  const {
@@ -246,55 +248,66 @@ export class TraceStorage {
246
248
  pathname
247
249
  } = parseUrl(currentResource.name);
248
250
  const res = new TraceNode(initiatorType, fetchStart | 0, responseEnd | 0, "".concat(protocol, "://").concat(hostname, ":").concat(port).concat(pathname), entryType);
249
- this.#storeSTN(res);
251
+ if (!this.#storeSTN(res)) allStored = false;
250
252
  }
251
- this.#laststart = resources[resources.length - 1].fetchStart | 0;
253
+ return allStored;
252
254
  }
253
255
 
254
- // JavascriptError (FEATURE) events pipes into ST here.
256
+ /**
257
+ * JavascriptError (FEATURE) events pipes into ST here.
258
+ * @param {*} type
259
+ * @param {*} name
260
+ * @param {*} params
261
+ * @param {*} metrics
262
+ * @returns {boolean} true if the error node was successfully stored, false otherwise
263
+ */
255
264
  storeErrorAgg(type, name, params, metrics) {
256
- if (type !== 'err') return; // internal errors are purposefully ignored
257
- if (!this.#canStoreNewNode()) return;
258
- this.#storeSTN(new TraceNode('error', metrics.time, metrics.time, params.message, params.stackHash));
265
+ if (type !== 'err') return false; // internal errors are purposefully ignored
266
+ return this.#storeSTN(new TraceNode('error', metrics.time, metrics.time, params.message, params.stackHash));
259
267
  }
260
268
 
261
- // Ajax (FEATURE) events--XML & fetches--pipes into ST here.
269
+ /**
270
+ * Ajax (FEATURE) events--XML & fetches--pipes into ST here.
271
+ * @param {*} type
272
+ * @param {*} name
273
+ * @param {*} params
274
+ * @param {*} metrics
275
+ * @returns {boolean} true if the Ajax node was successfully stored, false otherwise
276
+ */
262
277
  storeXhrAgg(type, name, params, metrics) {
263
- if (type !== 'xhr') return;
264
- if (!this.#canStoreNewNode()) return;
265
- 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
+ if (type !== 'xhr') return false;
279
+ return this.#storeSTN(new TraceNode('Ajax', metrics.time, metrics.time + metrics.duration, "".concat(params.status, " ").concat(params.method, ": ").concat(params.host).concat(params.pathname), 'ajax'));
266
280
  }
267
281
 
268
- /* Below are the interface expected & required of whatever storage is used across all features on an individual basis. This allows a common `.events` property on Trace shared with AggregateBase.
269
- Note that the usage must be in sync with the EventStoreManager class such that AggregateBase.makeHarvestPayload can run the same regardless of which storage class a feature is using. */
270
- isEmpty() {
271
- return this.nodeCount === 0;
272
- }
273
- save() {
274
- this.#backupTrace = this.trace;
282
+ /**
283
+ * Trims stored trace nodes in the event buffer by start time.
284
+ * @param {number} lookbackDuration
285
+ * @returns {void}
286
+ */
287
+ trimSTNsByTime(lookbackDuration = ERROR_MODE_SECONDS_WINDOW) {
288
+ this.parent.events.clear({
289
+ clearBeforeTime: Math.max(now - lookbackDuration, 0),
290
+ timestampKey: 'e'
291
+ });
275
292
  }
276
- get() {
277
- return [{
278
- targetApp: this.parent.agentRef.runtime.entityManager.get(),
279
- data: this.takeSTNs()
280
- }];
293
+
294
+ /**
295
+ * Trims stored trace nodes in the event buffer before a given index value.
296
+ * @param {number} index
297
+ * @returns {void}
298
+ */
299
+ trimSTNsByIndex(index = 0) {
300
+ this.parent.events.clear({
301
+ clearBeforeIndex: index // trims before index value
302
+ });
281
303
  }
304
+
305
+ /**
306
+ * clears the stored events in the event buffer.
307
+ * This is used to release references to past events for garbage collection.
308
+ * @returns {void}
309
+ */
282
310
  clear() {
283
- this.trace = {};
284
- this.nodeCount = 0;
285
311
  this.prevStoredEvents.clear(); // release references to past events for GC
286
- this.earliestTimeStamp = Infinity;
287
- this.latestTimeStamp = 0;
288
- }
289
- reloadSave() {
290
- for (const stnsArray of Object.values(this.#backupTrace)) {
291
- for (const stn of stnsArray) {
292
- if (!this.#canStoreNewNode()) return; // stop attempting to re-store nodes
293
- this.#storeSTN(stn);
294
- }
295
- }
296
- }
297
- clearSave() {
298
- this.#backupTrace = undefined;
299
312
  }
300
313
  }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Copyright 2020-2025 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ export function evtName(type) {
6
+ switch (type) {
7
+ case 'keydown':
8
+ case 'keyup':
9
+ case 'keypress':
10
+ return 'typing';
11
+ case 'mousemove':
12
+ case 'mouseenter':
13
+ case 'mouseleave':
14
+ case 'mouseover':
15
+ case 'mouseout':
16
+ return 'mousing';
17
+ case 'touchstart':
18
+ case 'touchmove':
19
+ case 'touchend':
20
+ case 'touchcancel':
21
+ case 'touchenter':
22
+ case 'touchleave':
23
+ return 'touching';
24
+ case 'scroll':
25
+ case 'scrollend':
26
+ return 'scrolling';
27
+ default:
28
+ return type;
29
+ }
30
+ }
31
+ export function isTrivial(node) {
32
+ const limit = 4;
33
+ return !!(node && typeof node.e === 'number' && typeof node.s === 'number' && node.e - node.s < limit);
34
+ }
@@ -11,4 +11,5 @@ export const END = '-end';
11
11
  export const FN_START = 'fn' + START;
12
12
  export const FN_END = 'fn' + END;
13
13
  export const PUSH_STATE = 'pushState';
14
- export const MAX_NODES_PER_HARVEST = 1000;
14
+ export const MAX_NODES_PER_HARVEST = 1000;
15
+ export const ERROR_MODE_SECONDS_WINDOW = 30 * 1000; // sliding window of nodes to track when simply monitoring (but not harvesting) in error mode
@@ -58,8 +58,7 @@ export class AggregateBase extends FeatureBase {
58
58
  #setupEventStore(entityGuid) {
59
59
  if (this.events) return;
60
60
  switch (this.featureName) {
61
- // SessionTrace + Replay have their own storage mechanisms.
62
- case FEATURE_NAMES.sessionTrace:
61
+ // SessionReplay has its own storage mechanisms.
63
62
  case FEATURE_NAMES.sessionReplay:
64
63
  break;
65
64
  // Jserror and Metric features uses a singleton EventAggregator instead of a regular EventBuffer.
@@ -19,6 +19,9 @@ export class EventBuffer {
19
19
  this.maxPayloadSize = maxPayloadSize;
20
20
  this.featureAgg = featureAgg;
21
21
  }
22
+ get length() {
23
+ return this.#buffer.length;
24
+ }
22
25
  isEmpty() {
23
26
  return this.#buffer.length === 0;
24
27
  }
@@ -51,12 +54,40 @@ export class EventBuffer {
51
54
  return true;
52
55
  }
53
56
 
57
+ /**
58
+ * Merges events in the buffer that match the given criteria.
59
+ * @param {Function} matcher - A function that takes an event and returns true if it should be merged.
60
+ * @param {Object} data - The data to merge into the matching events.
61
+ * @returns {boolean} true if a match was found and merged; false otherwise.
62
+ */
63
+ merge(matcher, data) {
64
+ if (this.isEmpty() || !matcher) return false;
65
+ const matchIdx = this.#buffer.findIndex(matcher);
66
+ if (matchIdx < 0) return false;
67
+ this.#buffer[matchIdx] = {
68
+ ...this.#buffer[matchIdx],
69
+ ...data
70
+ };
71
+ return true;
72
+ }
73
+
54
74
  /**
55
75
  * Wipes the main buffer
76
+ * @param {Object} [opts] - options for clearing the buffer
77
+ * @param {Number} [opts.clearBeforeTime] - timestamp before which all events should be cleared
78
+ * @param {String} [opts.timestampKey] - the key in the event object that contains the timestamp to compare against `clearBefore`
79
+ * @param {Number} [opts.clearBeforeIndex] - index before which all events should be cleared
80
+ * @returns {void}
56
81
  */
57
- clear() {
58
- this.#buffer = [];
59
- this.#rawBytes = 0;
82
+ clear(opts = {}) {
83
+ if (opts.clearBeforeTime !== undefined && opts.timestampKey) {
84
+ this.#buffer = this.#buffer.filter(event => event[opts.timestampKey] >= opts.clearBeforeTime);
85
+ } else if (opts.clearBeforeIndex !== undefined) {
86
+ this.#buffer = this.#buffer.slice(opts.clearBeforeIndex);
87
+ } else {
88
+ this.#buffer = [];
89
+ }
90
+ this.#rawBytes = this.#buffer.length ? stringify(this.#buffer)?.length || 0 : 0; // recalculate raw bytes after clearing
60
91
  }
61
92
 
62
93
  /**
@@ -41,7 +41,24 @@ export class EventStoreManager {
41
41
  this.appStorageMap.set(targetEntityGuid, eventStorage);
42
42
  }
43
43
 
44
- // This class must contain an union of all methods from all supported storage classes and conceptualize away the target app argument.
44
+ /** IMPORTANT
45
+ * This class must contain an union of all methods from all supported storage classes and conceptualize away the target app argument.
46
+ */
47
+
48
+ get length() {
49
+ return this.#getEventStore().length;
50
+ }
51
+
52
+ /**
53
+ * Calls the merge method on the underlying storage class.
54
+ * @param {*} matcher
55
+ * @param {*} data
56
+ * @param {*} targetEntityGuid
57
+ * @returns {boolean} True if the merge was successful
58
+ */
59
+ merge(matcher, data, targetEntityGuid) {
60
+ return this.#getEventStore(targetEntityGuid).merge(matcher, data);
61
+ }
45
62
 
46
63
  /**
47
64
  * Calls the isEmpty method on the underlying storage class. If target is provided, runs just for the target, otherwise runs for all apps.