@newrelic/browser-agent 1.318.0-rc.9 → 1.319.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 +15 -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/v2/mfe-vitals.js +59 -24
- package/dist/cjs/loaders/api/register-api-types.js +7 -0
- package/dist/cjs/loaders/api/register.js +13 -5
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/v2/mfe-vitals.js +59 -24
- package/dist/esm/loaders/api/register-api-types.js +8 -0
- package/dist/esm/loaders/api/register.js +13 -5
- package/dist/types/common/v2/mfe-vitals.d.ts +7 -10
- package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -1
- package/dist/types/loaders/api/register-api-types.d.ts +18 -0
- package/package.json +1 -1
- package/src/common/v2/mfe-vitals.js +57 -24
- package/src/loaders/api/register-api-types.js +8 -0
- package/src/loaders/api/register.js +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [1.319.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.318.0...v1.319.0) (2026-07-28)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* Automatically Detect MFE Web Sockets ([#1726](https://github.com/newrelic/newrelic-browser-agent/issues/1726)) ([746d055](https://github.com/newrelic/newrelic-browser-agent/commit/746d05504c451b954cd0d2b86b92415c22293f2a))
|
|
12
|
+
* Do not obfuscate attribute keys ([#1810](https://github.com/newrelic/newrelic-browser-agent/issues/1810)) ([c5d9a77](https://github.com/newrelic/newrelic-browser-agent/commit/c5d9a773470d670cb27b63777dd035934f1dbfd9))
|
|
13
|
+
* MFE vitals improvements ([#1813](https://github.com/newrelic/newrelic-browser-agent/issues/1813)) ([21f38a9](https://github.com/newrelic/newrelic-browser-agent/commit/21f38a9af35f5204a47be02bfa2a73d41d8ba1f6))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* Avoid capturing agent AJAX payloads ([#1812](https://github.com/newrelic/newrelic-browser-agent/issues/1812)) ([ba069fc](https://github.com/newrelic/newrelic-browser-agent/commit/ba069fcea5f381db698044b84064bbba97676cec))
|
|
19
|
+
* Keep Ajax payload out of unrelated events ([#1809](https://github.com/newrelic/newrelic-browser-agent/issues/1809)) ([b0cb098](https://github.com/newrelic/newrelic-browser-agent/commit/b0cb098d0ea36d1d35212355bc51051f3c02c1ae))
|
|
20
|
+
|
|
6
21
|
## [1.318.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.317.0...v1.318.0) (2026-07-08)
|
|
7
22
|
|
|
8
23
|
|
|
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.RRWEB_PACKAGE_NAME = exports.D
|
|
|
17
17
|
/**
|
|
18
18
|
* Exposes the version of the agent
|
|
19
19
|
*/
|
|
20
|
-
const VERSION = exports.VERSION = "1.
|
|
20
|
+
const VERSION = exports.VERSION = "1.319.0-rc.0";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Exposes the build type of the agent
|
|
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.RRWEB_PACKAGE_NAME = exports.D
|
|
|
17
17
|
/**
|
|
18
18
|
* Exposes the version of the agent
|
|
19
19
|
*/
|
|
20
|
-
const VERSION = exports.VERSION = "1.
|
|
20
|
+
const VERSION = exports.VERSION = "1.319.0-rc.0";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Exposes the build type of the agent
|
|
@@ -12,7 +12,9 @@ var _now = require("../timing/now");
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* @typedef {import('
|
|
15
|
+
* @typedef {import('../../loaders/api/register-api-types').RegisterAPITimings} RegisterAPITimings
|
|
16
|
+
* @typedef {import('../../loaders/api/register-api-types').RegisterAPITarget} RegisterAPITarget
|
|
17
|
+
* @typedef {import('../../loaders/api/register-api-types').RegisterAPIVitals} RegisterAPIVitals
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
const isObservable = node => {
|
|
@@ -22,6 +24,15 @@ const isObservable = node => {
|
|
|
22
24
|
return false;
|
|
23
25
|
}
|
|
24
26
|
};
|
|
27
|
+
const isMatch = (dataset, target) => dataset?.nrMfeId === target.id && (!dataset?.nrMfeObserved || dataset?.nrMfeObserved === target.instance);
|
|
28
|
+
const escapeSelectorValue = value => {
|
|
29
|
+
try {
|
|
30
|
+
return _runtime.globalScope.CSS.escape(value);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
// give up
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
25
36
|
|
|
26
37
|
/**
|
|
27
38
|
* Check if node is within a specific MFE
|
|
@@ -29,12 +40,17 @@ const isObservable = node => {
|
|
|
29
40
|
* @param {string} id - MFE ID to match
|
|
30
41
|
* @returns {boolean}
|
|
31
42
|
*/
|
|
32
|
-
const isInMFE = (node,
|
|
33
|
-
|
|
43
|
+
const isInMFE = (node, target = {}) => {
|
|
44
|
+
const id = target.id;
|
|
45
|
+
const instance = target.instance;
|
|
46
|
+
if (!node || !id || !instance) return false;
|
|
34
47
|
try {
|
|
35
48
|
let curr = node.nodeType === 1 ? node : node.parentElement;
|
|
36
49
|
while (curr?.tagName) {
|
|
37
|
-
if (curr.dataset
|
|
50
|
+
if (isMatch(curr.dataset, target)) {
|
|
51
|
+
curr.dataset.nrMfeObserved = instance; // mark that this MFE has been observed for vitals
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
38
54
|
curr = curr.parentNode;
|
|
39
55
|
}
|
|
40
56
|
} catch (e) {}
|
|
@@ -47,16 +63,19 @@ const isInMFE = (node, id) => {
|
|
|
47
63
|
* @param {Function} onMatch - Callback when matching node is *added*
|
|
48
64
|
* @returns {MutationObserver}
|
|
49
65
|
*/
|
|
50
|
-
const observeMutations = (
|
|
66
|
+
const observeMutations = (target, onMatch) => {
|
|
51
67
|
// Try to find existing MFE root
|
|
52
|
-
const
|
|
68
|
+
const potentialMatches = _runtime.globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
|
|
69
|
+
const mfeRoot = (Array.from(potentialMatches) || []).find(x => isMatch(x.dataset, target));
|
|
53
70
|
let observingRoot = !!mfeRoot;
|
|
71
|
+
if (observingRoot) mfeRoot.dataset.nrMfeObserved ??= target.instance; // mark that this MFE has been observed for vitals
|
|
72
|
+
|
|
54
73
|
const obs = new _runtime.globalScope.MutationObserver(mutations => {
|
|
55
74
|
mutations.forEach(m => {
|
|
56
75
|
m.addedNodes.forEach(node => {
|
|
57
76
|
// Check if this is the MFE root being added
|
|
58
77
|
const elem = node.nodeType === 1 ? node : null;
|
|
59
|
-
if (elem?.dataset
|
|
78
|
+
if (!observingRoot && isMatch(elem?.dataset, target)) {
|
|
60
79
|
// Found the root! Lets switch to observing just this subtree for performance reasons
|
|
61
80
|
obs.disconnect();
|
|
62
81
|
obs.observe(elem, {
|
|
@@ -64,10 +83,11 @@ const observeMutations = (id, onMatch) => {
|
|
|
64
83
|
subtree: true
|
|
65
84
|
});
|
|
66
85
|
observingRoot = true;
|
|
86
|
+
elem.dataset.nrMfeObserved = target.instance; // mark that this MFE has been observed for vitals
|
|
67
87
|
}
|
|
68
88
|
|
|
69
89
|
// Only check isInMFE if we're observing the whole document; skip expensive ancestor walk when observing root
|
|
70
|
-
if (isObservable(node) && (observingRoot || isInMFE(node,
|
|
90
|
+
if (isObservable(node) && (observingRoot || isInMFE(node, target))) {
|
|
71
91
|
onMatch(node, obs);
|
|
72
92
|
}
|
|
73
93
|
});
|
|
@@ -104,14 +124,15 @@ const observePerformance = (observers, config, onEntry) => {
|
|
|
104
124
|
|
|
105
125
|
/**
|
|
106
126
|
* Tracks all Core Web Vitals for a specific MFE.
|
|
107
|
-
* @param {
|
|
108
|
-
* @
|
|
127
|
+
* @param {RegisterAPITarget} target - The MFE target to track vitals for
|
|
128
|
+
* @param {RegisterAPITimings} timings - The timings object to use for relative time calculations
|
|
129
|
+
* @returns {RegisterAPIVitals} An object containing the vitals values and a disconnect method to stop tracking
|
|
109
130
|
*/
|
|
110
|
-
function trackMFEVitals(
|
|
111
|
-
let fcpObservedAt
|
|
112
|
-
let lcpObservedAt
|
|
131
|
+
function trackMFEVitals(target, timings) {
|
|
132
|
+
let fcpObservedAt;
|
|
133
|
+
let lcpObservedAt;
|
|
113
134
|
const getTimeRelativeToScriptStart = capturedAt => {
|
|
114
|
-
if (capturedAt
|
|
135
|
+
if (capturedAt == null) return capturedAt;
|
|
115
136
|
return capturedAt - (timings?.scriptStart || timings?.registeredAt || 0);
|
|
116
137
|
};
|
|
117
138
|
const vitals = {
|
|
@@ -126,15 +147,21 @@ function trackMFEVitals(id, timings) {
|
|
|
126
147
|
}
|
|
127
148
|
},
|
|
128
149
|
cls: {
|
|
129
|
-
value:
|
|
150
|
+
value: undefined
|
|
130
151
|
},
|
|
131
152
|
inp: {
|
|
132
|
-
value:
|
|
153
|
+
value: undefined
|
|
133
154
|
},
|
|
134
155
|
disconnect: () => {}
|
|
135
156
|
};
|
|
136
|
-
if (!
|
|
157
|
+
if (!target || !_runtime.isBrowserScope || !_runtime.globalScope.MutationObserver || !_runtime.globalScope.PerformanceObserver) return vitals;
|
|
137
158
|
const observers = [];
|
|
159
|
+
|
|
160
|
+
// If FCP hasn't been observed within 10 seconds, give up and shut down all observers.
|
|
161
|
+
// Once FCP is observed, the other vitals are left to record until their natural lifespan ends.
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
if (!fcpObservedAt) vitals.disconnect();
|
|
164
|
+
}, 10000);
|
|
138
165
|
const populateVitalMinimums = () => {
|
|
139
166
|
fcpObservedAt ??= (0, _now.now)();
|
|
140
167
|
lcpObservedAt ??= (0, _now.now)();
|
|
@@ -142,14 +169,16 @@ function trackMFEVitals(id, timings) {
|
|
|
142
169
|
};
|
|
143
170
|
|
|
144
171
|
// if the MFE has already rendered something on the page before we could set up listeners, just populate vital minimums immediately
|
|
145
|
-
|
|
172
|
+
const existingRoots = _runtime.globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
|
|
173
|
+
if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums();
|
|
146
174
|
|
|
147
175
|
// Track FCP - first contentful paint
|
|
148
|
-
observeMutations(
|
|
176
|
+
const fcpObs = observeMutations(target, (_, obs) => {
|
|
149
177
|
// An observed "FCP" means _something_ rendered, so at minimum we can populate all the baseline values for the vitals
|
|
150
178
|
populateVitalMinimums();
|
|
151
179
|
obs.disconnect();
|
|
152
180
|
});
|
|
181
|
+
observers.push(fcpObs);
|
|
153
182
|
|
|
154
183
|
// Track LCP - largest contentful paint
|
|
155
184
|
let largestSize = 0;
|
|
@@ -173,7 +202,7 @@ function trackMFEVitals(id, timings) {
|
|
|
173
202
|
} catch (e) {
|
|
174
203
|
// ResizeObserver not supported
|
|
175
204
|
}
|
|
176
|
-
const lcpObs = observeMutations(
|
|
205
|
+
const lcpObs = observeMutations(target, node => {
|
|
177
206
|
// an observed "LCP" means _something_ rendered, so at minimum we can make sure all the baseline values are populated for the vitals
|
|
178
207
|
populateVitalMinimums();
|
|
179
208
|
if (resizeObs) {
|
|
@@ -225,7 +254,7 @@ function trackMFEVitals(id, timings) {
|
|
|
225
254
|
}, entry => {
|
|
226
255
|
if (entry.hadRecentInput) return;
|
|
227
256
|
(entry.sources || []).some(source => {
|
|
228
|
-
if (isInMFE(source.node,
|
|
257
|
+
if (isInMFE(source.node, target)) {
|
|
229
258
|
// 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
259
|
populateVitalMinimums();
|
|
231
260
|
vitals.cls.value += entry.value;
|
|
@@ -241,8 +270,8 @@ function trackMFEVitals(id, timings) {
|
|
|
241
270
|
buffered: true,
|
|
242
271
|
durationThreshold: 40
|
|
243
272
|
}, entry => {
|
|
244
|
-
if (!entry.interactionId || !isInMFE(entry.target,
|
|
245
|
-
if (vitals.inp.value ===
|
|
273
|
+
if (!entry.interactionId || !isInMFE(entry.target, target)) return;
|
|
274
|
+
if (vitals.inp.value === undefined || entry.duration > vitals.inp.value) {
|
|
246
275
|
// 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
276
|
populateVitalMinimums();
|
|
248
277
|
vitals.inp.value = entry.duration;
|
|
@@ -268,6 +297,12 @@ function trackMFEVitals(id, timings) {
|
|
|
268
297
|
}
|
|
269
298
|
});
|
|
270
299
|
disconnectInteractionListeners();
|
|
300
|
+
['visibilitychange', 'pagehide'].forEach(type => {
|
|
301
|
+
_runtime.globalScope.removeEventListener(type, vitals.disconnect, {
|
|
302
|
+
once: true,
|
|
303
|
+
passive: true
|
|
304
|
+
});
|
|
305
|
+
});
|
|
271
306
|
};
|
|
272
307
|
|
|
273
308
|
// Auto-disconnect LCP observer on user interaction (per Web Vitals spec)
|
|
@@ -281,7 +316,7 @@ function trackMFEVitals(id, timings) {
|
|
|
281
316
|
}
|
|
282
317
|
};
|
|
283
318
|
const handleInteraction = event => {
|
|
284
|
-
if (!isInMFE(event?.target,
|
|
319
|
+
if (!isInMFE(event?.target, target)) return;
|
|
285
320
|
disconnectLCP();
|
|
286
321
|
disconnectInteractionListeners();
|
|
287
322
|
};
|
|
@@ -53,4 +53,11 @@ exports.default = void 0;
|
|
|
53
53
|
* @property {Object} [asset] - The asset path (if found) for the registered entity.
|
|
54
54
|
* @property {string} type - The type of timing associated with the registered entity, 'script' or 'link' if found with the performance resource API, 'fetch' for dynamic imports, 'inline' if found to be associated with the root document URL, or 'unknown' if no associated resource could be found.
|
|
55
55
|
*/
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {Object} RegisterAPIVitals
|
|
58
|
+
* @property {number} [fcp] - The first contentful paint timing for the registered entity.
|
|
59
|
+
* @property {number} [lcp] - The largest contentful paint timing for the registered entity.
|
|
60
|
+
* @property {number} [cls] - The cumulative layout shift score for the registered entity.
|
|
61
|
+
* @property {number} [inp] - The interaction to next paint timing for the registered entity.
|
|
62
|
+
*/
|
|
56
63
|
var _default = exports.default = {};
|
|
@@ -87,7 +87,7 @@ function register(agentRef, target) {
|
|
|
87
87
|
const timings = (0, _scriptTracker.findScriptTimings)();
|
|
88
88
|
|
|
89
89
|
// Track MFE vitals for this entity
|
|
90
|
-
const vitals = (0, _mfeVitals.trackMFEVitals)(target
|
|
90
|
+
const vitals = (0, _mfeVitals.trackMFEVitals)(target, timings);
|
|
91
91
|
const attrs = {};
|
|
92
92
|
|
|
93
93
|
// Only define attributes getter if it doesn't already exist
|
|
@@ -233,13 +233,21 @@ function register(agentRef, target) {
|
|
|
233
233
|
timeToRegister: timings.registeredAt,
|
|
234
234
|
// timestamp when register() was called
|
|
235
235
|
// leave room to extend these with more data keys as needed
|
|
236
|
-
|
|
236
|
+
...(vitals.fcp.value >= 0 && {
|
|
237
|
+
'nr.vitals.fcp.value': vitals.fcp.value
|
|
238
|
+
}),
|
|
237
239
|
// FCP vital object with value and metadata
|
|
238
|
-
|
|
240
|
+
...(vitals.lcp.value >= 0 && {
|
|
241
|
+
'nr.vitals.lcp.value': vitals.lcp.value
|
|
242
|
+
}),
|
|
239
243
|
// LCP vital object with value and metadata
|
|
240
|
-
|
|
244
|
+
...(vitals.cls.value >= 0 && {
|
|
245
|
+
'nr.vitals.cls.value': vitals.cls.value
|
|
246
|
+
}),
|
|
241
247
|
// CLS vital object with value and metadata
|
|
242
|
-
|
|
248
|
+
...(vitals.inp.value >= 0 && {
|
|
249
|
+
'nr.vitals.inp.value': vitals.inp.value
|
|
250
|
+
}) // INP vital object with value and metadata
|
|
243
251
|
};
|
|
244
252
|
api.recordCustomEvent('MicroFrontEndTiming', eventData);
|
|
245
253
|
}
|
|
@@ -7,7 +7,9 @@ import { globalScope, isBrowserScope } from '../constants/runtime';
|
|
|
7
7
|
import { now } from '../timing/now';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* @typedef {import('
|
|
10
|
+
* @typedef {import('../../loaders/api/register-api-types').RegisterAPITimings} RegisterAPITimings
|
|
11
|
+
* @typedef {import('../../loaders/api/register-api-types').RegisterAPITarget} RegisterAPITarget
|
|
12
|
+
* @typedef {import('../../loaders/api/register-api-types').RegisterAPIVitals} RegisterAPIVitals
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
const isObservable = node => {
|
|
@@ -17,6 +19,15 @@ const isObservable = node => {
|
|
|
17
19
|
return false;
|
|
18
20
|
}
|
|
19
21
|
};
|
|
22
|
+
const isMatch = (dataset, target) => dataset?.nrMfeId === target.id && (!dataset?.nrMfeObserved || dataset?.nrMfeObserved === target.instance);
|
|
23
|
+
const escapeSelectorValue = value => {
|
|
24
|
+
try {
|
|
25
|
+
return globalScope.CSS.escape(value);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
// give up
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
20
31
|
|
|
21
32
|
/**
|
|
22
33
|
* Check if node is within a specific MFE
|
|
@@ -24,12 +35,17 @@ const isObservable = node => {
|
|
|
24
35
|
* @param {string} id - MFE ID to match
|
|
25
36
|
* @returns {boolean}
|
|
26
37
|
*/
|
|
27
|
-
const isInMFE = (node,
|
|
28
|
-
|
|
38
|
+
const isInMFE = (node, target = {}) => {
|
|
39
|
+
const id = target.id;
|
|
40
|
+
const instance = target.instance;
|
|
41
|
+
if (!node || !id || !instance) return false;
|
|
29
42
|
try {
|
|
30
43
|
let curr = node.nodeType === 1 ? node : node.parentElement;
|
|
31
44
|
while (curr?.tagName) {
|
|
32
|
-
if (curr.dataset
|
|
45
|
+
if (isMatch(curr.dataset, target)) {
|
|
46
|
+
curr.dataset.nrMfeObserved = instance; // mark that this MFE has been observed for vitals
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
33
49
|
curr = curr.parentNode;
|
|
34
50
|
}
|
|
35
51
|
} catch (e) {}
|
|
@@ -42,16 +58,19 @@ const isInMFE = (node, id) => {
|
|
|
42
58
|
* @param {Function} onMatch - Callback when matching node is *added*
|
|
43
59
|
* @returns {MutationObserver}
|
|
44
60
|
*/
|
|
45
|
-
const observeMutations = (
|
|
61
|
+
const observeMutations = (target, onMatch) => {
|
|
46
62
|
// Try to find existing MFE root
|
|
47
|
-
const
|
|
63
|
+
const potentialMatches = globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
|
|
64
|
+
const mfeRoot = (Array.from(potentialMatches) || []).find(x => isMatch(x.dataset, target));
|
|
48
65
|
let observingRoot = !!mfeRoot;
|
|
66
|
+
if (observingRoot) mfeRoot.dataset.nrMfeObserved ??= target.instance; // mark that this MFE has been observed for vitals
|
|
67
|
+
|
|
49
68
|
const obs = new globalScope.MutationObserver(mutations => {
|
|
50
69
|
mutations.forEach(m => {
|
|
51
70
|
m.addedNodes.forEach(node => {
|
|
52
71
|
// Check if this is the MFE root being added
|
|
53
72
|
const elem = node.nodeType === 1 ? node : null;
|
|
54
|
-
if (elem?.dataset
|
|
73
|
+
if (!observingRoot && isMatch(elem?.dataset, target)) {
|
|
55
74
|
// Found the root! Lets switch to observing just this subtree for performance reasons
|
|
56
75
|
obs.disconnect();
|
|
57
76
|
obs.observe(elem, {
|
|
@@ -59,10 +78,11 @@ const observeMutations = (id, onMatch) => {
|
|
|
59
78
|
subtree: true
|
|
60
79
|
});
|
|
61
80
|
observingRoot = true;
|
|
81
|
+
elem.dataset.nrMfeObserved = target.instance; // mark that this MFE has been observed for vitals
|
|
62
82
|
}
|
|
63
83
|
|
|
64
84
|
// Only check isInMFE if we're observing the whole document; skip expensive ancestor walk when observing root
|
|
65
|
-
if (isObservable(node) && (observingRoot || isInMFE(node,
|
|
85
|
+
if (isObservable(node) && (observingRoot || isInMFE(node, target))) {
|
|
66
86
|
onMatch(node, obs);
|
|
67
87
|
}
|
|
68
88
|
});
|
|
@@ -99,14 +119,15 @@ const observePerformance = (observers, config, onEntry) => {
|
|
|
99
119
|
|
|
100
120
|
/**
|
|
101
121
|
* Tracks all Core Web Vitals for a specific MFE.
|
|
102
|
-
* @param {
|
|
103
|
-
* @
|
|
122
|
+
* @param {RegisterAPITarget} target - The MFE target to track vitals for
|
|
123
|
+
* @param {RegisterAPITimings} timings - The timings object to use for relative time calculations
|
|
124
|
+
* @returns {RegisterAPIVitals} An object containing the vitals values and a disconnect method to stop tracking
|
|
104
125
|
*/
|
|
105
|
-
export function trackMFEVitals(
|
|
106
|
-
let fcpObservedAt
|
|
107
|
-
let lcpObservedAt
|
|
126
|
+
export function trackMFEVitals(target, timings) {
|
|
127
|
+
let fcpObservedAt;
|
|
128
|
+
let lcpObservedAt;
|
|
108
129
|
const getTimeRelativeToScriptStart = capturedAt => {
|
|
109
|
-
if (capturedAt
|
|
130
|
+
if (capturedAt == null) return capturedAt;
|
|
110
131
|
return capturedAt - (timings?.scriptStart || timings?.registeredAt || 0);
|
|
111
132
|
};
|
|
112
133
|
const vitals = {
|
|
@@ -121,15 +142,21 @@ export function trackMFEVitals(id, timings) {
|
|
|
121
142
|
}
|
|
122
143
|
},
|
|
123
144
|
cls: {
|
|
124
|
-
value:
|
|
145
|
+
value: undefined
|
|
125
146
|
},
|
|
126
147
|
inp: {
|
|
127
|
-
value:
|
|
148
|
+
value: undefined
|
|
128
149
|
},
|
|
129
150
|
disconnect: () => {}
|
|
130
151
|
};
|
|
131
|
-
if (!
|
|
152
|
+
if (!target || !isBrowserScope || !globalScope.MutationObserver || !globalScope.PerformanceObserver) return vitals;
|
|
132
153
|
const observers = [];
|
|
154
|
+
|
|
155
|
+
// If FCP hasn't been observed within 10 seconds, give up and shut down all observers.
|
|
156
|
+
// Once FCP is observed, the other vitals are left to record until their natural lifespan ends.
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
if (!fcpObservedAt) vitals.disconnect();
|
|
159
|
+
}, 10000);
|
|
133
160
|
const populateVitalMinimums = () => {
|
|
134
161
|
fcpObservedAt ??= now();
|
|
135
162
|
lcpObservedAt ??= now();
|
|
@@ -137,14 +164,16 @@ export function trackMFEVitals(id, timings) {
|
|
|
137
164
|
};
|
|
138
165
|
|
|
139
166
|
// if the MFE has already rendered something on the page before we could set up listeners, just populate vital minimums immediately
|
|
140
|
-
|
|
167
|
+
const existingRoots = globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
|
|
168
|
+
if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums();
|
|
141
169
|
|
|
142
170
|
// Track FCP - first contentful paint
|
|
143
|
-
observeMutations(
|
|
171
|
+
const fcpObs = observeMutations(target, (_, obs) => {
|
|
144
172
|
// An observed "FCP" means _something_ rendered, so at minimum we can populate all the baseline values for the vitals
|
|
145
173
|
populateVitalMinimums();
|
|
146
174
|
obs.disconnect();
|
|
147
175
|
});
|
|
176
|
+
observers.push(fcpObs);
|
|
148
177
|
|
|
149
178
|
// Track LCP - largest contentful paint
|
|
150
179
|
let largestSize = 0;
|
|
@@ -168,7 +197,7 @@ export function trackMFEVitals(id, timings) {
|
|
|
168
197
|
} catch (e) {
|
|
169
198
|
// ResizeObserver not supported
|
|
170
199
|
}
|
|
171
|
-
const lcpObs = observeMutations(
|
|
200
|
+
const lcpObs = observeMutations(target, node => {
|
|
172
201
|
// an observed "LCP" means _something_ rendered, so at minimum we can make sure all the baseline values are populated for the vitals
|
|
173
202
|
populateVitalMinimums();
|
|
174
203
|
if (resizeObs) {
|
|
@@ -220,7 +249,7 @@ export function trackMFEVitals(id, timings) {
|
|
|
220
249
|
}, entry => {
|
|
221
250
|
if (entry.hadRecentInput) return;
|
|
222
251
|
(entry.sources || []).some(source => {
|
|
223
|
-
if (isInMFE(source.node,
|
|
252
|
+
if (isInMFE(source.node, target)) {
|
|
224
253
|
// 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
|
|
225
254
|
populateVitalMinimums();
|
|
226
255
|
vitals.cls.value += entry.value;
|
|
@@ -236,8 +265,8 @@ export function trackMFEVitals(id, timings) {
|
|
|
236
265
|
buffered: true,
|
|
237
266
|
durationThreshold: 40
|
|
238
267
|
}, entry => {
|
|
239
|
-
if (!entry.interactionId || !isInMFE(entry.target,
|
|
240
|
-
if (vitals.inp.value ===
|
|
268
|
+
if (!entry.interactionId || !isInMFE(entry.target, target)) return;
|
|
269
|
+
if (vitals.inp.value === undefined || entry.duration > vitals.inp.value) {
|
|
241
270
|
// 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
|
|
242
271
|
populateVitalMinimums();
|
|
243
272
|
vitals.inp.value = entry.duration;
|
|
@@ -263,6 +292,12 @@ export function trackMFEVitals(id, timings) {
|
|
|
263
292
|
}
|
|
264
293
|
});
|
|
265
294
|
disconnectInteractionListeners();
|
|
295
|
+
['visibilitychange', 'pagehide'].forEach(type => {
|
|
296
|
+
globalScope.removeEventListener(type, vitals.disconnect, {
|
|
297
|
+
once: true,
|
|
298
|
+
passive: true
|
|
299
|
+
});
|
|
300
|
+
});
|
|
266
301
|
};
|
|
267
302
|
|
|
268
303
|
// Auto-disconnect LCP observer on user interaction (per Web Vitals spec)
|
|
@@ -276,7 +311,7 @@ export function trackMFEVitals(id, timings) {
|
|
|
276
311
|
}
|
|
277
312
|
};
|
|
278
313
|
const handleInteraction = event => {
|
|
279
|
-
if (!isInMFE(event?.target,
|
|
314
|
+
if (!isInMFE(event?.target, target)) return;
|
|
280
315
|
disconnectLCP();
|
|
281
316
|
disconnectInteractionListeners();
|
|
282
317
|
};
|
|
@@ -53,4 +53,12 @@
|
|
|
53
53
|
* @property {string} type - The type of timing associated with the registered entity, 'script' or 'link' if found with the performance resource API, 'fetch' for dynamic imports, 'inline' if found to be associated with the root document URL, or 'unknown' if no associated resource could be found.
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {Object} RegisterAPIVitals
|
|
58
|
+
* @property {number} [fcp] - The first contentful paint timing for the registered entity.
|
|
59
|
+
* @property {number} [lcp] - The largest contentful paint timing for the registered entity.
|
|
60
|
+
* @property {number} [cls] - The cumulative layout shift score for the registered entity.
|
|
61
|
+
* @property {number} [inp] - The interaction to next paint timing for the registered entity.
|
|
62
|
+
*/
|
|
63
|
+
|
|
56
64
|
export default {};
|
|
@@ -80,7 +80,7 @@ function register(agentRef, target) {
|
|
|
80
80
|
const timings = findScriptTimings();
|
|
81
81
|
|
|
82
82
|
// Track MFE vitals for this entity
|
|
83
|
-
const vitals = trackMFEVitals(target
|
|
83
|
+
const vitals = trackMFEVitals(target, timings);
|
|
84
84
|
const attrs = {};
|
|
85
85
|
|
|
86
86
|
// Only define attributes getter if it doesn't already exist
|
|
@@ -226,13 +226,21 @@ function register(agentRef, target) {
|
|
|
226
226
|
timeToRegister: timings.registeredAt,
|
|
227
227
|
// timestamp when register() was called
|
|
228
228
|
// leave room to extend these with more data keys as needed
|
|
229
|
-
|
|
229
|
+
...(vitals.fcp.value >= 0 && {
|
|
230
|
+
'nr.vitals.fcp.value': vitals.fcp.value
|
|
231
|
+
}),
|
|
230
232
|
// FCP vital object with value and metadata
|
|
231
|
-
|
|
233
|
+
...(vitals.lcp.value >= 0 && {
|
|
234
|
+
'nr.vitals.lcp.value': vitals.lcp.value
|
|
235
|
+
}),
|
|
232
236
|
// LCP vital object with value and metadata
|
|
233
|
-
|
|
237
|
+
...(vitals.cls.value >= 0 && {
|
|
238
|
+
'nr.vitals.cls.value': vitals.cls.value
|
|
239
|
+
}),
|
|
234
240
|
// CLS vital object with value and metadata
|
|
235
|
-
|
|
241
|
+
...(vitals.inp.value >= 0 && {
|
|
242
|
+
'nr.vitals.inp.value': vitals.inp.value
|
|
243
|
+
}) // INP vital object with value and metadata
|
|
236
244
|
};
|
|
237
245
|
api.recordCustomEvent('MicroFrontEndTiming', eventData);
|
|
238
246
|
}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tracks all Core Web Vitals for a specific MFE.
|
|
3
|
-
* @param {
|
|
4
|
-
* @
|
|
3
|
+
* @param {RegisterAPITarget} target - The MFE target to track vitals for
|
|
4
|
+
* @param {RegisterAPITimings} timings - The timings object to use for relative time calculations
|
|
5
|
+
* @returns {RegisterAPIVitals} An object containing the vitals values and a disconnect method to stop tracking
|
|
5
6
|
*/
|
|
6
|
-
export function trackMFEVitals(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
inp: object | null;
|
|
11
|
-
disconnect: Function;
|
|
12
|
-
};
|
|
13
|
-
export type RegisterAPITimings = any;
|
|
7
|
+
export function trackMFEVitals(target: RegisterAPITarget, timings: RegisterAPITimings): RegisterAPIVitals;
|
|
8
|
+
export type RegisterAPITimings = import("../../loaders/api/register-api-types").RegisterAPITimings;
|
|
9
|
+
export type RegisterAPITarget = import("../../loaders/api/register-api-types").RegisterAPITarget;
|
|
10
|
+
export type RegisterAPIVitals = import("../../loaders/api/register-api-types").RegisterAPIVitals;
|
|
14
11
|
//# sourceMappingURL=mfe-vitals.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mfe-vitals.d.ts","sourceRoot":"","sources":["../../../../src/common/v2/mfe-vitals.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mfe-vitals.d.ts","sourceRoot":"","sources":["../../../../src/common/v2/mfe-vitals.js"],"names":[],"mappings":"AAoHA;;;;;GAKG;AACH,uCAJW,iBAAiB,WACjB,kBAAkB,GAChB,iBAAiB,CAqM7B;iCApTY,OAAO,sCAAsC,EAAE,kBAAkB;gCACjE,OAAO,sCAAsC,EAAE,iBAAiB;gCAChE,OAAO,sCAAsC,EAAE,iBAAiB"}
|
|
@@ -146,4 +146,22 @@ export type RegisterAPITimings = {
|
|
|
146
146
|
*/
|
|
147
147
|
type: string;
|
|
148
148
|
};
|
|
149
|
+
export type RegisterAPIVitals = {
|
|
150
|
+
/**
|
|
151
|
+
* - The first contentful paint timing for the registered entity.
|
|
152
|
+
*/
|
|
153
|
+
fcp?: number | undefined;
|
|
154
|
+
/**
|
|
155
|
+
* - The largest contentful paint timing for the registered entity.
|
|
156
|
+
*/
|
|
157
|
+
lcp?: number | undefined;
|
|
158
|
+
/**
|
|
159
|
+
* - The cumulative layout shift score for the registered entity.
|
|
160
|
+
*/
|
|
161
|
+
cls?: number | undefined;
|
|
162
|
+
/**
|
|
163
|
+
* - The interaction to next paint timing for the registered entity.
|
|
164
|
+
*/
|
|
165
|
+
inp?: number | undefined;
|
|
166
|
+
};
|
|
149
167
|
//# sourceMappingURL=register-api-types.d.ts.map
|
package/package.json
CHANGED
|
@@ -7,7 +7,9 @@ import { globalScope, isBrowserScope } from '../constants/runtime'
|
|
|
7
7
|
import { now } from '../timing/now'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* @typedef {import('
|
|
10
|
+
* @typedef {import('../../loaders/api/register-api-types').RegisterAPITimings} RegisterAPITimings
|
|
11
|
+
* @typedef {import('../../loaders/api/register-api-types').RegisterAPITarget} RegisterAPITarget
|
|
12
|
+
* @typedef {import('../../loaders/api/register-api-types').RegisterAPIVitals} RegisterAPIVitals
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
const isObservable = (node) => {
|
|
@@ -18,18 +20,34 @@ const isObservable = (node) => {
|
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
const isMatch = (dataset, target) => dataset?.nrMfeId === target.id && (!dataset?.nrMfeObserved || dataset?.nrMfeObserved === target.instance)
|
|
24
|
+
|
|
25
|
+
const escapeSelectorValue = (value) => {
|
|
26
|
+
try {
|
|
27
|
+
return globalScope.CSS.escape(value)
|
|
28
|
+
} catch (e) {
|
|
29
|
+
// give up
|
|
30
|
+
return value
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
/**
|
|
22
35
|
* Check if node is within a specific MFE
|
|
23
36
|
* @param {Node} node - DOM node to check
|
|
24
37
|
* @param {string} id - MFE ID to match
|
|
25
38
|
* @returns {boolean}
|
|
26
39
|
*/
|
|
27
|
-
const isInMFE = (node,
|
|
28
|
-
|
|
40
|
+
const isInMFE = (node, target = {}) => {
|
|
41
|
+
const id = target.id
|
|
42
|
+
const instance = target.instance
|
|
43
|
+
if (!node || !id || !instance) return false
|
|
29
44
|
try {
|
|
30
45
|
let curr = node.nodeType === 1 ? node : node.parentElement
|
|
31
46
|
while (curr?.tagName) {
|
|
32
|
-
if (curr.dataset
|
|
47
|
+
if (isMatch(curr.dataset, target)) {
|
|
48
|
+
curr.dataset.nrMfeObserved = instance // mark that this MFE has been observed for vitals
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
33
51
|
curr = curr.parentNode
|
|
34
52
|
}
|
|
35
53
|
} catch (e) {}
|
|
@@ -42,25 +60,28 @@ const isInMFE = (node, id) => {
|
|
|
42
60
|
* @param {Function} onMatch - Callback when matching node is *added*
|
|
43
61
|
* @returns {MutationObserver}
|
|
44
62
|
*/
|
|
45
|
-
const observeMutations = (
|
|
63
|
+
const observeMutations = (target, onMatch) => {
|
|
46
64
|
// Try to find existing MFE root
|
|
47
|
-
const
|
|
65
|
+
const potentialMatches = globalScope.document?.querySelectorAll(`[data-nr-mfe-id="${escapeSelectorValue(target.id)}"]`)
|
|
66
|
+
const mfeRoot = (Array.from(potentialMatches) || []).find(x => isMatch(x.dataset, target))
|
|
48
67
|
let observingRoot = !!mfeRoot
|
|
68
|
+
if (observingRoot) mfeRoot.dataset.nrMfeObserved ??= target.instance // mark that this MFE has been observed for vitals
|
|
49
69
|
|
|
50
70
|
const obs = new globalScope.MutationObserver(mutations => {
|
|
51
71
|
mutations.forEach(m => {
|
|
52
72
|
m.addedNodes.forEach(node => {
|
|
53
73
|
// Check if this is the MFE root being added
|
|
54
74
|
const elem = node.nodeType === 1 ? node : null
|
|
55
|
-
if (elem?.dataset
|
|
75
|
+
if (!observingRoot && isMatch(elem?.dataset, target)) {
|
|
56
76
|
// Found the root! Lets switch to observing just this subtree for performance reasons
|
|
57
77
|
obs.disconnect()
|
|
58
78
|
obs.observe(elem, { childList: true, subtree: true })
|
|
59
79
|
observingRoot = true
|
|
80
|
+
elem.dataset.nrMfeObserved = target.instance // mark that this MFE has been observed for vitals
|
|
60
81
|
}
|
|
61
82
|
|
|
62
83
|
// Only check isInMFE if we're observing the whole document; skip expensive ancestor walk when observing root
|
|
63
|
-
if (isObservable(node) && (observingRoot || isInMFE(node,
|
|
84
|
+
if (isObservable(node) && (observingRoot || isInMFE(node, target))) {
|
|
64
85
|
onMatch(node, obs)
|
|
65
86
|
}
|
|
66
87
|
})
|
|
@@ -95,15 +116,16 @@ const observePerformance = (observers, config, onEntry) => {
|
|
|
95
116
|
|
|
96
117
|
/**
|
|
97
118
|
* Tracks all Core Web Vitals for a specific MFE.
|
|
98
|
-
* @param {
|
|
99
|
-
* @
|
|
119
|
+
* @param {RegisterAPITarget} target - The MFE target to track vitals for
|
|
120
|
+
* @param {RegisterAPITimings} timings - The timings object to use for relative time calculations
|
|
121
|
+
* @returns {RegisterAPIVitals} An object containing the vitals values and a disconnect method to stop tracking
|
|
100
122
|
*/
|
|
101
|
-
export function trackMFEVitals (
|
|
102
|
-
let fcpObservedAt
|
|
103
|
-
let lcpObservedAt
|
|
123
|
+
export function trackMFEVitals (target, timings) {
|
|
124
|
+
let fcpObservedAt
|
|
125
|
+
let lcpObservedAt
|
|
104
126
|
|
|
105
127
|
const getTimeRelativeToScriptStart = (capturedAt) => {
|
|
106
|
-
if (capturedAt
|
|
128
|
+
if (capturedAt == null) return capturedAt
|
|
107
129
|
return capturedAt - (timings?.scriptStart || timings?.registeredAt || 0)
|
|
108
130
|
}
|
|
109
131
|
|
|
@@ -115,18 +137,24 @@ export function trackMFEVitals (id, timings) {
|
|
|
115
137
|
get value () { return getTimeRelativeToScriptStart(lcpObservedAt) }
|
|
116
138
|
},
|
|
117
139
|
cls: {
|
|
118
|
-
value:
|
|
140
|
+
value: undefined
|
|
119
141
|
},
|
|
120
142
|
inp: {
|
|
121
|
-
value:
|
|
143
|
+
value: undefined
|
|
122
144
|
},
|
|
123
145
|
disconnect: () => {}
|
|
124
146
|
}
|
|
125
147
|
|
|
126
|
-
if (!
|
|
148
|
+
if (!target || !isBrowserScope || !globalScope.MutationObserver || !globalScope.PerformanceObserver) return vitals
|
|
127
149
|
|
|
128
150
|
const observers = []
|
|
129
151
|
|
|
152
|
+
// If FCP hasn't been observed within 10 seconds, give up and shut down all observers.
|
|
153
|
+
// Once FCP is observed, the other vitals are left to record until their natural lifespan ends.
|
|
154
|
+
setTimeout(() => {
|
|
155
|
+
if (!fcpObservedAt) vitals.disconnect()
|
|
156
|
+
}, 10000)
|
|
157
|
+
|
|
130
158
|
const populateVitalMinimums = () => {
|
|
131
159
|
fcpObservedAt ??= now()
|
|
132
160
|
lcpObservedAt ??= now()
|
|
@@ -134,14 +162,16 @@ export function trackMFEVitals (id, timings) {
|
|
|
134
162
|
}
|
|
135
163
|
|
|
136
164
|
// if the MFE has already rendered something on the page before we could set up listeners, just populate vital minimums immediately
|
|
137
|
-
|
|
165
|
+
const existingRoots = globalScope.document?.querySelectorAll(`[data-nr-mfe-id="${escapeSelectorValue(target.id)}"]`)
|
|
166
|
+
if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums()
|
|
138
167
|
|
|
139
168
|
// Track FCP - first contentful paint
|
|
140
|
-
observeMutations(
|
|
169
|
+
const fcpObs = observeMutations(target, (_, obs) => {
|
|
141
170
|
// An observed "FCP" means _something_ rendered, so at minimum we can populate all the baseline values for the vitals
|
|
142
171
|
populateVitalMinimums()
|
|
143
172
|
obs.disconnect()
|
|
144
173
|
})
|
|
174
|
+
observers.push(fcpObs)
|
|
145
175
|
|
|
146
176
|
// Track LCP - largest contentful paint
|
|
147
177
|
let largestSize = 0
|
|
@@ -167,7 +197,7 @@ export function trackMFEVitals (id, timings) {
|
|
|
167
197
|
// ResizeObserver not supported
|
|
168
198
|
}
|
|
169
199
|
|
|
170
|
-
const lcpObs = observeMutations(
|
|
200
|
+
const lcpObs = observeMutations(target, (node) => {
|
|
171
201
|
// an observed "LCP" means _something_ rendered, so at minimum we can make sure all the baseline values are populated for the vitals
|
|
172
202
|
populateVitalMinimums()
|
|
173
203
|
|
|
@@ -214,7 +244,7 @@ export function trackMFEVitals (id, timings) {
|
|
|
214
244
|
observePerformance(observers, { type: 'layout-shift', buffered: true }, (entry) => {
|
|
215
245
|
if (entry.hadRecentInput) return
|
|
216
246
|
;(entry.sources || []).some(source => {
|
|
217
|
-
if (isInMFE(source.node,
|
|
247
|
+
if (isInMFE(source.node, target)) {
|
|
218
248
|
// 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
|
|
219
249
|
populateVitalMinimums()
|
|
220
250
|
vitals.cls.value += entry.value
|
|
@@ -226,8 +256,8 @@ export function trackMFEVitals (id, timings) {
|
|
|
226
256
|
|
|
227
257
|
// Track INP - interaction to next paint
|
|
228
258
|
observePerformance(observers, { type: 'event', buffered: true, durationThreshold: 40 }, (entry) => {
|
|
229
|
-
if (!entry.interactionId || !isInMFE(entry.target,
|
|
230
|
-
if (vitals.inp.value ===
|
|
259
|
+
if (!entry.interactionId || !isInMFE(entry.target, target)) return
|
|
260
|
+
if (vitals.inp.value === undefined || entry.duration > vitals.inp.value) {
|
|
231
261
|
// 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
|
|
232
262
|
populateVitalMinimums()
|
|
233
263
|
vitals.inp.value = entry.duration
|
|
@@ -252,6 +282,9 @@ export function trackMFEVitals (id, timings) {
|
|
|
252
282
|
}
|
|
253
283
|
})
|
|
254
284
|
disconnectInteractionListeners()
|
|
285
|
+
;['visibilitychange', 'pagehide'].forEach(type => {
|
|
286
|
+
globalScope.removeEventListener(type, vitals.disconnect, { once: true, passive: true })
|
|
287
|
+
})
|
|
255
288
|
}
|
|
256
289
|
|
|
257
290
|
// Auto-disconnect LCP observer on user interaction (per Web Vitals spec)
|
|
@@ -266,7 +299,7 @@ export function trackMFEVitals (id, timings) {
|
|
|
266
299
|
}
|
|
267
300
|
|
|
268
301
|
const handleInteraction = (event) => {
|
|
269
|
-
if (!isInMFE(event?.target,
|
|
302
|
+
if (!isInMFE(event?.target, target)) return
|
|
270
303
|
|
|
271
304
|
disconnectLCP()
|
|
272
305
|
disconnectInteractionListeners()
|
|
@@ -53,4 +53,12 @@
|
|
|
53
53
|
* @property {string} type - The type of timing associated with the registered entity, 'script' or 'link' if found with the performance resource API, 'fetch' for dynamic imports, 'inline' if found to be associated with the root document URL, or 'unknown' if no associated resource could be found.
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {Object} RegisterAPIVitals
|
|
58
|
+
* @property {number} [fcp] - The first contentful paint timing for the registered entity.
|
|
59
|
+
* @property {number} [lcp] - The largest contentful paint timing for the registered entity.
|
|
60
|
+
* @property {number} [cls] - The cumulative layout shift score for the registered entity.
|
|
61
|
+
* @property {number} [inp] - The interaction to next paint timing for the registered entity.
|
|
62
|
+
*/
|
|
63
|
+
|
|
56
64
|
export default {}
|
|
@@ -85,7 +85,7 @@ function register (agentRef, target) {
|
|
|
85
85
|
const timings = findScriptTimings()
|
|
86
86
|
|
|
87
87
|
// Track MFE vitals for this entity
|
|
88
|
-
const vitals = trackMFEVitals(target
|
|
88
|
+
const vitals = trackMFEVitals(target, timings)
|
|
89
89
|
|
|
90
90
|
const attrs = {}
|
|
91
91
|
|
|
@@ -203,10 +203,10 @@ function register (agentRef, target) {
|
|
|
203
203
|
timeToLoad: timeToFetch + timeToExecute, // fetch time and script time together
|
|
204
204
|
timeToRegister: timings.registeredAt, // timestamp when register() was called
|
|
205
205
|
// leave room to extend these with more data keys as needed
|
|
206
|
-
'nr.vitals.fcp.value': vitals.fcp
|
|
207
|
-
'nr.vitals.lcp.value': vitals.lcp
|
|
208
|
-
'nr.vitals.cls.value': vitals.cls
|
|
209
|
-
'nr.vitals.inp.value': vitals.inp
|
|
206
|
+
...(vitals.fcp.value >= 0 && { 'nr.vitals.fcp.value': vitals.fcp.value }), // FCP vital object with value and metadata
|
|
207
|
+
...(vitals.lcp.value >= 0 && { 'nr.vitals.lcp.value': vitals.lcp.value }), // LCP vital object with value and metadata
|
|
208
|
+
...(vitals.cls.value >= 0 && { 'nr.vitals.cls.value': vitals.cls.value }), // CLS vital object with value and metadata
|
|
209
|
+
...(vitals.inp.value >= 0 && { 'nr.vitals.inp.value': vitals.inp.value }) // INP vital object with value and metadata
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
api.recordCustomEvent('MicroFrontEndTiming', eventData)
|