@newrelic/browser-agent 1.321.0-rc.8 → 1.321.0-rc.9

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.
Files changed (46) hide show
  1. package/dist/cjs/common/constants/env.cdn.js +1 -1
  2. package/dist/cjs/common/constants/env.npm.js +1 -1
  3. package/dist/cjs/common/util/console.js +1 -0
  4. package/dist/cjs/common/v2/manifest.js +142 -0
  5. package/dist/cjs/common/v2/script-tracker-types.js +27 -0
  6. package/dist/cjs/common/v2/script-tracker.js +223 -25
  7. package/dist/cjs/common/v2/utils.js +47 -1
  8. package/dist/cjs/features/generic_events/aggregate/index.js +28 -1
  9. package/dist/cjs/loaders/api/register-api-types.js +13 -4
  10. package/dist/cjs/loaders/api/register.js +18 -0
  11. package/dist/esm/common/constants/env.cdn.js +1 -1
  12. package/dist/esm/common/constants/env.npm.js +1 -1
  13. package/dist/esm/common/util/console.js +1 -0
  14. package/dist/esm/common/v2/manifest.js +136 -0
  15. package/dist/esm/common/v2/script-tracker-types.js +24 -0
  16. package/dist/esm/common/v2/script-tracker.js +222 -26
  17. package/dist/esm/common/v2/utils.js +46 -1
  18. package/dist/esm/features/generic_events/aggregate/index.js +29 -2
  19. package/dist/esm/loaders/api/register-api-types.js +14 -4
  20. package/dist/esm/loaders/api/register.js +18 -1
  21. package/dist/tsconfig.tsbuildinfo +1 -1
  22. package/dist/types/common/config/init-types.d.ts +1 -1
  23. package/dist/types/common/util/console.d.ts +1 -0
  24. package/dist/types/common/util/console.d.ts.map +1 -1
  25. package/dist/types/common/v2/manifest.d.ts +66 -0
  26. package/dist/types/common/v2/manifest.d.ts.map +1 -0
  27. package/dist/types/common/v2/script-tracker-types.d.ts +25 -0
  28. package/dist/types/common/v2/script-tracker-types.d.ts.map +1 -0
  29. package/dist/types/common/v2/script-tracker.d.ts +25 -4
  30. package/dist/types/common/v2/script-tracker.d.ts.map +1 -1
  31. package/dist/types/common/v2/utils.d.ts +12 -0
  32. package/dist/types/common/v2/utils.d.ts.map +1 -1
  33. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  34. package/dist/types/loaders/api/register-api-types.d.ts +31 -4
  35. package/dist/types/loaders/api/register-api-types.d.ts.map +1 -1
  36. package/dist/types/loaders/api/register.d.ts +1 -0
  37. package/dist/types/loaders/api/register.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/common/util/console.js +1 -0
  40. package/src/common/v2/manifest.js +126 -0
  41. package/src/common/v2/script-tracker-types.js +24 -0
  42. package/src/common/v2/script-tracker.js +218 -26
  43. package/src/common/v2/utils.js +42 -1
  44. package/src/features/generic_events/aggregate/index.js +22 -2
  45. package/src/loaders/api/register-api-types.js +14 -4
  46. package/src/loaders/api/register.js +16 -1
@@ -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.321.0-rc.8";
20
+ const VERSION = exports.VERSION = "1.321.0-rc.9";
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.321.0-rc.8";
20
+ const VERSION = exports.VERSION = "1.321.0-rc.9";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -100,6 +100,7 @@ var _globalEvent = require("../dispatch/global-event");
100
100
  * | 77 | Agent rejected post message, could not validate origin. |
101
101
  * | 78 | RegisteredIframeEntity could not determine parent origin and will not register, to avoid trusting messages from any origin. |
102
102
  * | 79 | Unable to initialize Connector and/or Harvester. |
103
+ * | 80 | An invalid manifest option was provided to register() and will be ignored. |
103
104
  * | 81 | Entities were detected that share a name with different IDs - This can cause multiple entities to have the same name in New Relic. |
104
105
  * | 82 | Entities were detected that share an ID with different names - This can cause your entity's name to change unexpectedly. |
105
106
  *
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.matchManifestAsset = matchManifestAsset;
7
+ exports.parseManifest = parseManifest;
8
+ var _cleanUrl = require("../url/clean-url");
9
+ var _console = require("../util/console");
10
+ var _invoke = require("../util/invoke");
11
+ /**
12
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
13
+ * SPDX-License-Identifier: Apache-2.0
14
+ */
15
+
16
+ /**
17
+ * @typedef {Object} AssetFile
18
+ * @property {string|RegExp} matcher - the path/path-fragment string, or RegExp, used to match a resolved URL against this asset
19
+ * @property {'script'|'asset'} [type] - optional override for script-capability inference. When omitted, script-capability is inferred from a
20
+ * `.js` suffix on a string `matcher` (a RegExp `matcher` is never inferred as a script). Supply `type: 'script'` to explicitly flag an entry
21
+ * as a script regardless of its matcher shape/suffix (e.g. an extensionless URL, or a RegExp targeting a script path), or `type: 'asset'` to
22
+ * explicitly flag it as non-script. Any other value is invalid, logs a warning, and is ignored (falls back to inference).
23
+ */
24
+
25
+ /** Warns at most once per page for any invalid manifest entry (bad `matcher` or bad `type`) -- mirrors the
26
+ * existing `invalidTimingMethod` single()-wrapped warning pattern in register.js. */
27
+ const warnInvalidManifestEntry = (0, _invoke.single)(secondary => (0, _console.warn)(80, secondary));
28
+
29
+ /**
30
+ * @typedef {Object} ParsedManifestAsset
31
+ * @property {string|RegExp} pattern - the original matcher supplied by the customer
32
+ * @property {(url: string) => boolean} test - precompiled matcher against a resolved URL
33
+ * @property {boolean} isScript
34
+ */
35
+
36
+ /**
37
+ * @typedef {Object} ParsedManifest
38
+ * @property {ParsedManifestAsset[]} assets - all supplied assets
39
+ * @property {ParsedManifestAsset[]} scripts - the subset of assets inferred/declared as scripts (`.js`)
40
+ */
41
+
42
+ /**
43
+ * Parses a raw manifest supplied to `register()` into precompiled matcher closures. Parsing happens once, at
44
+ * registration time, so downstream event attribution (which runs on every ajax/error/log/websocket event) never has
45
+ * to re-derive matching logic from the raw customer input.
46
+ * @param {{assets?: Array<AssetFile>}} [rawManifest]
47
+ * @returns {ParsedManifest|undefined} undefined if no usable assets were supplied, so callers can cheaply skip all manifest logic
48
+ */
49
+ function parseManifest(rawManifest) {
50
+ if (!Array.isArray(rawManifest?.assets) || !rawManifest.assets.length) return undefined;
51
+ const assets = rawManifest.assets.map(parseAsset).filter(Boolean);
52
+ if (!assets.length) return undefined;
53
+ return {
54
+ assets,
55
+ scripts: assets.filter(asset => asset.isScript)
56
+ };
57
+ }
58
+
59
+ /**
60
+ * @param {AssetFile} entry
61
+ * @returns {ParsedManifestAsset|undefined}
62
+ */
63
+ function parseAsset(entry) {
64
+ if (!entry || typeof entry !== 'object') return undefined;
65
+ const {
66
+ matcher,
67
+ type
68
+ } = entry;
69
+ const inferredIsScript = typeof matcher === 'string' && isScriptPath(matcher);
70
+ let isScript = inferredIsScript;
71
+ if (type !== undefined) {
72
+ if (isValidType(type)) {
73
+ isScript = isScriptType(type);
74
+ } else {
75
+ // Invalid `type` never overrides -- fall back to inference-only behavior rather than forcing `isScript: false`,
76
+ // so a typo'd `type` can't silently break stack-trace attribution for an otherwise-valid `.js` matcher.
77
+ warnInvalidManifestEntry(type);
78
+ }
79
+ }
80
+ if (isRegExp(matcher)) {
81
+ // Reset lastIndex before every test -- a `g`/`y` flagged matcher otherwise carries state across calls
82
+ // (this closure is reused for every event), causing intermittent, input-order-dependent match failures.
83
+ return {
84
+ pattern: matcher,
85
+ test: url => {
86
+ matcher.lastIndex = 0;
87
+ return matcher.test((0, _cleanUrl.cleanURL)(url));
88
+ },
89
+ isScript
90
+ };
91
+ }
92
+ if (typeof matcher === 'string' && matcher.length > 0) {
93
+ return {
94
+ pattern: matcher,
95
+ test: url => (0, _cleanUrl.cleanURL)(url).includes(matcher),
96
+ isScript
97
+ };
98
+ }
99
+
100
+ // Every other matcher shape (missing, empty string, null, number, plain object, ...) is invalid -- notably an
101
+ // empty string must be rejected here rather than falling through to the string branch above, since
102
+ // ''.includes('') is always true and would otherwise silently match every URL.
103
+ warnInvalidManifestEntry(matcher);
104
+ return undefined;
105
+ }
106
+ function isScriptPath(path) {
107
+ return path.endsWith('.js');
108
+ }
109
+ function isValidType(type) {
110
+ return type === 'script' || type === 'asset';
111
+ }
112
+
113
+ /**
114
+ * Cross-realm-safe check for whether a value is a RegExp. `instanceof RegExp` only returns true when the value's
115
+ * prototype chain links to THIS realm's `RegExp.prototype` -- a regex literal constructed in a different realm
116
+ * (an iframe, a Worker via structured clone, a WebDriver sandbox such as Firefox's geckodriver executeScript
117
+ * context) is still a genuine RegExp, just not an instance of this realm's constructor, and would otherwise be
118
+ * silently dropped by parseAsset below.
119
+ * @param {*} value
120
+ * @returns {boolean}
121
+ */
122
+ function isRegExp(value) {
123
+ return Object.prototype.toString.call(value) === '[object RegExp]';
124
+ }
125
+ function isScriptType(type) {
126
+ return type === 'script';
127
+ }
128
+
129
+ /**
130
+ * Determines whether a resolved URL matches any asset in a parsed manifest.
131
+ * @param {ParsedManifest|undefined} parsed
132
+ * @param {string} url
133
+ * @param {{scriptsOnly?: boolean}} [options]
134
+ * @returns {boolean}
135
+ */
136
+ function matchManifestAsset(parsed, url, {
137
+ scriptsOnly = false
138
+ } = {}) {
139
+ if (!parsed || !url) return false;
140
+ const candidates = scriptsOnly ? parsed.scripts : parsed.assets;
141
+ return candidates.some(asset => asset.test(url));
142
+ }
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ /**
8
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
9
+ * SPDX-License-Identifier: Apache-2.0
10
+ */
11
+ /**
12
+ * @typedef {(start: number, end: number) => void} RecordManifestScriptWindowFn - Widens the live scriptStart/
13
+ * scriptEnd window with one manifest script asset's DOM correlation timing. `start`/`end` are that asset's
14
+ * `correlation.script.start`/`.end`, or falsy if not yet resolved. See `findScriptTimings`, which registers the
15
+ * concrete implementation, for the exact widening semantics (never shrinks either bound).
16
+ */
17
+ /**
18
+ * @typedef {Object} TimingsInternals
19
+ * @property {Set<string>} weighedAssetUrls - Always present (seeded by `getOrCreateInternals`, see
20
+ * `script-tracker.js`). Cleaned URLs already folded into `timings.totalWeight`/`timings.renderBlocking` by
21
+ * `applyResourceWeight`, so the same underlying resource (e.g. a manifest asset that's also the .register calling
22
+ * script itself) is never counted twice.
23
+ * @property {RecordManifestScriptWindowFn} recordManifestScriptWindow - Always present (seeded by
24
+ * `getOrCreateInternals`, see `script-tracker.js`). `findScriptTimings` overrides the seeded default with one that
25
+ * folds into the live `scriptStart`/`scriptEnd` getters instead.
26
+ */
27
+ var _default = exports.default = {};
@@ -3,7 +3,9 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.applyManifestTimings = applyManifestTimings;
6
7
  exports.extractUrlsFromStack = extractUrlsFromStack;
8
+ exports.findCorrelation = findCorrelation;
7
9
  exports.findScriptTimings = findScriptTimings;
8
10
  exports.getDeepStackTrace = getDeepStackTrace;
9
11
  exports.thisFile = exports.scriptCorrelations = void 0;
@@ -20,8 +22,10 @@ var _timingFactory = require("./timing-factory");
20
22
  */
21
23
 
22
24
  /**
23
- * @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
25
+ * @typedef {import('../../loaders/api/register-api-types').RegisterAPITimings} RegisterAPITimings
24
26
  * @typedef {import('../../loaders/api/register-api-types').RegisterAPITarget} RegisterAPITarget
27
+ * @typedef {import('./script-tracker-types').RecordManifestScriptWindowFn} RecordManifestScriptWindowFn
28
+ * @typedef {import('./script-tracker-types').TimingsInternals} TimingsInternals
25
29
  */
26
30
 
27
31
  /** export for testing purposes */
@@ -41,7 +45,39 @@ const scriptCorrelations = exports.scriptCorrelations = new Map();
41
45
  let poSubscribers = [];
42
46
 
43
47
  /**
44
- * Retrieves a script correlation by URL using exact matching
48
+ * Bookkeeping keyed by a `timings` object, kept off the object itself since it's exposed directly to customers via
49
+ * `register().metadata.timings`.
50
+ * @type {WeakMap<RegisterAPITimings, TimingsInternals>}
51
+ */
52
+ const timingsInternals = new WeakMap();
53
+
54
+ /**
55
+ * Gets (or lazily creates) the bookkeeping record for a `timings` object. A fresh record's `recordManifestScriptWindow`
56
+ * defaults to widening `timings.scriptStart`/`scriptEnd` directly -- correct for a plain `timings` object never
57
+ * produced by `findScriptTimings`. `findScriptTimings` overrides that default with one that folds into its live
58
+ * getters instead.
59
+ * @param {RegisterAPITimings} timings
60
+ * @returns {TimingsInternals}
61
+ */
62
+ function getOrCreateInternals(timings) {
63
+ let internals = timingsInternals.get(timings);
64
+ if (!internals) {
65
+ internals = {
66
+ weighedAssetUrls: new Set(),
67
+ recordManifestScriptWindow: (start, end) => {
68
+ if (start) timings.scriptStart = timings.scriptStart > 0 ? Math.min(timings.scriptStart, start) : start;
69
+ if (end) timings.scriptEnd = timings.scriptEnd > 0 ? Math.max(timings.scriptEnd, end) : end;
70
+ }
71
+ };
72
+ timingsInternals.set(timings, internals);
73
+ }
74
+ return internals;
75
+ }
76
+
77
+ /**
78
+ * Retrieves a script correlation by URL using exact matching. Exported so other features (e.g. generic_events'
79
+ * resource attribution) can key off the same DOM node/load-timing tracking this module already does for every
80
+ * `<script>` element, rather than setting up a second, redundant observer.
45
81
  * @param {string} targetUrl - The URL to find
46
82
  * @returns {ScriptCorrelation | undefined} - The correlation object if found
47
83
  */
@@ -94,20 +130,23 @@ if (_runtime.globalScope.MutationObserver && _runtime.globalScope.document) {
94
130
  });
95
131
  }
96
132
  if (_runtime.globalScope.PerformanceObserver?.supportedEntryTypes.includes('resource')) {
97
- /** We must track the script assets this way, because the performance buffer can fill up and when it does that
98
- * it stops accepting new entries (instead of dropping old entries), which means if the register API is called
99
- * after the buffer fills up we won't be able to get the script timing information from the resource timing API
100
- */
133
+ // Tracked via an observer (not a later buffer read) because the performance buffer stops accepting new entries
134
+ // once full, instead of dropping old ones -- a late register() call could otherwise miss timing entirely.
101
135
  const scriptObserver = new PerformanceObserver(list => {
102
- list.getEntries().filter(validEntryCriteria).forEach(entry => {
103
- // Update correlation with performance data (creates entry if needed)
104
- const entryUrl = (0, _cleanUrl.cleanURL)(entry.name);
105
- const correlation = getOrCreateCorrelation(entryUrl);
106
- correlation.performance.start = Math.floor(entry.startTime);
107
- correlation.performance.end = Math.floor(entry.responseEnd);
108
- correlation.performance.value = entry;
109
-
110
- // Clear resolved or expired subscribers
136
+ list.getEntries().forEach(entry => {
137
+ // Correlation bookkeeping only makes sense for script-like entries -- gated on validEntryCriteria so
138
+ // scriptCorrelations doesn't grow for every image/css/font load on the page.
139
+ if (validEntryCriteria(entry)) {
140
+ const entryUrl = (0, _cleanUrl.cleanURL)(entry.name);
141
+ const correlation = getOrCreateCorrelation(entryUrl);
142
+ correlation.performance.start = Math.floor(entry.startTime);
143
+ correlation.performance.end = Math.floor(entry.responseEnd);
144
+ correlation.performance.value = entry;
145
+ }
146
+
147
+ // Late-resolution subscribers can be for any asset type (not just scripts), so every entry is checked here,
148
+ // unfiltered. Skipped when nothing is pending, the common case.
149
+ if (!poSubscribers.length) return;
111
150
  const canClear = [];
112
151
  poSubscribers.forEach(({
113
152
  test,
@@ -206,6 +245,33 @@ function applyPerformanceEntry(timings, entry) {
206
245
  timings.fetchEnd = Math.floor(entry.responseEnd);
207
246
  timings.asset = entry.name;
208
247
  timings.type = entry.initiatorType;
248
+ applyResourceWeight(timings, entry);
249
+ }
250
+
251
+ /**
252
+ * Accumulates the byte weight and render-blocking status of a single detected asset (the entry script or a resolved
253
+ * manifest asset) into a timings object. Shared by both the entry-script path (`applyPerformanceEntry`) and the
254
+ * manifest path (`applyManifestEntry`) so `totalWeight`/`renderBlocking` reflect every asset actually detected,
255
+ * regardless of which path found it.
256
+ * @param {RegisterAPITimings} timings
257
+ * @param {PerformanceResourceTiming} entry
258
+ */
259
+ function applyResourceWeight(timings, entry) {
260
+ // De-dupe by cleaned URL: a manifest can list the .register calling script itself as one of its own assets,
261
+ // which would otherwise weigh the same resource twice (once via findScriptTimings, once via applyManifestTimings).
262
+ const url = (0, _cleanUrl.cleanURL)(entry.name);
263
+ const {
264
+ weighedAssetUrls
265
+ } = getOrCreateInternals(timings);
266
+ if (weighedAssetUrls.has(url)) return;
267
+ weighedAssetUrls.add(url);
268
+
269
+ // transferSize is 0 for cross-origin responses without Timing-Allow-Origin (a privacy restriction, not a
270
+ // zero-byte asset) -- adding 0 is correct either way.
271
+ timings.totalWeight = (timings.totalWeight || 0) + (entry.transferSize || 0);
272
+ // 'blocking' always wins and never gets downgraded; 'non-blocking' only applies if nothing already resolved
273
+ // true; no value at all (unsupported browser) leaves renderBlocking untouched (stays `undefined`).
274
+ if (entry.renderBlockingStatus === 'blocking') timings.renderBlocking = true;else if (entry.renderBlockingStatus === 'non-blocking' && timings.renderBlocking !== true) timings.renderBlocking = false;
209
275
  }
210
276
 
211
277
  /**
@@ -228,8 +294,114 @@ function subscribeToLatePerformanceEntry(timings, mfeScriptUrl) {
228
294
  }
229
295
 
230
296
  /**
231
- * 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.
232
- * @param {RegisterAPITarget} [target] - The MFE target being registered. Its id is used to scope stale-correlation detection per-MFE rather than per-script-URL, so one script registering multiple distinct MFEs doesn't misclassify a later MFE's registration as a stale reuse of an earlier one's.
297
+ * Applies one manifest asset's performance entry to a timings object: weight/renderBlocking always accumulate;
298
+ * fetchStart/fetchEnd and scriptStart/scriptEnd widen (never shrink) only when `timingMethod` calls for it; asset/
299
+ * type get anchored to the first script asset seen to resolve.
300
+ * @param {RegisterAPITimings} timings
301
+ * @param {PerformanceResourceTiming} entry
302
+ * @param {import('./manifest').ParsedManifestAsset} asset - the manifest asset this entry resolved
303
+ * @param {{ resolved: boolean }} entryState - shared "first script asset wins" guard for a single `applyManifestTimings` call
304
+ * @param {'entry'|'scripts'|'all'} [timingMethod] - the registered MFE's timing method; `undefined`/'entry' means weight/render-blocking still accumulate, but no timing widening happens at all
305
+ */
306
+ function applyManifestEntry(timings, entry, asset, entryState, timingMethod) {
307
+ // Weight isn't a timing concern, so it accumulates for every matched asset regardless of timingMethod.
308
+ applyResourceWeight(timings, entry);
309
+ if (timingMethod !== 'scripts' && timingMethod !== 'all') return; // no timing-widening effect at the 'entry' default/unset
310
+
311
+ const widensAllAssets = timingMethod === 'all';
312
+ // Under 'scripts', only script assets widen the fetch window; under 'all', every matched asset does.
313
+ if (widensAllAssets || asset.isScript) {
314
+ const start = Math.floor(entry.startTime);
315
+ const end = Math.floor(entry.responseEnd);
316
+ // fetchStart/fetchEnd default to 0 ("not yet found") -- only fold into the min/max once they're positive,
317
+ // or 0 would permanently win Math.min.
318
+ timings.fetchStart = timings.fetchStart > 0 ? Math.min(timings.fetchStart, start) : start;
319
+ timings.fetchEnd = timings.fetchEnd > 0 ? Math.max(timings.fetchEnd, end) : end;
320
+ }
321
+
322
+ // Non-script assets never execute, so only script assets widen the execution window or anchor asset/type.
323
+ if (asset.isScript) {
324
+ const correlation = findCorrelation((0, _cleanUrl.cleanURL)(entry.name));
325
+ if (correlation) {
326
+ // Widens the aggregate scriptStart/scriptEnd window with this asset's current correlation timing. Re-called
327
+ // as a 'load'/'error' listener below if its DOM completion hasn't fired yet, so a later, larger end still counts.
328
+ const widenScriptWindowForAsset = () => {
329
+ const {
330
+ start: scriptStart,
331
+ end: scriptEnd
332
+ } = correlation.script;
333
+ getOrCreateInternals(timings).recordManifestScriptWindow(scriptStart, scriptEnd);
334
+ };
335
+ widenScriptWindowForAsset();
336
+ if (!correlation.dom.end && correlation.dom.value) {
337
+ ;
338
+ ['load', 'error'].forEach(eventType => correlation.dom.value.addEventListener(eventType, widenScriptWindowForAsset, {
339
+ once: true
340
+ }));
341
+ }
342
+ }
343
+ if (!entryState.resolved) {
344
+ timings.asset = entry.name;
345
+ timings.type = entry.initiatorType;
346
+ entryState.resolved = true;
347
+ }
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Subscribes to late resource timing emissions for manifest assets not yet resolved against the buffered entries.
353
+ * Reuses the shared page-wide scriptObserver/poSubscribers mechanism (one PerformanceObserver for all MFEs, not
354
+ * one per MFE) and, unlike that observer's own correlation bookkeeping, checks every resource entry -- not just
355
+ * script-like ones -- so lazy-loaded images/fonts/stylesheets resolve too.
356
+ * @param {RegisterAPITimings} timings
357
+ * @param {Set<import('./manifest').ParsedManifestAsset>} pending - manifest assets still unresolved
358
+ * @param {{ resolved: boolean }} entryState - shared "first script asset wins" guard for a single `applyManifestTimings` call
359
+ * @param {'entry'|'scripts'|'all'} [timingMethod] - forwarded to `applyManifestEntry` for each late-resolving asset
360
+ */
361
+ function subscribeToLateManifestEntries(timings, pending, entryState, timingMethod) {
362
+ if (!_runtime.globalScope.PerformanceObserver?.supportedEntryTypes?.includes('resource')) return;
363
+ poSubscribers.push({
364
+ addedAt: (0, _now.now)(),
365
+ test: entry => {
366
+ const matched = [...pending].find(asset => asset.test(entry.name));
367
+ if (matched) {
368
+ applyManifestEntry(timings, entry, matched, entryState, timingMethod);
369
+ pending.delete(matched);
370
+ }
371
+ return pending.size === 0;
372
+ }
373
+ });
374
+ }
375
+
376
+ /**
377
+ * Applies a registered MFE's manifest to a timings object (already populated by `findScriptTimings`). No-op if no
378
+ * manifest is present. Weight/renderBlocking always accumulate from every detected manifest asset; timing widening
379
+ * (fetchStart/fetchEnd/scriptStart/scriptEnd/asset anchor) is opt-in via `timingMethod` -- see `applyManifestEntry`.
380
+ * @param {RegisterAPITimings} timings - the timings object to widen in place
381
+ * @param {RegisterAPITarget} target - the registered MFE target, which may carry a parsed `manifest`
382
+ */
383
+ function applyManifestTimings(timings, target) {
384
+ const parsedManifest = target?.manifest;
385
+ if (!parsedManifest || !parsedManifest.assets.length) return;
386
+ const entryState = {
387
+ resolved: false
388
+ };
389
+ const pending = new Set(parsedManifest.assets);
390
+ const resourceEntries = _runtime.globalScope.performance?.getEntriesByType('resource') || [];
391
+ resourceEntries.forEach(entry => {
392
+ const matched = [...pending].find(asset => asset.test(entry.name));
393
+ if (matched) {
394
+ applyManifestEntry(timings, entry, matched, entryState, target.timingMethod);
395
+ pending.delete(matched);
396
+ }
397
+ });
398
+ if (pending.size) subscribeToLateManifestEntries(timings, pending, entryState, target.timingMethod);
399
+ }
400
+
401
+ /**
402
+ * Uses the initiator function's stack to find script timing information via the resource timing API.
403
+ * @param {RegisterAPITarget} [target] - the MFE target being registered; its id scopes stale-correlation
404
+ * detection per-MFE rather than per-script-URL (see isCorrelationStale below)
233
405
  * @returns {RegisterAPITimings} Object containing script fetch start and end times, and the asset URL if found
234
406
  */
235
407
  function findScriptTimings(target) {
@@ -242,7 +414,9 @@ function findScriptTimings(target) {
242
414
  scriptStart: 0,
243
415
  scriptEnd: 0,
244
416
  asset: undefined,
245
- type: 'unknown'
417
+ type: 'unknown',
418
+ totalWeight: 0,
419
+ renderBlocking: undefined
246
420
  };
247
421
  const stack = getDeepStackTrace();
248
422
  if (!stack) return timings;
@@ -279,9 +453,8 @@ function findScriptTimings(target) {
279
453
  }
280
454
 
281
455
  // A correlation can be reused across multiple `register()` calls for the same script URL (e.g. an SPA
282
- // remounting the same MFE without the script actually reloading). When that happens, its dom/performance
283
- // timings still describe the *original* load, not this one. Detect that case so scriptStart/scriptEnd
284
- // below can ignore the stale data instead of reporting it as if it were fresh.
456
+ // remounting the same MFE without the script reloading) -- its dom/performance timings would then describe
457
+ // the *original* load. Detect that so scriptStart/scriptEnd below can ignore the stale data.
285
458
  const correlation = timings.correlation;
286
459
  const alreadyClaimedByThisMFE = !!mfeId && !!correlation?.claimedBy.has(mfeId);
287
460
  if (correlation && mfeId) correlation.claimedBy.add(mfeId);
@@ -292,10 +465,35 @@ function findScriptTimings(target) {
292
465
  return staleness > _scriptTrackerConstants.CORRELATION_STALE_THRESHOLD_MS;
293
466
  };
294
467
 
295
- // Use getters here because the correlation data may arrive after this function returns the timing object, and we want to provide the most up-to-date timing information possible when the getters are accessed at harvest time.
296
- // Non-stale: fall back to fetchEnd if correlation data isn't available yet (our best approximation for script execution start). Stale: fall back straight to registeredAt — fetchEnd would be derived from the same stale correlation, so it can't be trusted either.
297
- Object.defineProperty(timings, 'scriptStart', (0, _timingFactory.timingFactory)(() => isCorrelationStale() ? timings.registeredAt : correlation?.script.start ?? timings.fetchEnd));
298
- Object.defineProperty(timings, 'scriptEnd', (0, _timingFactory.timingFactory)(() => isCorrelationStale() ? timings.registeredAt : correlation?.script.end ?? timings.registeredAt));
468
+ // Only reached for a real (non-inline), stack-attributable script -- scriptStart/scriptEnd become live getters
469
+ // below, so manifest widening needs a hook that composes with them instead of overriding them (see
470
+ // recordManifestScriptWindow's doc comment). Every other path keeps getOrCreateInternals' plain-value-widening
471
+ // default, which is already correct there.
472
+ let manifestScriptStart = 0;
473
+ let manifestScriptEnd = 0;
474
+ /**
475
+ * Widens the running manifestScriptStart/manifestScriptEnd accumulators with one asset's correlation timing.
476
+ * Never shrinks either bound; a falsy (unresolved) start/end is ignored.
477
+ * @type {RecordManifestScriptWindowFn}
478
+ */
479
+ getOrCreateInternals(timings).recordManifestScriptWindow = (start, end) => {
480
+ if (start) manifestScriptStart = manifestScriptStart > 0 ? Math.min(manifestScriptStart, start) : start;
481
+ if (end) manifestScriptEnd = manifestScriptEnd > 0 ? Math.max(manifestScriptEnd, end) : end;
482
+ };
483
+
484
+ // Getters, since correlation data may still arrive after this function returns -- we want the freshest value
485
+ // at harvest time. Non-stale: fall back to fetchEnd (best approximation) if correlation isn't available yet.
486
+ // Stale: fall back to registeredAt, since fetchEnd would derive from the same stale correlation. Manifest
487
+ // widening is re-read on every access rather than baked in once, so it composes with a correlation that
488
+ // resolves later.
489
+ Object.defineProperty(timings, 'scriptStart', (0, _timingFactory.timingFactory)(() => {
490
+ const ownStart = isCorrelationStale() ? timings.registeredAt : correlation?.script.start ?? timings.fetchEnd;
491
+ return manifestScriptStart > 0 ? Math.min(ownStart, manifestScriptStart) : ownStart;
492
+ }));
493
+ Object.defineProperty(timings, 'scriptEnd', (0, _timingFactory.timingFactory)(() => {
494
+ const ownEnd = isCorrelationStale() ? timings.registeredAt : correlation?.script.end ?? timings.registeredAt;
495
+ return manifestScriptEnd > 0 ? Math.max(ownEnd, manifestScriptEnd) : ownEnd;
496
+ }));
299
497
  } catch (error) {
300
498
  // Don't let stack parsing errors break anything
301
499
  }
@@ -9,11 +9,14 @@ exports.findTargetsFromStackTrace = findTargetsFromStackTrace;
9
9
  exports.getRegisteredEntityByIframeInterfaceId = getRegisteredEntityByIframeInterfaceId;
10
10
  exports.getRegisteredTargetsFromFilename = getRegisteredTargetsFromFilename;
11
11
  exports.getRegisteredTargetsFromId = getRegisteredTargetsFromId;
12
+ exports.getRegisteredTargetsFromResourceUrl = getRegisteredTargetsFromResourceUrl;
12
13
  exports.getVersion2Attributes = getVersion2Attributes;
13
14
  exports.getVersion2DuplicationAttributes = getVersion2DuplicationAttributes;
14
15
  exports.isMfeTarget = isMfeTarget;
15
16
  exports.shouldDuplicate = shouldDuplicate;
16
17
  var _scriptTracker = require("./script-tracker");
18
+ var _manifest = require("./manifest");
19
+ var _cleanUrl = require("../url/clean-url");
17
20
  var _constants = require("./constants");
18
21
  /**
19
22
  * Copyright 2020-2026 New Relic, Inc. All rights reserved.
@@ -61,6 +64,38 @@ function getRegisteredTargetsFromId(id, agentRef) {
61
64
  return registeredEntities?.filter(entity => String(entity.metadata.target.id) === String(id)).map(entity => entity.metadata.target) || [];
62
65
  }
63
66
 
67
+ /**
68
+ * Returns the registered target(s) whose resource matches a given resource URL -- used to attribute `BrowserPerformance`
69
+ * (PerformanceResourceTiming) events, which never have a JS call stack to walk (they're fired for declarative
70
+ * `<script src>`/`<link>`/`<img>` tags, not JS execution), so stack-trace attribution (see {@link findTargetsFromStackTrace})
71
+ * doesn't apply. Unlike {@link getRegisteredTargetsFromFilename}, this matches manifest assets of ANY type (scripts,
72
+ * images, fonts, css, etc.) -- non-script assets can't produce a JS stack frame to match against, but they can and do
73
+ * produce their own resource timing entries. Returns an empty array if no target is found.
74
+ * @param {string} url - the resource's URL, as reported by the Performance API
75
+ * @param {*} agentRef
76
+ * @returns {import("../../interfaces/registered-entity").RegisterAPIMetadataTarget[]}
77
+ */
78
+ function getRegisteredTargetsFromResourceUrl(url, agentRef) {
79
+ if (!isValid(url, agentRef)) return [];
80
+ const registeredEntities = agentRef.runtime.registeredEntities;
81
+ const cleanedUrl = (0, _cleanUrl.cleanURL)(url);
82
+ const matches = registeredEntities?.filter(entity => {
83
+ const manifest = entity.metadata.target?.manifest;
84
+ if (manifest) {
85
+ // A manifest was supplied -- it is the sole source of truth for attribution. The caller-script fallback
86
+ // below never applies here, so e.g. a registrar script that calls register() on behalf of many MFEs (each
87
+ // with its own manifest naming only that MFE's own files) never has ITS OWN activity attributed just because
88
+ // it happened to be the one that called register().
89
+ return (0, _manifest.matchManifestAsset)(manifest, url, {
90
+ scriptsOnly: false
91
+ });
92
+ }
93
+ // No manifest -- fall back to matching the resolved URL of whatever script called register().
94
+ return !!entity.metadata.timings?.asset && (0, _cleanUrl.cleanURL)(entity.metadata.timings.asset) === cleanedUrl;
95
+ });
96
+ return dedupeRegisteredEntitiesByAsset(matches).map(entity => entity.metadata.target);
97
+ }
98
+
64
99
  /**
65
100
  * Returns the registered target(s) associated with a given filename if found in the resource timing API during registration. Returns an empty array if no target is found.
66
101
  * Multiple registrations that resolve to the same underlying script asset AND represent the same logical MFE (i.e.
@@ -77,7 +112,18 @@ function getRegisteredTargetsFromId(id, agentRef) {
77
112
  function getRegisteredTargetsFromFilename(filename, agentRef) {
78
113
  if (!isValid(filename, agentRef)) return [];
79
114
  const registeredEntities = agentRef.runtime.registeredEntities;
80
- const matches = registeredEntities?.filter(entity => entity.metadata.timings?.asset?.endsWith(filename));
115
+ const matches = registeredEntities?.filter(entity => {
116
+ const manifest = entity.metadata.target?.manifest;
117
+ if (manifest) {
118
+ // See the identical case in getRegisteredTargetsFromResourceUrl above: once a manifest exists, it is the
119
+ // sole source of truth for attribution -- the caller-script fallback below never applies.
120
+ return (0, _manifest.matchManifestAsset)(manifest, filename, {
121
+ scriptsOnly: true
122
+ });
123
+ }
124
+ // No manifest -- fall back to matching the resolved URL of whatever script called register().
125
+ return !!entity.metadata.timings?.asset?.endsWith(filename);
126
+ });
81
127
  return dedupeRegisteredEntitiesByAsset(matches).map(entity => entity.metadata.target);
82
128
  }
83
129
 
@@ -18,6 +18,7 @@ var _iframe = require("../../../common/dom/iframe");
18
18
  var _typeCheck = require("../../../common/util/type-check");
19
19
  var _events = require("../../../common/constants/events");
20
20
  var _utils = require("../../../common/v2/utils");
21
+ var _scriptTracker = require("../../../common/v2/script-tracker");
21
22
  /**
22
23
  * Copyright 2020-2026 New Relic, Inc. All rights reserved.
23
24
  * SPDX-License-Identifier: Apache-2.0
@@ -239,7 +240,33 @@ class Aggregate extends _aggregateBase.AggregateBase {
239
240
  entryDuration: duration,
240
241
  firstParty
241
242
  };
242
- this.addEvent(event);
243
+ const targets = (0, _utils.getRegisteredTargetsFromResourceUrl)(name, this.agentRef);
244
+ if (targets.length) {
245
+ targets.forEach(target => this.addEvent({
246
+ ...event
247
+ }, target));
248
+ return;
249
+ }
250
+ this.addEvent({
251
+ ...event
252
+ }, undefined);
253
+
254
+ // This resource entry can resolve before a self-registering script has actually executed and called
255
+ // register() on itself (responseEnd fires before script parse/execution). If this URL belongs to a
256
+ // <script> we're tracking that hasn't finished loading yet, retry resolution once it has -- reporting
257
+ // a second (deliberately duplicate) copy under the real target if one shows up by then. One-shot,
258
+ // self-cleans via `once: true`, so there's nothing to leak if the script never registers.
259
+ const correlation = (0, _scriptTracker.findCorrelation)((0, _cleanUrl.cleanURL)(name));
260
+ if (correlation?.dom.value && !correlation.dom.end) {
261
+ const retryAttribution = () => {
262
+ (0, _utils.getRegisteredTargetsFromResourceUrl)(name, this.agentRef).forEach(target => this.addEvent({
263
+ ...event
264
+ }, target));
265
+ };
266
+ ['load', 'error'].forEach(evtType => correlation.dom.value.addEventListener(evtType, retryAttribution, {
267
+ once: true
268
+ }));
269
+ }
243
270
  } catch (err) {
244
271
  this.ee.emit('internal-error', [err, 'GenericEvents-Resource']);
245
272
  }