@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,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
|
-
|
|
13
|
-
const SUPPORTS_PERFORMANCE_OBSERVER = typeof globalScope.PerformanceObserver === 'function';
|
|
11
|
+
import { evtName } from './utils';
|
|
14
12
|
const ignoredEvents = {
|
|
15
|
-
// we find that certain events
|
|
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
|
|
32
|
-
typing:
|
|
33
|
-
scrolling:
|
|
34
|
-
mousing:
|
|
35
|
-
touching:
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
55
|
-
if (
|
|
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
|
-
/**
|
|
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
|
|
63
|
-
|
|
64
|
-
if
|
|
65
|
-
this.
|
|
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
|
-
*
|
|
70
|
-
* @param {
|
|
71
|
-
* @returns {
|
|
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
|
-
|
|
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.#
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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
|
-
|
|
253
|
+
return allStored;
|
|
252
254
|
}
|
|
253
255
|
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
this
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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.
|