@newrelic/browser-agent 1.317.0 → 1.318.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/cjs/common/config/configurable.js +1 -1
- package/dist/cjs/common/config/init-types.js +2 -0
- package/dist/cjs/common/config/init.js +3 -0
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/harvest/harvester.js +3 -219
- package/dist/cjs/common/harvest/send.js +232 -0
- package/dist/cjs/common/harvest/types.js +9 -5
- package/dist/cjs/common/session/session-entity.js +8 -8
- package/dist/cjs/common/v2/mfe-vitals.js +303 -0
- package/dist/cjs/features/generic_events/aggregate/index.js +1 -1
- package/dist/cjs/features/generic_events/instrument/index.js +1 -1
- package/dist/cjs/features/logging/aggregate/index.js +5 -5
- package/dist/cjs/features/page_view_event/aggregate/index.js +65 -30
- package/dist/cjs/features/session_replay/aggregate/index.js +32 -28
- package/dist/cjs/features/session_trace/aggregate/index.js +29 -27
- package/dist/cjs/features/session_trace/constants.js +17 -3
- package/dist/cjs/features/utils/aggregate-base.js +2 -0
- package/dist/cjs/loaders/api/register.js +27 -4
- package/dist/esm/common/config/configurable.js +1 -1
- package/dist/esm/common/config/init-types.js +2 -0
- package/dist/esm/common/config/init.js +3 -0
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/harvest/harvester.js +3 -219
- package/dist/esm/common/harvest/send.js +226 -0
- package/dist/esm/common/harvest/types.js +9 -5
- package/dist/esm/common/session/session-entity.js +9 -9
- package/dist/esm/common/v2/mfe-vitals.js +298 -0
- package/dist/esm/features/generic_events/aggregate/index.js +1 -1
- package/dist/esm/features/generic_events/instrument/index.js +1 -1
- package/dist/esm/features/logging/aggregate/index.js +5 -5
- package/dist/esm/features/page_view_event/aggregate/index.js +64 -29
- package/dist/esm/features/session_replay/aggregate/index.js +32 -28
- package/dist/esm/features/session_trace/aggregate/index.js +30 -28
- package/dist/esm/features/session_trace/constants.js +16 -2
- package/dist/esm/features/utils/aggregate-base.js +2 -0
- package/dist/esm/loaders/api/register.js +27 -4
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/common/config/init-types.d.ts +6 -0
- package/dist/types/common/config/init.d.ts.map +1 -1
- package/dist/types/common/harvest/harvester.d.ts +0 -17
- package/dist/types/common/harvest/harvester.d.ts.map +1 -1
- package/dist/types/common/harvest/send.d.ts +9 -0
- package/dist/types/common/harvest/send.d.ts.map +1 -0
- package/dist/types/common/harvest/types.d.ts +39 -10
- package/dist/types/common/harvest/types.d.ts.map +1 -1
- package/dist/types/common/session/session-entity.d.ts.map +1 -1
- package/dist/types/common/v2/mfe-vitals.d.ts +14 -0
- package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -0
- package/dist/types/features/page_view_event/aggregate/index.d.ts +1 -0
- package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +2 -2
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts +1 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_trace/constants.d.ts +17 -0
- package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
- package/dist/types/loaders/api/register.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/common/config/configurable.js +1 -1
- package/src/common/config/init-types.js +2 -0
- package/src/common/config/init.js +2 -1
- package/src/common/harvest/harvester.js +3 -202
- package/src/common/harvest/send.js +209 -0
- package/src/common/harvest/types.js +9 -5
- package/src/common/session/session-entity.js +8 -9
- package/src/common/v2/mfe-vitals.js +285 -0
- package/src/features/generic_events/aggregate/index.js +1 -1
- package/src/features/generic_events/instrument/index.js +1 -1
- package/src/features/logging/aggregate/index.js +5 -5
- package/src/features/page_view_event/aggregate/index.js +52 -27
- package/src/features/session_replay/aggregate/index.js +30 -27
- package/src/features/session_trace/aggregate/index.js +31 -28
- package/src/features/session_trace/constants.js +7 -1
- package/src/features/utils/aggregate-base.js +2 -0
- package/src/loaders/api/register.js +25 -4
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.trackMFEVitals = trackMFEVitals;
|
|
7
|
+
var _runtime = require("../constants/runtime");
|
|
8
|
+
var _now = require("../timing/now");
|
|
9
|
+
/**
|
|
10
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
11
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const isObservable = node => {
|
|
19
|
+
try {
|
|
20
|
+
return node?.textContent?.trim() || ['img', 'video', 'canvas', 'svg'].includes(node?.nodeName?.toLowerCase());
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if node is within a specific MFE
|
|
28
|
+
* @param {Node} node - DOM node to check
|
|
29
|
+
* @param {string} id - MFE ID to match
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
const isInMFE = (node, id) => {
|
|
33
|
+
if (!node || !id) return false;
|
|
34
|
+
try {
|
|
35
|
+
let curr = node.nodeType === 1 ? node : node.parentElement;
|
|
36
|
+
while (curr?.tagName) {
|
|
37
|
+
if (curr.dataset?.nrMfeId === id) return true;
|
|
38
|
+
curr = curr.parentNode;
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {}
|
|
41
|
+
return false;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create mutation observer for MFE nodes
|
|
46
|
+
* @param {string} id - MFE ID to track
|
|
47
|
+
* @param {Function} onMatch - Callback when matching node is *added*
|
|
48
|
+
* @returns {MutationObserver}
|
|
49
|
+
*/
|
|
50
|
+
const observeMutations = (id, onMatch) => {
|
|
51
|
+
// Try to find existing MFE root
|
|
52
|
+
const mfeRoot = _runtime.globalScope.document?.querySelector("[data-nr-mfe-id=\"".concat(id, "\"]"));
|
|
53
|
+
let observingRoot = !!mfeRoot;
|
|
54
|
+
const obs = new _runtime.globalScope.MutationObserver(mutations => {
|
|
55
|
+
mutations.forEach(m => {
|
|
56
|
+
m.addedNodes.forEach(node => {
|
|
57
|
+
// Check if this is the MFE root being added
|
|
58
|
+
const elem = node.nodeType === 1 ? node : null;
|
|
59
|
+
if (elem?.dataset?.nrMfeId === id) {
|
|
60
|
+
// Found the root! Lets switch to observing just this subtree for performance reasons
|
|
61
|
+
obs.disconnect();
|
|
62
|
+
obs.observe(elem, {
|
|
63
|
+
childList: true,
|
|
64
|
+
subtree: true
|
|
65
|
+
});
|
|
66
|
+
observingRoot = true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Only check isInMFE if we're observing the whole document; skip expensive ancestor walk when observing root
|
|
70
|
+
if (isObservable(node) && (observingRoot || isInMFE(node, id))) {
|
|
71
|
+
onMatch(node, obs);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// If root exists, observe just that subtree; otherwise observe document to catch when root is added
|
|
78
|
+
obs.observe(mfeRoot || _runtime.globalScope.document, {
|
|
79
|
+
childList: true,
|
|
80
|
+
subtree: true
|
|
81
|
+
});
|
|
82
|
+
return obs;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create performance observer for MFE entries
|
|
87
|
+
* @param {Array} observers - Array to track observers
|
|
88
|
+
* @param {Object} config - Observer configuration
|
|
89
|
+
* @param {Function} onEntry - Callback for each entry
|
|
90
|
+
* @returns {PerformanceObserver|null} Observer if successful, null if unsupported
|
|
91
|
+
*/
|
|
92
|
+
const observePerformance = (observers, config, onEntry) => {
|
|
93
|
+
try {
|
|
94
|
+
const obs = new _runtime.globalScope.PerformanceObserver(list => {
|
|
95
|
+
list.getEntries().forEach(onEntry);
|
|
96
|
+
});
|
|
97
|
+
obs.observe(config);
|
|
98
|
+
observers.push(obs);
|
|
99
|
+
return obs;
|
|
100
|
+
} catch (e) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Tracks all Core Web Vitals for a specific MFE.
|
|
107
|
+
* @param {string} id - The MFE ID to track
|
|
108
|
+
* @returns {{fcp: object|null, lcp: object|null, cls: object|null, inp: object|null, disconnect: Function}}
|
|
109
|
+
*/
|
|
110
|
+
function trackMFEVitals(id, timings) {
|
|
111
|
+
let fcpObservedAt = null;
|
|
112
|
+
let lcpObservedAt = null;
|
|
113
|
+
const getTimeRelativeToScriptStart = capturedAt => {
|
|
114
|
+
if (capturedAt === null) return null;
|
|
115
|
+
return capturedAt - (timings?.scriptStart || timings?.registeredAt || 0);
|
|
116
|
+
};
|
|
117
|
+
const vitals = {
|
|
118
|
+
fcp: {
|
|
119
|
+
get value() {
|
|
120
|
+
return getTimeRelativeToScriptStart(fcpObservedAt);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
lcp: {
|
|
124
|
+
get value() {
|
|
125
|
+
return getTimeRelativeToScriptStart(lcpObservedAt);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
cls: {
|
|
129
|
+
value: null
|
|
130
|
+
},
|
|
131
|
+
inp: {
|
|
132
|
+
value: null
|
|
133
|
+
},
|
|
134
|
+
disconnect: () => {}
|
|
135
|
+
};
|
|
136
|
+
if (!id || !_runtime.isBrowserScope || !_runtime.globalScope.MutationObserver || !_runtime.globalScope.PerformanceObserver) return vitals;
|
|
137
|
+
const observers = [];
|
|
138
|
+
const populateVitalMinimums = () => {
|
|
139
|
+
fcpObservedAt ??= (0, _now.now)();
|
|
140
|
+
lcpObservedAt ??= (0, _now.now)();
|
|
141
|
+
vitals.cls.value ??= 0;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// if the MFE has already rendered something on the page before we could set up listeners, just populate vital minimums immediately
|
|
145
|
+
if (_runtime.globalScope.document?.querySelector("[data-nr-mfe-id=\"".concat(id, "\"]"))) populateVitalMinimums();
|
|
146
|
+
|
|
147
|
+
// Track FCP - first contentful paint
|
|
148
|
+
observeMutations(id, (_, obs) => {
|
|
149
|
+
// An observed "FCP" means _something_ rendered, so at minimum we can populate all the baseline values for the vitals
|
|
150
|
+
populateVitalMinimums();
|
|
151
|
+
obs.disconnect();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Track LCP - largest contentful paint
|
|
155
|
+
let largestSize = 0;
|
|
156
|
+
let resizeObs = null;
|
|
157
|
+
const observedElements = new Set();
|
|
158
|
+
try {
|
|
159
|
+
resizeObs = new _runtime.globalScope.ResizeObserver(entries => {
|
|
160
|
+
entries.forEach(entry => {
|
|
161
|
+
try {
|
|
162
|
+
const size = entry.contentRect.width * entry.contentRect.height;
|
|
163
|
+
if (size > largestSize) {
|
|
164
|
+
largestSize = size;
|
|
165
|
+
lcpObservedAt = (0, _now.now)();
|
|
166
|
+
}
|
|
167
|
+
resizeObs.unobserve(entry.target);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
// Element may be detached from DOM
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
} catch (e) {
|
|
174
|
+
// ResizeObserver not supported
|
|
175
|
+
}
|
|
176
|
+
const lcpObs = observeMutations(id, node => {
|
|
177
|
+
// an observed "LCP" means _something_ rendered, so at minimum we can make sure all the baseline values are populated for the vitals
|
|
178
|
+
populateVitalMinimums();
|
|
179
|
+
if (resizeObs) {
|
|
180
|
+
try {
|
|
181
|
+
const elem = node.nodeType === 1 ? node : node.parentElement;
|
|
182
|
+
if (elem && !observedElements.has(elem)) {
|
|
183
|
+
observedElements.add(elem);
|
|
184
|
+
|
|
185
|
+
// For media elements, wait for content to load before observing size
|
|
186
|
+
if (elem.tagName === 'IMG') {
|
|
187
|
+
if (elem.complete) {
|
|
188
|
+
resizeObs.observe(elem);
|
|
189
|
+
} else {
|
|
190
|
+
elem.addEventListener('load', () => {
|
|
191
|
+
resizeObs.observe(elem);
|
|
192
|
+
}, {
|
|
193
|
+
once: true
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
} else if (elem.tagName === 'VIDEO') {
|
|
197
|
+
// For video, wait for first frame (HAVE_CURRENT_DATA = 2)
|
|
198
|
+
if (elem.readyState >= 2) {
|
|
199
|
+
resizeObs.observe(elem);
|
|
200
|
+
} else {
|
|
201
|
+
elem.addEventListener('loadeddata', () => {
|
|
202
|
+
resizeObs.observe(elem);
|
|
203
|
+
}, {
|
|
204
|
+
once: true
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
// For other elements, observe immediately
|
|
209
|
+
resizeObs.observe(elem);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch (e) {
|
|
213
|
+
// Element may not be observable
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
if (resizeObs) observers.push(resizeObs);
|
|
218
|
+
observers.push(lcpObs);
|
|
219
|
+
|
|
220
|
+
// Track CLS - cumulative layout shift
|
|
221
|
+
// Initialize CLS to 0 if browser supports it
|
|
222
|
+
observePerformance(observers, {
|
|
223
|
+
type: 'layout-shift',
|
|
224
|
+
buffered: true
|
|
225
|
+
}, entry => {
|
|
226
|
+
if (entry.hadRecentInput) return;
|
|
227
|
+
(entry.sources || []).some(source => {
|
|
228
|
+
if (isInMFE(source.node, id)) {
|
|
229
|
+
// an observed "CLS" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
|
|
230
|
+
populateVitalMinimums();
|
|
231
|
+
vitals.cls.value += entry.value;
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Track INP - interaction to next paint
|
|
239
|
+
observePerformance(observers, {
|
|
240
|
+
type: 'event',
|
|
241
|
+
buffered: true,
|
|
242
|
+
durationThreshold: 40
|
|
243
|
+
}, entry => {
|
|
244
|
+
if (!entry.interactionId || !isInMFE(entry.target, id)) return;
|
|
245
|
+
if (vitals.inp.value === null || entry.duration > vitals.inp.value) {
|
|
246
|
+
// an observed "INP" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
|
|
247
|
+
populateVitalMinimums();
|
|
248
|
+
vitals.inp.value = entry.duration;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
const interactionEvents = ['pointerdown', 'keydown'];
|
|
252
|
+
const disconnectInteractionListeners = () => {
|
|
253
|
+
interactionEvents.forEach(type => {
|
|
254
|
+
_runtime.globalScope.removeEventListener(type, handleInteraction, {
|
|
255
|
+
passive: true
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Disconnect all observers
|
|
261
|
+
vitals.disconnect = () => {
|
|
262
|
+
// Disconnect all observers
|
|
263
|
+
observers.forEach(obs => {
|
|
264
|
+
try {
|
|
265
|
+
obs?.disconnect();
|
|
266
|
+
} catch (e) {
|
|
267
|
+
// Observer may already be disconnected
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
disconnectInteractionListeners();
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Auto-disconnect LCP observer on user interaction (per Web Vitals spec)
|
|
274
|
+
// CLS and INP continue tracking until visibility change or deregister
|
|
275
|
+
const disconnectLCP = () => {
|
|
276
|
+
try {
|
|
277
|
+
lcpObs?.disconnect();
|
|
278
|
+
resizeObs?.disconnect();
|
|
279
|
+
} catch (e) {
|
|
280
|
+
// Observer may already be disconnected
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const handleInteraction = event => {
|
|
284
|
+
if (!isInMFE(event?.target, id)) return;
|
|
285
|
+
disconnectLCP();
|
|
286
|
+
disconnectInteractionListeners();
|
|
287
|
+
};
|
|
288
|
+
interactionEvents.forEach(type => {
|
|
289
|
+
_runtime.globalScope.addEventListener(type, handleInteraction, {
|
|
290
|
+
passive: true
|
|
291
|
+
});
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
// Disconnect all observers on visibility change or page unload
|
|
295
|
+
;
|
|
296
|
+
['visibilitychange', 'pagehide'].forEach(type => {
|
|
297
|
+
_runtime.globalScope.addEventListener(type, vitals.disconnect, {
|
|
298
|
+
once: true,
|
|
299
|
+
passive: true
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
return vitals;
|
|
303
|
+
}
|
|
@@ -261,7 +261,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
261
261
|
};
|
|
262
262
|
this.addEvent(event, target);
|
|
263
263
|
}, this.featureName, this.ee);
|
|
264
|
-
if (agentRef.init.feature_flags.includes('websockets')) {
|
|
264
|
+
if (agentRef.init.feature_flags.includes('websockets') || agentRef.init.web_sockets?.enabled) {
|
|
265
265
|
(0, _registerHandler.registerHandler)('ws-complete', nrData => {
|
|
266
266
|
const event = {
|
|
267
267
|
...nrData,
|
|
@@ -30,7 +30,7 @@ class Instrument extends _instrumentBase.InstrumentBase {
|
|
|
30
30
|
static featureName = _constants.FEATURE_NAME;
|
|
31
31
|
constructor(agentRef) {
|
|
32
32
|
super(agentRef, _constants.FEATURE_NAME);
|
|
33
|
-
const websocketsEnabled = agentRef.init.feature_flags.includes('websockets');
|
|
33
|
+
const websocketsEnabled = agentRef.init.feature_flags.includes('websockets') || agentRef.init.web_sockets?.enabled;
|
|
34
34
|
const securityPolicyViolationEnabled = !agentRef.init.feature_flags.includes('no_spv');
|
|
35
35
|
|
|
36
36
|
/** config values that gate whether the generic events aggregator should be imported at all */
|
|
@@ -61,7 +61,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
61
61
|
}; // likewise, don't want to overwrite the mode if it was set already
|
|
62
62
|
const session = this.agentRef.runtime.session;
|
|
63
63
|
if ((0, _featureGates.canEnableSessionTracking)(agentRef.init) && session) {
|
|
64
|
-
if (session.
|
|
64
|
+
if (session.state.loggingMode === null || session.state.logApiMode === null) this.#writeToStorage(this.loggingMode);else updateLocalLoggingMode(session.state.loggingMode, session.state.logApiMode);
|
|
65
65
|
}
|
|
66
66
|
if (this.loggingMode.auto === _constants.LOGGING_MODE.OFF && this.loggingMode.api === _constants.LOGGING_MODE.OFF) {
|
|
67
67
|
this.blocked = true;
|
|
@@ -169,13 +169,13 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
169
169
|
auto: _constants.LOGGING_MODE.OFF,
|
|
170
170
|
api: _constants.LOGGING_MODE.OFF
|
|
171
171
|
};
|
|
172
|
-
this.#
|
|
172
|
+
this.#writeToStorage(this.loggingMode);
|
|
173
173
|
this.deregisterDrain();
|
|
174
174
|
}
|
|
175
|
-
#
|
|
175
|
+
#writeToStorage(logModes) {
|
|
176
176
|
this.agentRef.runtime.session?.write({
|
|
177
|
-
loggingMode:
|
|
178
|
-
logApiMode:
|
|
177
|
+
loggingMode: logModes.auto,
|
|
178
|
+
logApiMode: logModes.api
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
181
|
}
|
|
@@ -17,8 +17,9 @@ var _firstPaint = require("../../../common/vitals/first-paint");
|
|
|
17
17
|
var _timeToFirstByte = require("../../../common/vitals/time-to-first-byte");
|
|
18
18
|
var _now = require("../../../common/timing/now");
|
|
19
19
|
var _timeKeeper = require("../../../common/timing/time-keeper");
|
|
20
|
+
var _send = require("../../../common/harvest/send");
|
|
21
|
+
var _featureGates = require("../../utils/feature-gates");
|
|
20
22
|
var _obfuscate = require("../../../common/util/obfuscate");
|
|
21
|
-
var _harvester = require("../../../common/harvest/harvester");
|
|
22
23
|
var _features = require("../../../loaders/features/features");
|
|
23
24
|
var _submitData = require("../../../common/util/submit-data");
|
|
24
25
|
var _webdriverDetection = require("../../../common/util/webdriver-detection");
|
|
@@ -33,6 +34,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
33
34
|
static featureName = CONSTANTS.FEATURE_NAME;
|
|
34
35
|
constructor(agentRef) {
|
|
35
36
|
super(agentRef, CONSTANTS.FEATURE_NAME);
|
|
37
|
+
this.isSessionTrackingEnabled = (0, _featureGates.canEnableSessionTracking)(agentRef.init) && !!agentRef.runtime.session;
|
|
36
38
|
this.sentRum = false; // flag to facilitate calling sendRum() once externally (by the consent API in agent-session.js)
|
|
37
39
|
|
|
38
40
|
this.timeToFirstByte = 0;
|
|
@@ -44,6 +46,19 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
44
46
|
this.obfuscator = new _obfuscate.Obfuscator(agentRef, _events.EVENT_TYPES.PVE);
|
|
45
47
|
agentRef.runtime.timeKeeper = new _timeKeeper.TimeKeeper(agentRef.runtime.session);
|
|
46
48
|
if (_runtime.isBrowserScope) {
|
|
49
|
+
const cached = this.isSessionTrackingEnabled && agentRef.runtime.session?.state.cachedRumResponse;
|
|
50
|
+
if (cached) {
|
|
51
|
+
const {
|
|
52
|
+
app: cachedApp,
|
|
53
|
+
...cachedFlags
|
|
54
|
+
} = cached;
|
|
55
|
+
|
|
56
|
+
// set the agent runtime objects that require the rum response or entity guid
|
|
57
|
+
if (!Object.keys(this.agentRef.runtime.appMetadata).length) agentRef.runtime.appMetadata = cachedApp;
|
|
58
|
+
this.drain();
|
|
59
|
+
agentRef.runtime.harvester.startTimer();
|
|
60
|
+
(0, _featureFlags.activateFeatures)(cachedFlags, agentRef);
|
|
61
|
+
}
|
|
47
62
|
_timeToFirstByte.timeToFirstByte.subscribe(({
|
|
48
63
|
value,
|
|
49
64
|
attrs
|
|
@@ -88,8 +103,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
88
103
|
ua: info.userAttributes,
|
|
89
104
|
at: info.atts
|
|
90
105
|
};
|
|
91
|
-
if (this.agentRef.runtime.session) queryParameters.fsh = Number(this.agentRef.runtime.session.
|
|
92
|
-
|
|
106
|
+
if (this.agentRef.runtime.session) queryParameters.fsh = Number(!this.agentRef.runtime.session.state.cachedRumResponse);
|
|
93
107
|
let body = this.obfuscator.traverseAndObfuscateEvents({
|
|
94
108
|
ja: {
|
|
95
109
|
...customAttributes,
|
|
@@ -143,6 +157,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
143
157
|
}) {
|
|
144
158
|
const rumEndTime = (0, _now.now)();
|
|
145
159
|
let app, flags;
|
|
160
|
+
const hasCachedRumResponse = !!this.agentRef.runtime.session?.state.cachedRumResponse;
|
|
146
161
|
try {
|
|
147
162
|
({
|
|
148
163
|
app,
|
|
@@ -152,10 +167,20 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
152
167
|
// wont set entity stuff here, if main agent will later abort, if registered agent, nothing will happen
|
|
153
168
|
(0, _console.warn)(53, error);
|
|
154
169
|
}
|
|
170
|
+
const shouldCacheResponse = this.isSessionTrackingEnabled && !hasCachedRumResponse && !!app;
|
|
171
|
+
if (hasCachedRumResponse) {
|
|
172
|
+
let {
|
|
173
|
+
app: cachedApp,
|
|
174
|
+
...cachedFlags
|
|
175
|
+
} = this.agentRef.runtime.session.state.cachedRumResponse;
|
|
176
|
+
app ??= cachedApp;
|
|
177
|
+
flags = cachedFlags;
|
|
178
|
+
}
|
|
155
179
|
super.postHarvestCleanup({
|
|
156
180
|
sent,
|
|
157
|
-
retry
|
|
181
|
+
retry: retry && !hasCachedRumResponse
|
|
158
182
|
}); // this will set isRetrying & re-buffer the body if request is to be retried
|
|
183
|
+
|
|
159
184
|
if (this.isRetrying && this.retries++ < 1) {
|
|
160
185
|
// Only retry once
|
|
161
186
|
setTimeout(() => this.agentRef.runtime.harvester.triggerHarvestFor(this, {
|
|
@@ -165,7 +190,6 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
165
190
|
}
|
|
166
191
|
if (status >= 400 || status === 0) {
|
|
167
192
|
(0, _console.warn)(18, status);
|
|
168
|
-
this.blocked = true;
|
|
169
193
|
|
|
170
194
|
// Get estimated payload size of our backlog
|
|
171
195
|
const textEncoder = new TextEncoder();
|
|
@@ -176,15 +200,24 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
176
200
|
}, 0);
|
|
177
201
|
const BCSError = 'BCS/Error/';
|
|
178
202
|
// Send SMs about failed RUM request
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
203
|
+
const sm = [{
|
|
204
|
+
params: {
|
|
205
|
+
name: BCSError + status
|
|
206
|
+
},
|
|
207
|
+
stats: {
|
|
208
|
+
c: 1
|
|
209
|
+
}
|
|
210
|
+
}, {
|
|
211
|
+
params: {
|
|
212
|
+
name: BCSError + 'Duration/Ms'
|
|
213
|
+
},
|
|
214
|
+
stats: {
|
|
215
|
+
c: 1,
|
|
216
|
+
t: rumEndTime - this.rumStartTime
|
|
217
|
+
}
|
|
218
|
+
}];
|
|
219
|
+
if (!hasCachedRumResponse) {
|
|
220
|
+
sm.push({
|
|
188
221
|
params: {
|
|
189
222
|
name: BCSError + 'Dropped/Bytes'
|
|
190
223
|
},
|
|
@@ -192,28 +225,30 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
192
225
|
c: 1,
|
|
193
226
|
t: payloadSize
|
|
194
227
|
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
},
|
|
199
|
-
stats: {
|
|
200
|
-
c: 1,
|
|
201
|
-
t: rumEndTime - this.rumStartTime
|
|
202
|
-
}
|
|
203
|
-
}]
|
|
204
|
-
};
|
|
205
|
-
(0, _harvester.send)(this.agentRef, {
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
(0, _send.send)(this.agentRef, {
|
|
206
231
|
endpoint: _features.FEATURE_TO_ENDPOINT[_features.FEATURE_NAMES.metrics],
|
|
207
232
|
payload: {
|
|
208
|
-
body
|
|
233
|
+
body: {
|
|
234
|
+
sm
|
|
235
|
+
}
|
|
209
236
|
},
|
|
210
237
|
submitMethod: (0, _submitData.getSubmitMethod)(),
|
|
211
238
|
featureName: _features.FEATURE_NAMES.metrics
|
|
212
239
|
});
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
240
|
+
if (!hasCachedRumResponse) {
|
|
241
|
+
this.blocked = true;
|
|
242
|
+
this.ee.abort();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
} else if (shouldCacheResponse) {
|
|
246
|
+
this.agentRef.runtime.session.write({
|
|
247
|
+
cachedRumResponse: {
|
|
248
|
+
app,
|
|
249
|
+
...flags
|
|
250
|
+
}
|
|
251
|
+
});
|
|
217
252
|
}
|
|
218
253
|
try {
|
|
219
254
|
const wasReady = this.agentRef.runtime.timeKeeper.ready;
|
|
@@ -30,7 +30,7 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
|
|
|
30
30
|
*/
|
|
31
31
|
class Aggregate extends _aggregateBase.AggregateBase {
|
|
32
32
|
static featureName = _constants.FEATURE_NAME;
|
|
33
|
-
mode =
|
|
33
|
+
mode = null;
|
|
34
34
|
|
|
35
35
|
// pass the recorder into the aggregator
|
|
36
36
|
constructor(agentRef, args) {
|
|
@@ -75,7 +75,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
75
75
|
if (!this.recorder) return;
|
|
76
76
|
// if the mode changed on a different tab, it needs to update this instance to match
|
|
77
77
|
this.mode = agentRef.runtime.session.state.sessionReplayMode;
|
|
78
|
-
if (!this.initialized || this.mode === _constants2.MODE.OFF) return;
|
|
78
|
+
if (!this.initialized || this.mode === _constants2.MODE.OFF || !this.mode) return;
|
|
79
79
|
this.recorder?.startRecording(_constants.TRIGGERS.RESUME, this.mode);
|
|
80
80
|
});
|
|
81
81
|
this.ee.on(_constants2.SESSION_EVENTS.UPDATE, (type, data) => {
|
|
@@ -102,6 +102,10 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
102
102
|
this.waitForFlags(['srs', 'sr']).then(([srMode, entitled]) => {
|
|
103
103
|
this.entitled = !!entitled;
|
|
104
104
|
if (!this.entitled) {
|
|
105
|
+
this.mode = _constants2.MODE.OFF;
|
|
106
|
+
this.#writeToStorage({
|
|
107
|
+
sessionReplayMode: this.mode
|
|
108
|
+
});
|
|
105
109
|
this.deregisterDrain();
|
|
106
110
|
if (this.agentRef.runtime.isRecording) {
|
|
107
111
|
this.abort(_constants.ABORT_REASONS.ENTITLEMENTS);
|
|
@@ -113,7 +117,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
113
117
|
this.drain();
|
|
114
118
|
});
|
|
115
119
|
}).then(() => {
|
|
116
|
-
if (this.mode
|
|
120
|
+
if (!this.mode) {
|
|
117
121
|
this.recorder?.stopRecording(); // stop any conservative preload recording launched by instrument
|
|
118
122
|
while (this.recorder?.getEvents().events.length) this.recorder?.clearBuffer?.();
|
|
119
123
|
}
|
|
@@ -146,7 +150,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
146
150
|
// if the error was noticed AFTER the recorder was already imported....
|
|
147
151
|
if (this.recorder && this.initialized) {
|
|
148
152
|
if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(_constants.TRIGGERS.SWITCH_TO_FULL, this.mode); // off --> full
|
|
149
|
-
this
|
|
153
|
+
this.#writeToStorage({
|
|
150
154
|
sessionReplayMode: this.mode
|
|
151
155
|
});
|
|
152
156
|
} else {
|
|
@@ -178,35 +182,35 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
178
182
|
this.timeKeeper = timeKeeper;
|
|
179
183
|
if (this.recorder?.trigger === _constants.TRIGGERS.API && this.agentRef.runtime.isRecording) {
|
|
180
184
|
this.mode = _constants2.MODE.FULL;
|
|
181
|
-
} else if (
|
|
185
|
+
} else if (session.state.sessionReplayMode !== null && !ignoreSession) {
|
|
182
186
|
// inherit the mode of the existing session
|
|
183
187
|
this.mode = session.state.sessionReplayMode;
|
|
184
188
|
} else {
|
|
185
189
|
// The session is new... determine the mode the new session should start in
|
|
186
190
|
this.mode = srMode;
|
|
187
191
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
192
|
+
if (this.mode !== _constants2.MODE.OFF) {
|
|
193
|
+
try {
|
|
194
|
+
/** will return a recorder instance if already imported, otherwise, will fetch the recorder and initialize it */
|
|
195
|
+
this.recorder ??= await this.instrumentClass.importRecorder();
|
|
196
|
+
} catch (err) {
|
|
197
|
+
/** if the recorder fails to import, abort the feature */
|
|
198
|
+
return this.abort(_constants.ABORT_REASONS.IMPORT, err);
|
|
199
|
+
}
|
|
197
200
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
201
|
+
// If an error was noticed before the mode could be set (like in the early lifecycle of the page), immediately set to FULL mode
|
|
202
|
+
if (this.mode === _constants2.MODE.ERROR && this.instrumentClass.errorNoticed) {
|
|
203
|
+
this.mode = _constants2.MODE.FULL;
|
|
204
|
+
}
|
|
202
205
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
+
// FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
|
|
207
|
+
// ERROR mode will do this until an error is thrown, and then switch into FULL mode.
|
|
208
|
+
// The makeHarvestPayload should ensure that no payload is returned if we're not in FULL mode...
|
|
206
209
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
+
await this.prepUtils();
|
|
211
|
+
if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(trigger, this.mode);
|
|
212
|
+
}
|
|
213
|
+
this.#writeToStorage({
|
|
210
214
|
sessionReplayMode: this.mode
|
|
211
215
|
});
|
|
212
216
|
}
|
|
@@ -253,7 +257,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
253
257
|
this.abort(_constants.ABORT_REASONS.TOO_BIG, len);
|
|
254
258
|
return;
|
|
255
259
|
}
|
|
256
|
-
if (!this.agentRef.runtime.session.state.sessionReplaySentFirstChunk) this
|
|
260
|
+
if (!this.agentRef.runtime.session.state.sessionReplaySentFirstChunk) this.#writeToStorage({
|
|
257
261
|
sessionReplaySentFirstChunk: true
|
|
258
262
|
});
|
|
259
263
|
this.recorder.clearBuffer();
|
|
@@ -382,7 +386,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
382
386
|
if (forceHarvest) this.agentRef.runtime.harvester.triggerHarvestFor(this);
|
|
383
387
|
this.mode = _constants2.MODE.OFF;
|
|
384
388
|
this.recorder?.stopRecording?.();
|
|
385
|
-
this
|
|
389
|
+
this.#writeToStorage({
|
|
386
390
|
sessionReplayMode: this.mode
|
|
387
391
|
});
|
|
388
392
|
}
|
|
@@ -394,13 +398,13 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
394
398
|
this.blocked = true;
|
|
395
399
|
this.mode = _constants2.MODE.OFF;
|
|
396
400
|
this.recorder?.stopRecording?.();
|
|
397
|
-
this
|
|
401
|
+
this.#writeToStorage({
|
|
398
402
|
sessionReplayMode: this.mode
|
|
399
403
|
});
|
|
400
404
|
this.recorder?.clearTimestamps?.();
|
|
401
405
|
while (this.recorder?.getEvents().events.length) this.recorder?.clearBuffer?.();
|
|
402
406
|
}
|
|
403
|
-
|
|
407
|
+
#writeToStorage(state = {}) {
|
|
404
408
|
if (this.isSessionTrackingEnabled) {
|
|
405
409
|
this.agentRef.runtime.session.write(state);
|
|
406
410
|
}
|