@newrelic/browser-agent 1.309.0-rc.7 → 1.309.0-rc.8
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/util/script-tracker.js +116 -13
- package/dist/cjs/loaders/api/register-api-types.js +19 -13
- package/dist/cjs/loaders/api/register.js +5 -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/util/script-tracker.js +116 -13
- package/dist/esm/loaders/api/register-api-types.js +21 -13
- package/dist/esm/loaders/api/register.js +5 -5
- package/dist/types/common/util/script-tracker.d.ts +3 -6
- package/dist/types/common/util/script-tracker.d.ts.map +1 -1
- package/dist/types/loaders/api/register-api-types.d.ts +52 -17
- package/dist/types/loaders/api/register-api-types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/util/script-tracker.js +107 -13
- package/src/loaders/api/register-api-types.js +21 -13
- package/src/loaders/api/register.js +3 -5
|
@@ -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.309.0-rc.
|
|
20
|
+
const VERSION = exports.VERSION = "1.309.0-rc.8";
|
|
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.309.0-rc.
|
|
20
|
+
const VERSION = exports.VERSION = "1.309.0-rc.8";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Exposes the build type of the agent
|
|
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.findScriptTimings = findScriptTimings;
|
|
7
7
|
var _runtime = require("../constants/runtime");
|
|
8
|
+
var _now = require("../timing/now");
|
|
8
9
|
var _cleanUrl = require("../url/clean-url");
|
|
9
10
|
var _browserStackMatchers = require("./browser-stack-matchers");
|
|
10
11
|
/**
|
|
@@ -12,6 +13,44 @@ var _browserStackMatchers = require("./browser-stack-matchers");
|
|
|
12
13
|
* SPDX-License-Identifier: Apache-2.0
|
|
13
14
|
*/
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** @type {(entry: PerformanceEntry) => boolean} - A shared function to determine if a performance entry is a valid script or link resource for evaluation */
|
|
21
|
+
const validEntryCriteria = entry => entry.initiatorType === 'script' || entry.initiatorType === 'link' && entry.name.endsWith('.js');
|
|
22
|
+
|
|
23
|
+
/** @type {Set<PerformanceResourceTiming>} - A set of resource timing objects that are "valid" -- see "validEntryCriteria" */
|
|
24
|
+
const scripts = new Set();
|
|
25
|
+
/** @type {Array<{ test: (entry: PerformanceEntry) => boolean, addedAt: number }>} an array of PerformanceObserver subscribers to check for late emissions */
|
|
26
|
+
let poSubscribers = [];
|
|
27
|
+
if (_runtime.globalScope.PerformanceObserver?.supportedEntryTypes.includes('resource')) {
|
|
28
|
+
/** We must track the script assets this way, because the performance buffer can fill up and when it does that
|
|
29
|
+
* it stops accepting new entries (instead of dropping old entries), which means if the register API is called
|
|
30
|
+
* after the buffer fills up we won't be able to get the script timing information from the resource timing API
|
|
31
|
+
*/
|
|
32
|
+
const scriptObserver = new PerformanceObserver(list => {
|
|
33
|
+
list.getEntries().forEach(entry => {
|
|
34
|
+
if (validEntryCriteria(entry)) {
|
|
35
|
+
if (scripts.size > 250) scripts.delete(scripts.values().next().value); // keep the set from growing indefinitely, we only need to check recent entries against the stack of the register API caller, so we can drop old entries as new ones come in
|
|
36
|
+
scripts.add(entry);
|
|
37
|
+
const canClear = [];
|
|
38
|
+
poSubscribers.forEach(({
|
|
39
|
+
test,
|
|
40
|
+
addedAt
|
|
41
|
+
}, idx) => {
|
|
42
|
+
if (test(entry) || (0, _now.now)() - addedAt > 10000) canClear.push(idx); // Clear subscribers that have resolved or have been pending for more than 10 seconds
|
|
43
|
+
});
|
|
44
|
+
poSubscribers = poSubscribers.filter((_, idx) => !canClear.includes(idx));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
scriptObserver.observe({
|
|
49
|
+
type: 'resource',
|
|
50
|
+
buffered: true
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
15
54
|
/**
|
|
16
55
|
* Extracts URLs from stack traces using the same logic as compute-stack-trace.js
|
|
17
56
|
* @param {string} stack The error stack trace
|
|
@@ -48,31 +87,95 @@ function getDeepStackTrace() {
|
|
|
48
87
|
return stack;
|
|
49
88
|
}
|
|
50
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Indicates whether the provided URL matches any script preload link tags in the document.
|
|
92
|
+
* @param {string} targetUrl The URL to match against preload tags
|
|
93
|
+
* @returns {boolean} True if a matching preload link is found, false otherwise
|
|
94
|
+
*/
|
|
95
|
+
function wasPreloaded(targetUrl) {
|
|
96
|
+
if (!targetUrl || !_runtime.globalScope.document) return false;
|
|
97
|
+
try {
|
|
98
|
+
const linkTags = _runtime.globalScope.document.querySelectorAll('link[rel="preload"][as="script"]');
|
|
99
|
+
for (const link of linkTags) {
|
|
100
|
+
// link.href is resolved to an absolute URL by the browser (even if supplied as relative), so we can match exactly against the cleaned target URL
|
|
101
|
+
if ((0, _cleanUrl.cleanURL)(link.href) === targetUrl) return true;
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// Don't let DOM parsing errors break anything
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
51
109
|
/**
|
|
52
110
|
* Uses the stack of the initiator function, returns script timing information if a script can be found with the resource timing API matching the URL found in the stack.
|
|
53
|
-
* @returns {
|
|
111
|
+
* @returns {RegisterAPITimings} Object containing script fetch start and end times, and the asset URL if found
|
|
54
112
|
*/
|
|
55
113
|
function findScriptTimings() {
|
|
56
|
-
const stack = getDeepStackTrace();
|
|
57
114
|
const timings = {
|
|
115
|
+
registeredAt: (0, _now.now)(),
|
|
116
|
+
reportedAt: undefined,
|
|
58
117
|
fetchStart: 0,
|
|
59
118
|
fetchEnd: 0,
|
|
60
|
-
asset: undefined
|
|
119
|
+
asset: undefined,
|
|
120
|
+
type: 'unknown'
|
|
61
121
|
};
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
122
|
+
const stack = getDeepStackTrace();
|
|
123
|
+
if (!stack) return timings;
|
|
124
|
+
const navUrl = _runtime.globalScope.performance?.getEntriesByType('navigation')?.find(entry => entry.initiatorType === 'navigation')?.name || '';
|
|
64
125
|
try {
|
|
65
126
|
const mfeScriptUrl = extractUrlsFromStack(stack).at(-1); // array of URLs from the stack of the register API caller, the MFE script should be at the bottom
|
|
66
127
|
if (!mfeScriptUrl) return timings;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
128
|
+
if (navUrl.includes(mfeScriptUrl)) {
|
|
129
|
+
// this means the stack is indicating that the registration came from an inline script or eval, so we won't find a matching script resource - return early with just the URL
|
|
130
|
+
timings.asset = (0, _cleanUrl.cleanURL)(navUrl);
|
|
131
|
+
timings.type = 'inline';
|
|
132
|
+
return timings;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// try to find it in the static list, which updates faster than the PO. This cant be solely trusted, since the buffer can fill up and stop accepting entries,
|
|
136
|
+
// but it can still help in cases where the check is made before the asset is emitted by the performance observer and the buffer is not full. Fallback to checking the PO
|
|
137
|
+
// entries that have been buffered as seen in the PO callback if its not found in the static list.
|
|
138
|
+
const match = performance.getEntriesByType('resource').find(entryMatchesMfe) || [...scripts].find(entryMatchesMfe);
|
|
72
139
|
if (match) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
140
|
+
setMatchedAttributes(match);
|
|
141
|
+
} else {
|
|
142
|
+
// We didnt find a match with the PO, nor a static lookup... check if its preloaded and if so, set basic fallbacks and try to associate with a later script entry if possible, this can happen if the preload is reported late in the PO observer callback
|
|
143
|
+
if (wasPreloaded(mfeScriptUrl)) {
|
|
144
|
+
timings.asset = mfeScriptUrl;
|
|
145
|
+
timings.type = 'preload';
|
|
146
|
+
// wait for a late PO callback... The timings object can be mutated after the fact since we return a pointer and not a cloned object
|
|
147
|
+
poSubscribers.push({
|
|
148
|
+
addedAt: (0, _now.now)(),
|
|
149
|
+
test: entry => {
|
|
150
|
+
if (entryMatchesMfe(entry)) {
|
|
151
|
+
setMatchedAttributes(entry);
|
|
152
|
+
return true; // return true so that we know to clear this callback from the pending list since we found our match, otherwise it will stay in the list and be called for future entries which is unnecessary after we found our match and can cause performance issues if there are a lot of future entries and pending callbacks
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* A matcher function to determine if a performance entry corresponds to the MFE script based on URL matching. It checks if either the entry URL ends with the MFE script URL or vice versa, to account for potential differences in how URLs are represented in the stack trace versus the resource timing entries.
|
|
162
|
+
* @param {PerformanceResourceTiming} entry - The resource timing entry to compare to the MFE script
|
|
163
|
+
* @returns {boolean} True if we think the entry matches the MFE script, false otherwise
|
|
164
|
+
*/
|
|
165
|
+
function entryMatchesMfe(entry) {
|
|
166
|
+
const entryUrl = (0, _cleanUrl.cleanURL)(entry.name);
|
|
167
|
+
return entryUrl.endsWith(mfeScriptUrl) || mfeScriptUrl.endsWith(entryUrl);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A helper function to set the matched timing attributes on the timings object based on a performance entry. This is called when we have identified a resource timing entry that we believe corresponds to the MFE script, and we want to extract the fetch start and end times, as well as the asset URL and type.
|
|
172
|
+
* @param {PerformanceResourceTiming} entry - The resource timing entry to base values off of
|
|
173
|
+
*/
|
|
174
|
+
function setMatchedAttributes(entry) {
|
|
175
|
+
timings.fetchStart = Math.floor(entry.startTime);
|
|
176
|
+
timings.fetchEnd = Math.floor(entry.responseEnd);
|
|
177
|
+
timings.asset = entry.name;
|
|
178
|
+
timings.type = entry.initiatorType;
|
|
76
179
|
}
|
|
77
180
|
} catch (error) {
|
|
78
181
|
// Don't let stack parsing errors break anything
|
|
@@ -33,18 +33,24 @@ exports.default = void 0;
|
|
|
33
33
|
/**
|
|
34
34
|
* @typedef {Object} RegisterAPIMetadata
|
|
35
35
|
* @property {Object} customAttributes - The custom attributes for the registered entity.
|
|
36
|
-
* @property {
|
|
37
|
-
* @property {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
* @
|
|
41
|
-
* @property {
|
|
42
|
-
* @property {
|
|
43
|
-
* @property {string} [
|
|
44
|
-
* @property {string}
|
|
45
|
-
* @property {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
* @
|
|
36
|
+
* @property {RegisterAPITimings} timings - The timing metrics for the registered entity.
|
|
37
|
+
* @property {RegisterAPITarget} target - The options for the registered entity.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {Object} RegisterAPITarget
|
|
41
|
+
* @property {string} id - The ID for the registered entity.
|
|
42
|
+
* @property {string} name - The name returned for the registered entity.
|
|
43
|
+
* @property {{[key: string]: any}} [tags] - The tags for the registered entity as key-value pairs.
|
|
44
|
+
* @property {string} [parentId] - The parentId for the registered entity. If none was supplied, it will assume the entity guid from the main agent.
|
|
45
|
+
* @property {boolean} [isolated] - When true, each registration creates an isolated instance. When false, multiple registrations with the same id and isolated: false will share a single instance, including all custom attributes, ids, names, and metadata. Calling deregister on a shared instance will deregister it for all entities using the instance. Defaults to true.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {Object} RegisterAPITimings
|
|
49
|
+
* @property {number} registeredAt - The timestamp when the registered entity was created.
|
|
50
|
+
* @property {number} [reportedAt] - The timestamp when the registered entity was deregistered.
|
|
51
|
+
* @property {number} fetchStart - The timestamp when the registered entity began fetching.
|
|
52
|
+
* @property {number} fetchEnd - The timestamp when the registered entity finished fetching.
|
|
53
|
+
* @property {Object} [asset] - The asset path (if found) for the registered entity.
|
|
54
|
+
* @property {string} type - The type of timing associated with the registered entity, 'script' or 'link' if found with the performance resource API, 'inline' if found to be associated with the root document URL, or 'unknown' if no associated resource could be found.
|
|
49
55
|
*/
|
|
50
56
|
var _default = exports.default = {};
|
|
@@ -58,11 +58,7 @@ function register(agentRef, target, parent) {
|
|
|
58
58
|
target.blocked = false;
|
|
59
59
|
target.parent = parent || {};
|
|
60
60
|
if (typeof target.tags !== 'object' || target.tags === null || Array.isArray(target.tags)) target.tags = {};
|
|
61
|
-
const timings =
|
|
62
|
-
registeredAt: (0, _now.now)(),
|
|
63
|
-
reportedAt: undefined,
|
|
64
|
-
...(0, _scriptTracker.findScriptTimings)()
|
|
65
|
-
};
|
|
61
|
+
const timings = (0, _scriptTracker.findScriptTimings)();
|
|
66
62
|
const attrs = {};
|
|
67
63
|
|
|
68
64
|
// Process tags object and add to attrs, excluding protected keys
|
|
@@ -175,6 +171,10 @@ function register(agentRef, target, parent) {
|
|
|
175
171
|
if (timings.reportedAt) return;
|
|
176
172
|
timings.reportedAt = (0, _now.now)();
|
|
177
173
|
api.recordCustomEvent('MicroFrontEndTiming', {
|
|
174
|
+
assetUrl: timings.asset,
|
|
175
|
+
// the url of the script that was registered, or undefined if it could not be determined (inline or no match)
|
|
176
|
+
assetType: timings.type,
|
|
177
|
+
// the type of asset that was associated with the timings, one of 'script', 'link' (if preloaded and found in the resource timing buffer), 'preload' (if preloaded but not found in the resource timing buffer), or "unknown" if it could not be determined
|
|
178
178
|
timeToLoad: timings.registeredAt - timings.fetchStart,
|
|
179
179
|
// fetchStart to registeredAt
|
|
180
180
|
timeToBeRequested: timings.fetchStart,
|
|
@@ -4,9 +4,48 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { globalScope } from '../constants/runtime';
|
|
7
|
+
import { now } from '../timing/now';
|
|
7
8
|
import { cleanURL } from '../url/clean-url';
|
|
8
9
|
import { chrome, gecko } from './browser-stack-matchers';
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** @type {(entry: PerformanceEntry) => boolean} - A shared function to determine if a performance entry is a valid script or link resource for evaluation */
|
|
16
|
+
const validEntryCriteria = entry => entry.initiatorType === 'script' || entry.initiatorType === 'link' && entry.name.endsWith('.js');
|
|
17
|
+
|
|
18
|
+
/** @type {Set<PerformanceResourceTiming>} - A set of resource timing objects that are "valid" -- see "validEntryCriteria" */
|
|
19
|
+
const scripts = new Set();
|
|
20
|
+
/** @type {Array<{ test: (entry: PerformanceEntry) => boolean, addedAt: number }>} an array of PerformanceObserver subscribers to check for late emissions */
|
|
21
|
+
let poSubscribers = [];
|
|
22
|
+
if (globalScope.PerformanceObserver?.supportedEntryTypes.includes('resource')) {
|
|
23
|
+
/** We must track the script assets this way, because the performance buffer can fill up and when it does that
|
|
24
|
+
* it stops accepting new entries (instead of dropping old entries), which means if the register API is called
|
|
25
|
+
* after the buffer fills up we won't be able to get the script timing information from the resource timing API
|
|
26
|
+
*/
|
|
27
|
+
const scriptObserver = new PerformanceObserver(list => {
|
|
28
|
+
list.getEntries().forEach(entry => {
|
|
29
|
+
if (validEntryCriteria(entry)) {
|
|
30
|
+
if (scripts.size > 250) scripts.delete(scripts.values().next().value); // keep the set from growing indefinitely, we only need to check recent entries against the stack of the register API caller, so we can drop old entries as new ones come in
|
|
31
|
+
scripts.add(entry);
|
|
32
|
+
const canClear = [];
|
|
33
|
+
poSubscribers.forEach(({
|
|
34
|
+
test,
|
|
35
|
+
addedAt
|
|
36
|
+
}, idx) => {
|
|
37
|
+
if (test(entry) || now() - addedAt > 10000) canClear.push(idx); // Clear subscribers that have resolved or have been pending for more than 10 seconds
|
|
38
|
+
});
|
|
39
|
+
poSubscribers = poSubscribers.filter((_, idx) => !canClear.includes(idx));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
scriptObserver.observe({
|
|
44
|
+
type: 'resource',
|
|
45
|
+
buffered: true
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
10
49
|
/**
|
|
11
50
|
* Extracts URLs from stack traces using the same logic as compute-stack-trace.js
|
|
12
51
|
* @param {string} stack The error stack trace
|
|
@@ -43,31 +82,95 @@ function getDeepStackTrace() {
|
|
|
43
82
|
return stack;
|
|
44
83
|
}
|
|
45
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Indicates whether the provided URL matches any script preload link tags in the document.
|
|
87
|
+
* @param {string} targetUrl The URL to match against preload tags
|
|
88
|
+
* @returns {boolean} True if a matching preload link is found, false otherwise
|
|
89
|
+
*/
|
|
90
|
+
function wasPreloaded(targetUrl) {
|
|
91
|
+
if (!targetUrl || !globalScope.document) return false;
|
|
92
|
+
try {
|
|
93
|
+
const linkTags = globalScope.document.querySelectorAll('link[rel="preload"][as="script"]');
|
|
94
|
+
for (const link of linkTags) {
|
|
95
|
+
// link.href is resolved to an absolute URL by the browser (even if supplied as relative), so we can match exactly against the cleaned target URL
|
|
96
|
+
if (cleanURL(link.href) === targetUrl) return true;
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// Don't let DOM parsing errors break anything
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
46
104
|
/**
|
|
47
105
|
* Uses the stack of the initiator function, returns script timing information if a script can be found with the resource timing API matching the URL found in the stack.
|
|
48
|
-
* @returns {
|
|
106
|
+
* @returns {RegisterAPITimings} Object containing script fetch start and end times, and the asset URL if found
|
|
49
107
|
*/
|
|
50
108
|
export function findScriptTimings() {
|
|
51
|
-
const stack = getDeepStackTrace();
|
|
52
109
|
const timings = {
|
|
110
|
+
registeredAt: now(),
|
|
111
|
+
reportedAt: undefined,
|
|
53
112
|
fetchStart: 0,
|
|
54
113
|
fetchEnd: 0,
|
|
55
|
-
asset: undefined
|
|
114
|
+
asset: undefined,
|
|
115
|
+
type: 'unknown'
|
|
56
116
|
};
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
117
|
+
const stack = getDeepStackTrace();
|
|
118
|
+
if (!stack) return timings;
|
|
119
|
+
const navUrl = globalScope.performance?.getEntriesByType('navigation')?.find(entry => entry.initiatorType === 'navigation')?.name || '';
|
|
59
120
|
try {
|
|
60
121
|
const mfeScriptUrl = extractUrlsFromStack(stack).at(-1); // array of URLs from the stack of the register API caller, the MFE script should be at the bottom
|
|
61
122
|
if (!mfeScriptUrl) return timings;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
123
|
+
if (navUrl.includes(mfeScriptUrl)) {
|
|
124
|
+
// this means the stack is indicating that the registration came from an inline script or eval, so we won't find a matching script resource - return early with just the URL
|
|
125
|
+
timings.asset = cleanURL(navUrl);
|
|
126
|
+
timings.type = 'inline';
|
|
127
|
+
return timings;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// try to find it in the static list, which updates faster than the PO. This cant be solely trusted, since the buffer can fill up and stop accepting entries,
|
|
131
|
+
// but it can still help in cases where the check is made before the asset is emitted by the performance observer and the buffer is not full. Fallback to checking the PO
|
|
132
|
+
// entries that have been buffered as seen in the PO callback if its not found in the static list.
|
|
133
|
+
const match = performance.getEntriesByType('resource').find(entryMatchesMfe) || [...scripts].find(entryMatchesMfe);
|
|
67
134
|
if (match) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
135
|
+
setMatchedAttributes(match);
|
|
136
|
+
} else {
|
|
137
|
+
// We didnt find a match with the PO, nor a static lookup... check if its preloaded and if so, set basic fallbacks and try to associate with a later script entry if possible, this can happen if the preload is reported late in the PO observer callback
|
|
138
|
+
if (wasPreloaded(mfeScriptUrl)) {
|
|
139
|
+
timings.asset = mfeScriptUrl;
|
|
140
|
+
timings.type = 'preload';
|
|
141
|
+
// wait for a late PO callback... The timings object can be mutated after the fact since we return a pointer and not a cloned object
|
|
142
|
+
poSubscribers.push({
|
|
143
|
+
addedAt: now(),
|
|
144
|
+
test: entry => {
|
|
145
|
+
if (entryMatchesMfe(entry)) {
|
|
146
|
+
setMatchedAttributes(entry);
|
|
147
|
+
return true; // return true so that we know to clear this callback from the pending list since we found our match, otherwise it will stay in the list and be called for future entries which is unnecessary after we found our match and can cause performance issues if there are a lot of future entries and pending callbacks
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A matcher function to determine if a performance entry corresponds to the MFE script based on URL matching. It checks if either the entry URL ends with the MFE script URL or vice versa, to account for potential differences in how URLs are represented in the stack trace versus the resource timing entries.
|
|
157
|
+
* @param {PerformanceResourceTiming} entry - The resource timing entry to compare to the MFE script
|
|
158
|
+
* @returns {boolean} True if we think the entry matches the MFE script, false otherwise
|
|
159
|
+
*/
|
|
160
|
+
function entryMatchesMfe(entry) {
|
|
161
|
+
const entryUrl = cleanURL(entry.name);
|
|
162
|
+
return entryUrl.endsWith(mfeScriptUrl) || mfeScriptUrl.endsWith(entryUrl);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A helper function to set the matched timing attributes on the timings object based on a performance entry. This is called when we have identified a resource timing entry that we believe corresponds to the MFE script, and we want to extract the fetch start and end times, as well as the asset URL and type.
|
|
167
|
+
* @param {PerformanceResourceTiming} entry - The resource timing entry to base values off of
|
|
168
|
+
*/
|
|
169
|
+
function setMatchedAttributes(entry) {
|
|
170
|
+
timings.fetchStart = Math.floor(entry.startTime);
|
|
171
|
+
timings.fetchEnd = Math.floor(entry.responseEnd);
|
|
172
|
+
timings.asset = entry.name;
|
|
173
|
+
timings.type = entry.initiatorType;
|
|
71
174
|
}
|
|
72
175
|
} catch (error) {
|
|
73
176
|
// Don't let stack parsing errors break anything
|
|
@@ -30,19 +30,27 @@
|
|
|
30
30
|
/**
|
|
31
31
|
* @typedef {Object} RegisterAPIMetadata
|
|
32
32
|
* @property {Object} customAttributes - The custom attributes for the registered entity.
|
|
33
|
-
* @property {
|
|
34
|
-
* @property {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* @
|
|
39
|
-
* @property {
|
|
40
|
-
* @property {string}
|
|
41
|
-
* @property {string}
|
|
42
|
-
* @property {string}
|
|
43
|
-
* @property {
|
|
44
|
-
|
|
45
|
-
|
|
33
|
+
* @property {RegisterAPITimings} timings - The timing metrics for the registered entity.
|
|
34
|
+
* @property {RegisterAPITarget} target - The options for the registered entity.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} RegisterAPITarget
|
|
39
|
+
* @property {string} id - The ID for the registered entity.
|
|
40
|
+
* @property {string} name - The name returned for the registered entity.
|
|
41
|
+
* @property {{[key: string]: any}} [tags] - The tags for the registered entity as key-value pairs.
|
|
42
|
+
* @property {string} [parentId] - The parentId for the registered entity. If none was supplied, it will assume the entity guid from the main agent.
|
|
43
|
+
* @property {boolean} [isolated] - When true, each registration creates an isolated instance. When false, multiple registrations with the same id and isolated: false will share a single instance, including all custom attributes, ids, names, and metadata. Calling deregister on a shared instance will deregister it for all entities using the instance. Defaults to true.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} RegisterAPITimings
|
|
48
|
+
* @property {number} registeredAt - The timestamp when the registered entity was created.
|
|
49
|
+
* @property {number} [reportedAt] - The timestamp when the registered entity was deregistered.
|
|
50
|
+
* @property {number} fetchStart - The timestamp when the registered entity began fetching.
|
|
51
|
+
* @property {number} fetchEnd - The timestamp when the registered entity finished fetching.
|
|
52
|
+
* @property {Object} [asset] - The asset path (if found) for the registered entity.
|
|
53
|
+
* @property {string} type - The type of timing associated with the registered entity, 'script' or 'link' if found with the performance resource API, 'inline' if found to be associated with the root document URL, or 'unknown' if no associated resource could be found.
|
|
46
54
|
*/
|
|
47
55
|
|
|
48
56
|
export default {};
|
|
@@ -52,11 +52,7 @@ function register(agentRef, target, parent) {
|
|
|
52
52
|
target.blocked = false;
|
|
53
53
|
target.parent = parent || {};
|
|
54
54
|
if (typeof target.tags !== 'object' || target.tags === null || Array.isArray(target.tags)) target.tags = {};
|
|
55
|
-
const timings =
|
|
56
|
-
registeredAt: now(),
|
|
57
|
-
reportedAt: undefined,
|
|
58
|
-
...findScriptTimings()
|
|
59
|
-
};
|
|
55
|
+
const timings = findScriptTimings();
|
|
60
56
|
const attrs = {};
|
|
61
57
|
|
|
62
58
|
// Process tags object and add to attrs, excluding protected keys
|
|
@@ -169,6 +165,10 @@ function register(agentRef, target, parent) {
|
|
|
169
165
|
if (timings.reportedAt) return;
|
|
170
166
|
timings.reportedAt = now();
|
|
171
167
|
api.recordCustomEvent('MicroFrontEndTiming', {
|
|
168
|
+
assetUrl: timings.asset,
|
|
169
|
+
// the url of the script that was registered, or undefined if it could not be determined (inline or no match)
|
|
170
|
+
assetType: timings.type,
|
|
171
|
+
// the type of asset that was associated with the timings, one of 'script', 'link' (if preloaded and found in the resource timing buffer), 'preload' (if preloaded but not found in the resource timing buffer), or "unknown" if it could not be determined
|
|
172
172
|
timeToLoad: timings.registeredAt - timings.fetchStart,
|
|
173
173
|
// fetchStart to registeredAt
|
|
174
174
|
timeToBeRequested: timings.fetchStart,
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Uses the stack of the initiator function, returns script timing information if a script can be found with the resource timing API matching the URL found in the stack.
|
|
3
|
-
* @returns {
|
|
3
|
+
* @returns {RegisterAPITimings} Object containing script fetch start and end times, and the asset URL if found
|
|
4
4
|
*/
|
|
5
|
-
export function findScriptTimings():
|
|
6
|
-
|
|
7
|
-
fetchEnd: number;
|
|
8
|
-
asset: string | undefined;
|
|
9
|
-
};
|
|
5
|
+
export function findScriptTimings(): RegisterAPITimings;
|
|
6
|
+
export type RegisterAPITimings = any;
|
|
10
7
|
//# sourceMappingURL=script-tracker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"script-tracker.d.ts","sourceRoot":"","sources":["../../../../src/common/util/script-tracker.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"script-tracker.d.ts","sourceRoot":"","sources":["../../../../src/common/util/script-tracker.js"],"names":[],"mappings":"AAqGA;;;GAGG;AACH,qCAFa,kBAAkB,CAmE9B"}
|
|
@@ -90,25 +90,60 @@ export type RegisterAPIMetadata = {
|
|
|
90
90
|
/**
|
|
91
91
|
* - The timing metrics for the registered entity.
|
|
92
92
|
*/
|
|
93
|
-
timings:
|
|
94
|
-
registeredAt: number;
|
|
95
|
-
reportedAt?: number | undefined;
|
|
96
|
-
fetchStart: number;
|
|
97
|
-
fetchEnd: number;
|
|
98
|
-
asset?: Object | undefined;
|
|
99
|
-
};
|
|
93
|
+
timings: RegisterAPITimings;
|
|
100
94
|
/**
|
|
101
95
|
* - The options for the registered entity.
|
|
102
96
|
*/
|
|
103
|
-
target:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
97
|
+
target: RegisterAPITarget;
|
|
98
|
+
};
|
|
99
|
+
export type RegisterAPITarget = {
|
|
100
|
+
/**
|
|
101
|
+
* - The ID for the registered entity.
|
|
102
|
+
*/
|
|
103
|
+
id: string;
|
|
104
|
+
/**
|
|
105
|
+
* - The name returned for the registered entity.
|
|
106
|
+
*/
|
|
107
|
+
name: string;
|
|
108
|
+
/**
|
|
109
|
+
* - The tags for the registered entity as key-value pairs.
|
|
110
|
+
*/
|
|
111
|
+
tags?: {
|
|
112
|
+
[key: string]: any;
|
|
113
|
+
} | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* - The parentId for the registered entity. If none was supplied, it will assume the entity guid from the main agent.
|
|
116
|
+
*/
|
|
117
|
+
parentId?: string | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* - When true, each registration creates an isolated instance. When false, multiple registrations with the same id and isolated: false will share a single instance, including all custom attributes, ids, names, and metadata. Calling deregister on a shared instance will deregister it for all entities using the instance. Defaults to true.
|
|
120
|
+
*/
|
|
121
|
+
isolated?: boolean | undefined;
|
|
122
|
+
};
|
|
123
|
+
export type RegisterAPITimings = {
|
|
124
|
+
/**
|
|
125
|
+
* - The timestamp when the registered entity was created.
|
|
126
|
+
*/
|
|
127
|
+
registeredAt: number;
|
|
128
|
+
/**
|
|
129
|
+
* - The timestamp when the registered entity was deregistered.
|
|
130
|
+
*/
|
|
131
|
+
reportedAt?: number | undefined;
|
|
132
|
+
/**
|
|
133
|
+
* - The timestamp when the registered entity began fetching.
|
|
134
|
+
*/
|
|
135
|
+
fetchStart: number;
|
|
136
|
+
/**
|
|
137
|
+
* - The timestamp when the registered entity finished fetching.
|
|
138
|
+
*/
|
|
139
|
+
fetchEnd: number;
|
|
140
|
+
/**
|
|
141
|
+
* - The asset path (if found) for the registered entity.
|
|
142
|
+
*/
|
|
143
|
+
asset?: Object | undefined;
|
|
144
|
+
/**
|
|
145
|
+
* - The type of timing associated with the registered entity, 'script' or 'link' if found with the performance resource API, 'inline' if found to be associated with the root document URL, or 'unknown' if no associated resource could be found.
|
|
146
|
+
*/
|
|
147
|
+
type: string;
|
|
113
148
|
};
|
|
114
149
|
//# sourceMappingURL=register-api-types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register-api-types.d.ts","sourceRoot":"","sources":["../../../../src/loaders/api/register-api-types.js"],"names":[],"mappings":";;;;;;mBAOc,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,KAAK,IAAI;;;;SAC3C,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAA;KAAC,KAAK,IAAI;;;;iBACxH,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,KAAK,IAAI;;;;cAC1D,CAAC,MAAM,EAAE,sBAAsB,KAAK,WAAW;;;;gBAC/C,MAAM,IAAI;;;;uBACV,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,KAAK,IAAI;;;;aAChD,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,MAAM,GAAC,eAAe,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,GAAC,eAAe,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAC,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAC,CAAC;;;;2BACtM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI;;;;wBAC9B,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,IAAI;;;;eAClF,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,YAAY,CAAC,EAAE,OAAO,KAAK,IAAI;;;;cACtD,mBAAmB;;;;;;QAKnB,MAAM,GAAC,MAAM;;;;UACb,MAAM;;;;;;;;;;;;;;;;;;;;sBAQN,MAAM;;;;
|
|
1
|
+
{"version":3,"file":"register-api-types.d.ts","sourceRoot":"","sources":["../../../../src/loaders/api/register-api-types.js"],"names":[],"mappings":";;;;;;mBAOc,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,KAAK,IAAI;;;;SAC3C,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAA;KAAC,KAAK,IAAI;;;;iBACxH,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,KAAK,IAAI;;;;cAC1D,CAAC,MAAM,EAAE,sBAAsB,KAAK,WAAW;;;;gBAC/C,MAAM,IAAI;;;;uBACV,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,KAAK,IAAI;;;;aAChD,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAC,KAAK,CAAC,EAAE,MAAM,GAAC,eAAe,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,GAAC,eAAe,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAC,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAC,CAAC;;;;2BACtM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI;;;;wBAC9B,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,IAAI;;;;eAClF,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,YAAY,CAAC,EAAE,OAAO,KAAK,IAAI;;;;cACtD,mBAAmB;;;;;;QAKnB,MAAM,GAAC,MAAM;;;;UACb,MAAM;;;;;;;;;;;;;;;;;;;;sBAQN,MAAM;;;;aACN,kBAAkB;;;;YAClB,iBAAiB;;;;;;QAKjB,MAAM;;;;UACN,MAAM;;;;;;;;;;;;;;;;;;;;kBAQN,MAAM;;;;;;;;gBAEN,MAAM;;;;cACN,MAAM;;;;;;;;UAEN,MAAM"}
|
package/package.json
CHANGED
|
@@ -4,9 +4,43 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { globalScope } from '../constants/runtime'
|
|
7
|
+
import { now } from '../timing/now'
|
|
7
8
|
import { cleanURL } from '../url/clean-url'
|
|
8
9
|
import { chrome, gecko } from './browser-stack-matchers'
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** @type {(entry: PerformanceEntry) => boolean} - A shared function to determine if a performance entry is a valid script or link resource for evaluation */
|
|
16
|
+
const validEntryCriteria = entry => entry.initiatorType === 'script' || (entry.initiatorType === 'link' && entry.name.endsWith('.js'))
|
|
17
|
+
|
|
18
|
+
/** @type {Set<PerformanceResourceTiming>} - A set of resource timing objects that are "valid" -- see "validEntryCriteria" */
|
|
19
|
+
const scripts = new Set()
|
|
20
|
+
/** @type {Array<{ test: (entry: PerformanceEntry) => boolean, addedAt: number }>} an array of PerformanceObserver subscribers to check for late emissions */
|
|
21
|
+
let poSubscribers = []
|
|
22
|
+
|
|
23
|
+
if (globalScope.PerformanceObserver?.supportedEntryTypes.includes('resource')) {
|
|
24
|
+
/** We must track the script assets this way, because the performance buffer can fill up and when it does that
|
|
25
|
+
* it stops accepting new entries (instead of dropping old entries), which means if the register API is called
|
|
26
|
+
* after the buffer fills up we won't be able to get the script timing information from the resource timing API
|
|
27
|
+
*/
|
|
28
|
+
const scriptObserver = new PerformanceObserver((list) => {
|
|
29
|
+
list.getEntries().forEach((entry) => {
|
|
30
|
+
if (validEntryCriteria(entry)) {
|
|
31
|
+
if (scripts.size > 250) scripts.delete(scripts.values().next().value) // keep the set from growing indefinitely, we only need to check recent entries against the stack of the register API caller, so we can drop old entries as new ones come in
|
|
32
|
+
scripts.add(entry)
|
|
33
|
+
const canClear = []
|
|
34
|
+
poSubscribers.forEach(({ test, addedAt }, idx) => {
|
|
35
|
+
if (test(entry) || now() - addedAt > 10000) canClear.push(idx) // Clear subscribers that have resolved or have been pending for more than 10 seconds
|
|
36
|
+
})
|
|
37
|
+
poSubscribers = poSubscribers.filter((_, idx) => !canClear.includes(idx))
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
scriptObserver.observe({ type: 'resource', buffered: true })
|
|
42
|
+
}
|
|
43
|
+
|
|
10
44
|
/**
|
|
11
45
|
* Extracts URLs from stack traces using the same logic as compute-stack-trace.js
|
|
12
46
|
* @param {string} stack The error stack trace
|
|
@@ -45,29 +79,89 @@ function getDeepStackTrace () {
|
|
|
45
79
|
return stack
|
|
46
80
|
}
|
|
47
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Indicates whether the provided URL matches any script preload link tags in the document.
|
|
84
|
+
* @param {string} targetUrl The URL to match against preload tags
|
|
85
|
+
* @returns {boolean} True if a matching preload link is found, false otherwise
|
|
86
|
+
*/
|
|
87
|
+
function wasPreloaded (targetUrl) {
|
|
88
|
+
if (!targetUrl || !globalScope.document) return false
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const linkTags = globalScope.document.querySelectorAll('link[rel="preload"][as="script"]')
|
|
92
|
+
for (const link of linkTags) {
|
|
93
|
+
// link.href is resolved to an absolute URL by the browser (even if supplied as relative), so we can match exactly against the cleaned target URL
|
|
94
|
+
if (cleanURL(link.href) === targetUrl) return true
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// Don't let DOM parsing errors break anything
|
|
98
|
+
}
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
48
102
|
/**
|
|
49
103
|
* Uses the stack of the initiator function, returns script timing information if a script can be found with the resource timing API matching the URL found in the stack.
|
|
50
|
-
* @returns {
|
|
104
|
+
* @returns {RegisterAPITimings} Object containing script fetch start and end times, and the asset URL if found
|
|
51
105
|
*/
|
|
52
106
|
export function findScriptTimings () {
|
|
107
|
+
const timings = { registeredAt: now(), reportedAt: undefined, fetchStart: 0, fetchEnd: 0, asset: undefined, type: 'unknown' }
|
|
53
108
|
const stack = getDeepStackTrace()
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
if (scripts.length < 1 || !stack) return timings
|
|
109
|
+
if (!stack) return timings
|
|
110
|
+
const navUrl = globalScope.performance?.getEntriesByType('navigation')?.find(entry => entry.initiatorType === 'navigation')?.name || ''
|
|
57
111
|
|
|
58
112
|
try {
|
|
59
113
|
const mfeScriptUrl = extractUrlsFromStack(stack).at(-1) // array of URLs from the stack of the register API caller, the MFE script should be at the bottom
|
|
60
114
|
if (!mfeScriptUrl) return timings
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
115
|
+
if (navUrl.includes(mfeScriptUrl)) {
|
|
116
|
+
// this means the stack is indicating that the registration came from an inline script or eval, so we won't find a matching script resource - return early with just the URL
|
|
117
|
+
timings.asset = cleanURL(navUrl)
|
|
118
|
+
timings.type = 'inline'
|
|
119
|
+
return timings
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// try to find it in the static list, which updates faster than the PO. This cant be solely trusted, since the buffer can fill up and stop accepting entries,
|
|
123
|
+
// but it can still help in cases where the check is made before the asset is emitted by the performance observer and the buffer is not full. Fallback to checking the PO
|
|
124
|
+
// entries that have been buffered as seen in the PO callback if its not found in the static list.
|
|
125
|
+
const match = performance.getEntriesByType('resource').find(entryMatchesMfe) || [...scripts].find(entryMatchesMfe)
|
|
126
|
+
|
|
127
|
+
if (match) { setMatchedAttributes(match) } else {
|
|
128
|
+
// We didnt find a match with the PO, nor a static lookup... check if its preloaded and if so, set basic fallbacks and try to associate with a later script entry if possible, this can happen if the preload is reported late in the PO observer callback
|
|
129
|
+
if (wasPreloaded(mfeScriptUrl)) {
|
|
130
|
+
timings.asset = mfeScriptUrl
|
|
131
|
+
timings.type = 'preload'
|
|
132
|
+
// wait for a late PO callback... The timings object can be mutated after the fact since we return a pointer and not a cloned object
|
|
133
|
+
poSubscribers.push({
|
|
134
|
+
addedAt: now(),
|
|
135
|
+
test: (entry) => {
|
|
136
|
+
if (entryMatchesMfe(entry)) {
|
|
137
|
+
setMatchedAttributes(entry)
|
|
138
|
+
return true // return true so that we know to clear this callback from the pending list since we found our match, otherwise it will stay in the list and be called for future entries which is unnecessary after we found our match and can cause performance issues if there are a lot of future entries and pending callbacks
|
|
139
|
+
}
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A matcher function to determine if a performance entry corresponds to the MFE script based on URL matching. It checks if either the entry URL ends with the MFE script URL or vice versa, to account for potential differences in how URLs are represented in the stack trace versus the resource timing entries.
|
|
148
|
+
* @param {PerformanceResourceTiming} entry - The resource timing entry to compare to the MFE script
|
|
149
|
+
* @returns {boolean} True if we think the entry matches the MFE script, false otherwise
|
|
150
|
+
*/
|
|
151
|
+
function entryMatchesMfe (entry) {
|
|
152
|
+
const entryUrl = cleanURL(entry.name)
|
|
153
|
+
return entryUrl.endsWith(mfeScriptUrl) || mfeScriptUrl.endsWith(entryUrl)
|
|
154
|
+
}
|
|
66
155
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
156
|
+
/**
|
|
157
|
+
* A helper function to set the matched timing attributes on the timings object based on a performance entry. This is called when we have identified a resource timing entry that we believe corresponds to the MFE script, and we want to extract the fetch start and end times, as well as the asset URL and type.
|
|
158
|
+
* @param {PerformanceResourceTiming} entry - The resource timing entry to base values off of
|
|
159
|
+
*/
|
|
160
|
+
function setMatchedAttributes (entry) {
|
|
161
|
+
timings.fetchStart = Math.floor(entry.startTime)
|
|
162
|
+
timings.fetchEnd = Math.floor(entry.responseEnd)
|
|
163
|
+
timings.asset = entry.name
|
|
164
|
+
timings.type = entry.initiatorType
|
|
71
165
|
}
|
|
72
166
|
} catch (error) {
|
|
73
167
|
// Don't let stack parsing errors break anything
|
|
@@ -30,19 +30,27 @@
|
|
|
30
30
|
/**
|
|
31
31
|
* @typedef {Object} RegisterAPIMetadata
|
|
32
32
|
* @property {Object} customAttributes - The custom attributes for the registered entity.
|
|
33
|
-
* @property {
|
|
34
|
-
* @property {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* @
|
|
39
|
-
* @property {
|
|
40
|
-
* @property {string}
|
|
41
|
-
* @property {string}
|
|
42
|
-
* @property {string}
|
|
43
|
-
* @property {
|
|
44
|
-
|
|
45
|
-
|
|
33
|
+
* @property {RegisterAPITimings} timings - The timing metrics for the registered entity.
|
|
34
|
+
* @property {RegisterAPITarget} target - The options for the registered entity.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} RegisterAPITarget
|
|
39
|
+
* @property {string} id - The ID for the registered entity.
|
|
40
|
+
* @property {string} name - The name returned for the registered entity.
|
|
41
|
+
* @property {{[key: string]: any}} [tags] - The tags for the registered entity as key-value pairs.
|
|
42
|
+
* @property {string} [parentId] - The parentId for the registered entity. If none was supplied, it will assume the entity guid from the main agent.
|
|
43
|
+
* @property {boolean} [isolated] - When true, each registration creates an isolated instance. When false, multiple registrations with the same id and isolated: false will share a single instance, including all custom attributes, ids, names, and metadata. Calling deregister on a shared instance will deregister it for all entities using the instance. Defaults to true.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} RegisterAPITimings
|
|
48
|
+
* @property {number} registeredAt - The timestamp when the registered entity was created.
|
|
49
|
+
* @property {number} [reportedAt] - The timestamp when the registered entity was deregistered.
|
|
50
|
+
* @property {number} fetchStart - The timestamp when the registered entity began fetching.
|
|
51
|
+
* @property {number} fetchEnd - The timestamp when the registered entity finished fetching.
|
|
52
|
+
* @property {Object} [asset] - The asset path (if found) for the registered entity.
|
|
53
|
+
* @property {string} type - The type of timing associated with the registered entity, 'script' or 'link' if found with the performance resource API, 'inline' if found to be associated with the root document URL, or 'unknown' if no associated resource could be found.
|
|
46
54
|
*/
|
|
47
55
|
|
|
48
56
|
export default {}
|
|
@@ -54,11 +54,7 @@ function register (agentRef, target, parent) {
|
|
|
54
54
|
target.parent = parent || {}
|
|
55
55
|
if (typeof target.tags !== 'object' || target.tags === null || Array.isArray(target.tags)) target.tags = {}
|
|
56
56
|
|
|
57
|
-
const timings =
|
|
58
|
-
registeredAt: now(),
|
|
59
|
-
reportedAt: undefined,
|
|
60
|
-
...findScriptTimings()
|
|
61
|
-
}
|
|
57
|
+
const timings = findScriptTimings()
|
|
62
58
|
|
|
63
59
|
const attrs = {}
|
|
64
60
|
|
|
@@ -148,6 +144,8 @@ function register (agentRef, target, parent) {
|
|
|
148
144
|
if (timings.reportedAt) return
|
|
149
145
|
timings.reportedAt = now()
|
|
150
146
|
api.recordCustomEvent('MicroFrontEndTiming', {
|
|
147
|
+
assetUrl: timings.asset, // the url of the script that was registered, or undefined if it could not be determined (inline or no match)
|
|
148
|
+
assetType: timings.type, // the type of asset that was associated with the timings, one of 'script', 'link' (if preloaded and found in the resource timing buffer), 'preload' (if preloaded but not found in the resource timing buffer), or "unknown" if it could not be determined
|
|
151
149
|
timeToLoad: timings.registeredAt - timings.fetchStart, // fetchStart to registeredAt
|
|
152
150
|
timeToBeRequested: timings.fetchStart, // origin to fetchStart
|
|
153
151
|
timeToFetch: timings.fetchEnd - timings.fetchStart, // fetchStart to fetchEnd
|