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