@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.
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/wrap/wrap-events.js +2 -1
- package/dist/cjs/features/session_trace/aggregate/index.js +20 -21
- package/dist/cjs/features/session_trace/aggregate/trace/storage.js +198 -185
- package/dist/cjs/features/session_trace/aggregate/trace/utils.js +41 -0
- package/dist/cjs/features/session_trace/constants.js +3 -2
- package/dist/cjs/features/utils/aggregate-base.js +1 -2
- package/dist/cjs/features/utils/event-buffer.js +34 -3
- package/dist/cjs/features/utils/event-store-manager.js +18 -1
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/wrap/wrap-events.js +2 -1
- package/dist/esm/features/session_trace/aggregate/index.js +21 -21
- package/dist/esm/features/session_trace/aggregate/trace/storage.js +199 -186
- package/dist/esm/features/session_trace/aggregate/trace/utils.js +34 -0
- package/dist/esm/features/session_trace/constants.js +2 -1
- package/dist/esm/features/utils/aggregate-base.js +1 -2
- package/dist/esm/features/utils/event-buffer.js +34 -3
- package/dist/esm/features/utils/event-store-manager.js +18 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/common/wrap/wrap-events.d.ts.map +1 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts +5 -10
- package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_trace/aggregate/trace/storage.d.ts +81 -39
- package/dist/types/features/session_trace/aggregate/trace/storage.d.ts.map +1 -1
- package/dist/types/features/session_trace/aggregate/trace/utils.d.ts +7 -0
- package/dist/types/features/session_trace/aggregate/trace/utils.d.ts.map +1 -0
- package/dist/types/features/session_trace/constants.d.ts +1 -0
- package/dist/types/features/session_trace/constants.d.ts.map +1 -1
- package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
- package/dist/types/features/utils/event-buffer.d.ts +18 -1
- package/dist/types/features/utils/event-buffer.d.ts.map +1 -1
- package/dist/types/features/utils/event-store-manager.d.ts +12 -0
- package/dist/types/features/utils/event-store-manager.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/wrap/wrap-events.js +2 -1
- package/src/features/session_trace/aggregate/index.js +23 -15
- package/src/features/session_trace/aggregate/trace/storage.js +186 -189
- package/src/features/session_trace/aggregate/trace/utils.js +35 -0
- package/src/features/session_trace/constants.js +1 -0
- package/src/features/utils/aggregate-base.js +1 -2
- package/src/features/utils/event-buffer.js +35 -3
- 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
|
|
18
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
|
|
23
|
+
const SMEARABLES = {
|
|
24
|
+
typing: 'typing',
|
|
25
|
+
scrolling: 'scrolling',
|
|
26
|
+
mousing: 'mousing',
|
|
27
|
+
touching: 'touching'
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
66
|
-
* @param {
|
|
67
|
-
* @returns {
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.#
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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
|
-
|
|
239
|
+
return allStored
|
|
252
240
|
}
|
|
253
241
|
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
-
|
|
279
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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.
|