@newrelic/browser-agent 1.319.0 → 1.320.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cjs/common/config/init.js +17 -2
  3. package/dist/cjs/common/config/runtime.js +2 -1
  4. package/dist/cjs/common/constants/env.cdn.js +1 -1
  5. package/dist/cjs/common/constants/env.npm.js +1 -1
  6. package/dist/cjs/common/constants/iframe-constants.js +16 -0
  7. package/dist/cjs/common/url/add-url.js +23 -0
  8. package/dist/cjs/common/util/console.js +40 -33
  9. package/dist/cjs/common/v2/mfe-vitals.js +9 -2
  10. package/dist/cjs/common/v2/script-correlation.js +2 -0
  11. package/dist/cjs/common/v2/script-tracker-constants.js +13 -0
  12. package/dist/cjs/common/v2/script-tracker.js +54 -29
  13. package/dist/cjs/common/v2/timing-factory.js +18 -0
  14. package/dist/cjs/common/v2/utils.js +100 -6
  15. package/dist/cjs/features/ajax/instrument/index.js +4 -14
  16. package/dist/cjs/features/utils/instrument-base.js +13 -0
  17. package/dist/cjs/interfaces/registered-iframe-entity.js +527 -0
  18. package/dist/cjs/loaders/api/register.js +10 -6
  19. package/dist/cjs/loaders/configure/configure.js +14 -0
  20. package/dist/cjs/loaders/configure/iframe-message-handler.js +270 -0
  21. package/dist/esm/common/config/init.js +17 -2
  22. package/dist/esm/common/config/runtime.js +2 -1
  23. package/dist/esm/common/constants/env.cdn.js +1 -1
  24. package/dist/esm/common/constants/env.npm.js +1 -1
  25. package/dist/esm/common/constants/iframe-constants.js +10 -0
  26. package/dist/esm/common/url/add-url.js +16 -0
  27. package/dist/esm/common/util/console.js +40 -33
  28. package/dist/esm/common/v2/mfe-vitals.js +9 -2
  29. package/dist/esm/common/v2/script-correlation.js +2 -0
  30. package/dist/esm/common/v2/script-tracker-constants.js +7 -0
  31. package/dist/esm/common/v2/script-tracker.js +54 -29
  32. package/dist/esm/common/v2/timing-factory.js +12 -0
  33. package/dist/esm/common/v2/utils.js +97 -6
  34. package/dist/esm/features/ajax/instrument/index.js +1 -11
  35. package/dist/esm/features/utils/instrument-base.js +13 -0
  36. package/dist/esm/interfaces/registered-iframe-entity.js +518 -0
  37. package/dist/esm/loaders/api/register.js +10 -6
  38. package/dist/esm/loaders/configure/configure.js +15 -1
  39. package/dist/esm/loaders/configure/iframe-message-handler.js +263 -0
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/dist/types/common/config/init.d.ts.map +1 -1
  42. package/dist/types/common/config/runtime.d.ts.map +1 -1
  43. package/dist/types/common/constants/iframe-constants.d.ts +11 -0
  44. package/dist/types/common/constants/iframe-constants.d.ts.map +1 -0
  45. package/dist/types/common/url/add-url.d.ts +2 -0
  46. package/dist/types/common/url/add-url.d.ts.map +1 -0
  47. package/dist/types/common/util/console.d.ts +40 -33
  48. package/dist/types/common/util/console.d.ts.map +1 -1
  49. package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -1
  50. package/dist/types/common/v2/script-correlation.d.ts +2 -0
  51. package/dist/types/common/v2/script-correlation.d.ts.map +1 -1
  52. package/dist/types/common/v2/script-tracker-constants.d.ts +7 -0
  53. package/dist/types/common/v2/script-tracker-constants.d.ts.map +1 -0
  54. package/dist/types/common/v2/script-tracker.d.ts +4 -1
  55. package/dist/types/common/v2/script-tracker.d.ts.map +1 -1
  56. package/dist/types/common/v2/timing-factory.d.ts +9 -0
  57. package/dist/types/common/v2/timing-factory.d.ts.map +1 -0
  58. package/dist/types/common/v2/utils.d.ts +37 -0
  59. package/dist/types/common/v2/utils.d.ts.map +1 -1
  60. package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
  61. package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
  62. package/dist/types/interfaces/registered-iframe-entity.d.ts +116 -0
  63. package/dist/types/interfaces/registered-iframe-entity.d.ts.map +1 -0
  64. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  65. package/dist/types/loaders/configure/iframe-message-handler.d.ts +17 -0
  66. package/dist/types/loaders/configure/iframe-message-handler.d.ts.map +1 -0
  67. package/package.json +9 -1
  68. package/src/common/config/init.js +13 -2
  69. package/src/common/config/runtime.js +2 -1
  70. package/src/common/constants/iframe-constants.js +11 -0
  71. package/src/common/url/add-url.js +18 -0
  72. package/src/common/util/console.js +40 -33
  73. package/src/common/v2/mfe-vitals.js +7 -4
  74. package/src/common/v2/script-correlation.js +2 -0
  75. package/src/common/v2/script-tracker-constants.js +7 -0
  76. package/src/common/v2/script-tracker.js +65 -26
  77. package/src/common/v2/timing-factory.js +10 -0
  78. package/src/common/v2/utils.js +102 -6
  79. package/src/features/ajax/instrument/index.js +1 -13
  80. package/src/features/utils/instrument-base.js +12 -0
  81. package/src/interfaces/registered-iframe-entity.js +482 -0
  82. package/src/loaders/api/register.js +9 -5
  83. package/src/loaders/configure/configure.js +15 -1
  84. package/src/loaders/configure/iframe-message-handler.js +244 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,19 @@
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.320.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.319.0...v1.320.0) (2026-08-07)
7
+
8
+
9
+ ### Features
10
+
11
+ * Improve ability to apply reasonable caps to MFE vitals ([#1823](https://github.com/newrelic/newrelic-browser-agent/issues/1823)) ([df72604](https://github.com/newrelic/newrelic-browser-agent/commit/df7260404dee51cf3706311235ab9b0d785f8789))
12
+ * MFE iframe support - Beta Access ([#1752](https://github.com/newrelic/newrelic-browser-agent/issues/1752)) ([46e36f7](https://github.com/newrelic/newrelic-browser-agent/commit/46e36f77abbad70155f20755cd4554799125e334))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * Deduplicate auto-instrumented MFE events ([#1820](https://github.com/newrelic/newrelic-browser-agent/issues/1820)) ([ba60ac6](https://github.com/newrelic/newrelic-browser-agent/commit/ba60ac6550d93feb466af845a8f3f3fdb03b4970))
18
+
6
19
  ## [1.319.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.318.0...v1.319.0) (2026-07-28)
7
20
 
8
21
 
@@ -29,7 +29,9 @@ const InitModelFn = () => {
29
29
  feature_flags: [],
30
30
  experimental: {
31
31
  register: false,
32
- resources: false
32
+ resources: false,
33
+ iframe_bridge: false,
34
+ iframe_domains: []
33
35
  },
34
36
  mask_selector: '*',
35
37
  block_selector: '[data-nr-block]',
@@ -69,7 +71,20 @@ const InitModelFn = () => {
69
71
  set enabled(val) {
70
72
  hiddenState.experimental.register = val;
71
73
  },
72
- duplicate_data_to_container: false
74
+ duplicate_data_to_container: false,
75
+ // experimental iframe bridge feature
76
+ get allow_iframe_bridge() {
77
+ return hiddenState.feature_flags.includes(_constants.FEATURE_FLAGS.IFRAME_BRIDGE) || hiddenState.experimental.iframe_bridge;
78
+ },
79
+ set allow_iframe_bridge(val) {
80
+ hiddenState.experimental.iframe_bridge = val;
81
+ },
82
+ get iframe_domains() {
83
+ return hiddenState.experimental.iframe_domains;
84
+ },
85
+ set iframe_domains(val) {
86
+ if (Array.isArray(val)) hiddenState.experimental.iframe_domains = val;else (0, _console.warn)(1, val);
87
+ }
73
88
  }
74
89
  },
75
90
  browser_consent_mode: {
@@ -63,7 +63,8 @@ const RuntimeModel = {
63
63
  },
64
64
  get harvestCount() {
65
65
  return ++_harvestCount;
66
- }
66
+ },
67
+ listeningForIframeMessages: false
67
68
  };
68
69
  const mergeRuntime = runtime => {
69
70
  const modeledObject = (0, _configurable.getModeledObject)(runtime, RuntimeModel);
@@ -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.319.0";
20
+ const VERSION = exports.VERSION = "1.320.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.319.0";
20
+ const VERSION = exports.VERSION = "1.320.0";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.prefix = exports.IFRAME_VITALS_UPDATE = exports.IFRAME_TIMING_UPDATE = exports.IFRAME_API_RESPONSE = exports.IFRAME_API = exports.IFRAME_AJAX = void 0;
7
+ /**
8
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
9
+ * SPDX-License-Identifier: Apache-2.0
10
+ */
11
+ const prefix = exports.prefix = 'newrelic-iframe-';
12
+ const IFRAME_TIMING_UPDATE = exports.IFRAME_TIMING_UPDATE = prefix + 'timing-update';
13
+ const IFRAME_API = exports.IFRAME_API = prefix + 'api';
14
+ const IFRAME_API_RESPONSE = exports.IFRAME_API_RESPONSE = prefix + 'api-response';
15
+ const IFRAME_VITALS_UPDATE = exports.IFRAME_VITALS_UPDATE = prefix + 'vitals-update';
16
+ const IFRAME_AJAX = exports.IFRAME_AJAX = prefix + 'ajax';
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.addUrl = addUrl;
7
+ var _parseUrl = require("./parse-url.js");
8
+ /**
9
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
10
+ * SPDX-License-Identifier: Apache-2.0
11
+ */
12
+
13
+ function addUrl(ctx, url) {
14
+ var parsed = (0, _parseUrl.parseUrl)(url);
15
+ var params = ctx.params || ctx;
16
+ params.hostname = parsed.hostname;
17
+ params.port = parsed.port;
18
+ params.protocol = parsed.protocol;
19
+ params.host = parsed.hostname + ':' + parsed.port;
20
+ params.pathname = parsed.pathname;
21
+ ctx.parsedOrigin = parsed;
22
+ ctx.sameOrigin = parsed.sameOrigin;
23
+ }
@@ -21,39 +21,39 @@ var _globalEvent = require("../dispatch/global-event");
21
21
  *
22
22
  * | Code | Message |
23
23
  * |------|---------|
24
- * | 1 | An error occurred while setting a property of a Configurable |
25
- * | 2 | An error occurred while setting a Configurable |
26
- * | 3 | Setting a Configurable requires an object as input |
27
- * | 4 | Setting a Configurable requires a model to set its initial properties |
24
+ * | 1 | An error occurred while setting a property of a Configurable. |
25
+ * | 2 | An error occurred while setting a Configurable. |
26
+ * | 3 | Setting a Configurable requires an object as input. |
27
+ * | 4 | Setting a Configurable requires a model to set its initial properties. |
28
28
  * | 5 | An invalid session_replay.mask_selector was provided. \* will be used. |
29
- * | 6 | An invalid session_replay.block_selector was provided and will not be used |
30
- * | 7 | An invalid session_replay.mask_input_option was provided and will not be used |
31
- * | 8 | Shared context requires an object as input |
32
- * | 9 | An error occurred while setting SharedContext |
33
- * | 10 | Failed to read from storage API |
34
- * | 11 | Failed to write to the storage API |
29
+ * | 6 | An invalid session_replay.block_selector was provided and will not be used. |
30
+ * | 7 | An invalid session_replay.mask_input_option was provided and will not be used. |
31
+ * | 8 | Shared context requires an object as input. |
32
+ * | 9 | An error occurred while setting SharedContext. |
33
+ * | 10 | Failed to read from storage API. |
34
+ * | 11 | Failed to write to the storage API. |
35
35
  * | 12 | An obfuscation replacement rule was detected missing a "regex" value. |
36
- * | 13 | An obfuscation replacement rule contains a "regex" value with an invalid type (must be a string or RegExp) |
37
- * | 14 | An obfuscation replacement rule contains a "replacement" value with an invalid type (must be a string) |
38
- * | 15 | An error occurred while intercepting XHR |
39
- * | 16 | Could not cast log message to string |
36
+ * | 13 | An obfuscation replacement rule contains a "regex" value with an invalid type (must be a string or RegExp). |
37
+ * | 14 | An obfuscation replacement rule contains a "replacement" value with an invalid type (must be a string). |
38
+ * | 15 | An error occurred while intercepting XHR. |
39
+ * | 16 | Could not cast log message to string. |
40
40
  * | 17 | Could not calculate New Relic server time. Agent shutting down. |
41
41
  * | 18 | RUM call failed. Agent shutting down. |
42
42
  * | 19 | SPA scheduler is not initialized. Saved interaction is not sent! |
43
43
  * | 20 | A problem occurred when starting up session manager. This page will not start or extend any session. |
44
44
  * | 21 | Failed to initialize the agent. Could not determine the runtime environment. |
45
45
  * | 22 | Failed to initialize all enabled instrument classes (agent aborted) - |
46
- * | 23 | An unexpected issue occurred |
46
+ * | 23 | An unexpected issue occurred. |
47
47
  * | 24 | Something prevented the agent from instrumenting. |
48
48
  * | 25 | Something prevented the agent from being downloaded. |
49
49
  * | 26 | Failed to initialize instrument classes. |
50
50
  * | 27 | Downloading runtime APIs failed... |
51
51
  * | 28 | The Browser Agent is attempting to send a very large payload. This is usually tied to large amounts of custom attributes. Please check your configurations. |
52
- * | 29 | Failed to wrap logger: invalid argument(s) |
53
- * | 30 | Invalid log level |
54
- * | 31 | Ignored log: Log is larger than maximum payload size |
55
- * | 32 | Ignored log: Invalid message |
56
- * | 33 | Session Replay Aborted |
52
+ * | 29 | Failed to wrap logger: invalid argument(s). |
53
+ * | 30 | Invalid log level. |
54
+ * | 31 | Ignored log: Log is larger than maximum payload size. |
55
+ * | 32 | Ignored log: Invalid message. |
56
+ * | 33 | Session Replay Aborted. |
57
57
  * | 34 | Downloading and initializing a feature failed... |
58
58
  * | 35 | Call to agent api failed. The API is not currently initialized. |
59
59
  * | 36 | A feature is enabled but one or more dependent features have not been initialized. This may cause unintended consequences or missing data... |
@@ -62,7 +62,7 @@ var _globalEvent = require("../dispatch/global-event");
62
62
  * | 39 | Failed to execute setCustomAttribute. Name must be a string type. |
63
63
  * | 40 | Failed to execute setCustomAttribute. Non-null value must be a string, number or boolean type. |
64
64
  * | 41 | Failed to execute setUserId. Non-null value must be a string type. |
65
- * | 42 | Failed to execute setApplicationVersion. Expected <String \| null> |
65
+ * | 42 | Failed to execute setApplicationVersion. Expected <String \| null>. |
66
66
  * | 43 | Agent not configured properly. |
67
67
  * | 44 | Invalid object passed to generic event aggregate. Missing "eventType". |
68
68
  * | 45 | An internal agent process failed to execute. |
@@ -71,27 +71,34 @@ var _globalEvent = require("../dispatch/global-event");
71
71
  * | 48 | Supplied an invalid API target. Must be an <Object> that contains valid (string) id and name properties. |
72
72
  * | 49 | Supplied API target is missing an entityGuid. Some APIs may not behave correctly without a valid entityGuid (ex. logs). |
73
73
  * | 50 | Failed to connect. Cannot allow registered API. |
74
- * | 51 | Container agent is not available to register with. Can not connect |
74
+ * | 51 | Container agent is not available to register with. Can not connect. |
75
75
  * | 52 | Unexpected problem encountered. There should be at least one app for harvest! |
76
- * | 53 | Did not receive a valid entityGuid from connection response |
77
- * | 54 | An experimental feature is being used. Support can not be offered for issues |
78
- * | 55 | Register API has been disabled on the container agent |
79
- * | 56 | Could not find a matching entity to store data |
76
+ * | 53 | Did not receive a valid entityGuid from connection response. |
77
+ * | 54 | An experimental feature is being used. Support can not be offered for issues. |
78
+ * | 55 | Register API has been disabled on the container agent. |
79
+ * | 56 | Could not find a matching entity to store data. |
80
80
  * | 57 | Failed to execute measure. Arguments must have valid types. |
81
81
  * | 58 | Failed to execute measure. Resulting duration must be non-negative. |
82
- * | 59 | Session replay harvested before a session trace payload could be sent. This could be problematic for replays that rely on a trace |
83
- * | 60 | Session trace aborted |
82
+ * | 59 | Session replay harvested before a session trace payload could be sent. This could be problematic for replays that rely on a trace. |
83
+ * | 60 | Session trace aborted. |
84
84
  * | 61 | Timestamps must be non-negative and end time cannot be before start time. |
85
- * | 62 | Timestamp must be a unix timestamp greater than the page origin time |
86
- * | 63 | A single event was larger than the maximum allowed payload size |
85
+ * | 62 | Timestamp must be a unix timestamp greater than the page origin time. |
86
+ * | 63 | A single event was larger than the maximum allowed payload size. |
87
87
  * | 64 | Required globals have been mutated before being accessed by the browser agent. This can cause issues and should be avoided. |
88
- * | 65 | Consent API argument must be boolean or undefined |
89
- * | 66 | A new agent session has started |
88
+ * | 65 | Consent API argument must be boolean or undefined. |
89
+ * | 66 | A new agent session has started. |
90
90
  * | 67 | The "spa" feature has been deprecated and disabled. Please use/import "soft_navigations" instead for tracking of BrowserInteraction data. |
91
91
  * | 68 | API has been deregistered and can no longer be used. Call "register" API again with credentials to start over. |
92
- * | 69 | More than one Browser agent is running on the page |
92
+ * | 69 | More than one Browser agent is running on the page. |
93
93
  * | 70 | A session replay payload failed to send and is being retried. Recording is paused during the retry period, and will resume when a successful harvest is made. Some replay activity may be missed during retry phases. |
94
94
  * | 71 | An invalid feature mode was detected and set to "off". |
95
+ * | 72 | RegisteredIframeEntity failed to transmit API data from an iframe to window context. |
96
+ * | 73 | RegisteredIframeEntity failed to register with window context. |
97
+ * | 74 | RegisteredIframeEntity rejected message from unauthorized origin. |
98
+ * | 75 | RegisteredIframeEntity rejected message with mismatched iframeInterfaceId. |
99
+ * | 76 | Agent rejected post message, could not match with existing entity. |
100
+ * | 77 | Agent rejected post message, could not validate origin. |
101
+ * | 78 | RegisteredIframeEntity could not determine parent origin and will not register, to avoid trusting messages from any origin. |
95
102
  *
96
103
  * @param {number} code The warning code to emit, which will be used to link to the warning code documentation
97
104
  * @param {*} [secondary] Secondary data to include, usually an extra message, error or object
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.trackMFEVitals = trackMFEVitals;
7
7
  var _runtime = require("../constants/runtime");
8
8
  var _now = require("../timing/now");
9
+ var _scriptTrackerConstants = require("./script-tracker-constants");
9
10
  /**
10
11
  * Copyright 2020-2026 New Relic, Inc. All rights reserved.
11
12
  * SPDX-License-Identifier: Apache-2.0
@@ -139,11 +140,17 @@ function trackMFEVitals(target, timings) {
139
140
  fcp: {
140
141
  get value() {
141
142
  return getTimeRelativeToScriptStart(fcpObservedAt);
143
+ },
144
+ set value(v) {
145
+ fcpObservedAt = v;
142
146
  }
143
147
  },
144
148
  lcp: {
145
149
  get value() {
146
150
  return getTimeRelativeToScriptStart(lcpObservedAt);
151
+ },
152
+ set value(v) {
153
+ lcpObservedAt = v;
147
154
  }
148
155
  },
149
156
  cls: {
@@ -157,11 +164,11 @@ function trackMFEVitals(target, timings) {
157
164
  if (!target || !_runtime.isBrowserScope || !_runtime.globalScope.MutationObserver || !_runtime.globalScope.PerformanceObserver) return vitals;
158
165
  const observers = [];
159
166
 
160
- // If FCP hasn't been observed within 10 seconds, give up and shut down all observers.
167
+ // If FCP hasn't been observed within this window, give up and shut down all observers.
161
168
  // Once FCP is observed, the other vitals are left to record until their natural lifespan ends.
162
169
  setTimeout(() => {
163
170
  if (!fcpObservedAt) vitals.disconnect();
164
- }, 10000);
171
+ }, _scriptTrackerConstants.CORRELATION_STALE_THRESHOLD_MS);
165
172
  const populateVitalMinimums = () => {
166
173
  fcpObservedAt ??= (0, _now.now)();
167
174
  lcpObservedAt ??= (0, _now.now)();
@@ -24,6 +24,8 @@ class ScriptCorrelation {
24
24
  constructor(url) {
25
25
  /** @type {string} The cleaned URL of the script */
26
26
  this.url = url;
27
+ /** @type {Set<string>} MFE target ids that have already claimed this correlation via a completed `findScriptTimings()` call. Keyed per target rather than a single flag so one script that registers multiple distinct MFEs (each with its own id) doesn't have the second MFE's registration mistaken for a stale reuse of the first's. */
28
+ this.claimedBy = new Set();
27
29
  }
28
30
 
29
31
  /**
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.CORRELATION_STALE_THRESHOLD_MS = void 0;
7
+ /**
8
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
9
+ * SPDX-License-Identifier: Apache-2.0
10
+ */
11
+
12
+ /** @type {number} - Correlations are keyed only by script URL and never cleared per-registration, so a script registered more than once in a session (SPA remount, shared bundle, etc.) can reuse a correlation from a much earlier load. If the correlation's recorded start predates `registeredAt` by more than this, treat it as stale and fall back to `registeredAt`. */
13
+ const CORRELATION_STALE_THRESHOLD_MS = exports.CORRELATION_STALE_THRESHOLD_MS = 10000;
@@ -12,6 +12,8 @@ var _now = require("../timing/now");
12
12
  var _cleanUrl = require("../url/clean-url");
13
13
  var _browserStackMatchers = require("../util/browser-stack-matchers");
14
14
  var _scriptCorrelation = require("./script-correlation");
15
+ var _scriptTrackerConstants = require("./script-tracker-constants");
16
+ var _timingFactory = require("./timing-factory");
15
17
  /**
16
18
  * Copyright 2020-2026 New Relic, Inc. All rights reserved.
17
19
  * SPDX-License-Identifier: Apache-2.0
@@ -19,6 +21,7 @@ var _scriptCorrelation = require("./script-correlation");
19
21
 
20
22
  /**
21
23
  * @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
24
+ * @typedef {import('../../loaders/api/register-api-types').RegisterAPITarget} RegisterAPITarget
22
25
  */
23
26
 
24
27
  /** export for testing purposes */
@@ -30,7 +33,7 @@ try {
30
33
  }
31
34
 
32
35
  /** @type {(entry: PerformanceEntry) => boolean} - A shared function to determine if a performance entry is a valid script or link resource for evaluation */
33
- const validEntryCriteria = entry => entry.initiatorType === 'script' || ['link', 'fetch'].includes(entry.initiatorType) && entry.name.endsWith('.js');
36
+ const validEntryCriteria = entry => entry.initiatorType === 'script' || ['link', 'fetch'].includes(entry.initiatorType) && (0, _cleanUrl.cleanURL)(entry.name).endsWith('.js');
34
37
 
35
38
  /** @type {Map<string, ScriptCorrelation>} - Central registry for script correlations containing both DOM and Performance data */
36
39
  const scriptCorrelations = exports.scriptCorrelations = new Map();
@@ -205,11 +208,32 @@ function applyPerformanceEntry(timings, entry) {
205
208
  timings.type = entry.initiatorType;
206
209
  }
207
210
 
211
+ /**
212
+ * Subscribes to late resource timing emissions for a script URL.
213
+ * @param {RegisterAPITimings} timings - The timings object to update
214
+ * @param {string} mfeScriptUrl - The script URL to match
215
+ */
216
+ function subscribeToLatePerformanceEntry(timings, mfeScriptUrl) {
217
+ if (!_runtime.globalScope.PerformanceObserver?.supportedEntryTypes?.includes('resource')) return;
218
+ poSubscribers.push({
219
+ addedAt: (0, _now.now)(),
220
+ test: entry => {
221
+ if (entryMatchesUrl(entry, mfeScriptUrl)) {
222
+ applyPerformanceEntry(timings, entry);
223
+ return true;
224
+ }
225
+ return false;
226
+ }
227
+ });
228
+ }
229
+
208
230
  /**
209
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.
210
233
  * @returns {RegisterAPITimings} Object containing script fetch start and end times, and the asset URL if found
211
234
  */
212
- function findScriptTimings() {
235
+ function findScriptTimings(target) {
236
+ const mfeId = target?.id;
213
237
  const timings = {
214
238
  registeredAt: (0, _now.now)(),
215
239
  reportedAt: undefined,
@@ -239,38 +263,39 @@ function findScriptTimings() {
239
263
  // Get correlation data
240
264
  timings.correlation = findCorrelation(mfeScriptUrl);
241
265
 
242
- // Use correlation's performance entry if available, otherwise check live performance API
243
- const performanceEntry = timings.correlation?.performance.value || performance.getEntriesByType('resource').find(e => entryMatchesUrl(e, mfeScriptUrl));
266
+ // Use correlation's performance entry if available, otherwise check the live performance API before falling back to the buffered observer.
267
+ const performanceEntry = timings.correlation?.performance.value || _runtime.globalScope.performance?.getEntriesByType('resource')?.find(e => entryMatchesUrl(e, mfeScriptUrl));
244
268
  if (performanceEntry) {
245
269
  applyPerformanceEntry(timings, performanceEntry);
246
- } else if (wasPreloaded(mfeScriptUrl)) {
247
- // Handle preloaded scripts that may report late
248
- timings.asset = mfeScriptUrl;
249
- timings.type = 'preload';
270
+ } else {
271
+ const isPreloaded = wasPreloaded(mfeScriptUrl);
250
272
 
251
- // Subscribe to late performance observer callbacks
252
- poSubscribers.push({
253
- addedAt: (0, _now.now)(),
254
- test: entry => {
255
- if (entryMatchesUrl(entry, mfeScriptUrl)) {
256
- applyPerformanceEntry(timings, entry);
257
- return true;
258
- }
259
- return false;
260
- }
261
- });
273
+ // Handle preloaded scripts and any late resource emissions through the shared buffered observer.
274
+ if (isPreloaded) {
275
+ timings.asset = mfeScriptUrl;
276
+ timings.type = 'preload';
277
+ }
278
+ subscribeToLatePerformanceEntry(timings, mfeScriptUrl);
262
279
  }
263
280
 
264
- /*
265
- * 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.
266
- * The getters will fall back to fetchEnd if correlation data isn't available yet, which is our best approximation for script execution start when actual script timings can not be determined.
267
- */
268
- Object.defineProperty(timings, 'scriptStart', {
269
- get: () => timings.correlation?.script.start || timings.fetchEnd
270
- });
271
- Object.defineProperty(timings, 'scriptEnd', {
272
- get: () => timings.correlation?.script.end || timings.registeredAt
273
- });
281
+ // 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.
285
+ const correlation = timings.correlation;
286
+ const alreadyClaimedByThisMFE = !!mfeId && !!correlation?.claimedBy.has(mfeId);
287
+ if (correlation && mfeId) correlation.claimedBy.add(mfeId);
288
+ const isCorrelationStale = () => {
289
+ const correlationStart = correlation?.script.start;
290
+ if (!alreadyClaimedByThisMFE || !correlationStart) return false;
291
+ const staleness = timings.registeredAt - correlationStart;
292
+ return staleness > _scriptTrackerConstants.CORRELATION_STALE_THRESHOLD_MS;
293
+ };
294
+
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));
274
299
  } catch (error) {
275
300
  // Don't let stack parsing errors break anything
276
301
  }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.timingFactory = timingFactory;
7
+ /**
8
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
9
+ * SPDX-License-Identifier: Apache-2.0
10
+ */
11
+ function timingFactory(getLazyValue = () => {}, override) {
12
+ return {
13
+ get: () => override !== undefined ? override : getLazyValue(),
14
+ set: newValue => {
15
+ override = newValue;
16
+ }
17
+ };
18
+ }
@@ -4,7 +4,10 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.V2_TYPES = void 0;
7
+ exports.dedupeRegisteredEntitiesByAsset = dedupeRegisteredEntitiesByAsset;
8
+ exports.dedupeTargetsByInstance = dedupeTargetsByInstance;
7
9
  exports.findTargetsFromStackTrace = findTargetsFromStackTrace;
10
+ exports.getRegisteredEntityByIframeInterfaceId = getRegisteredEntityByIframeInterfaceId;
8
11
  exports.getRegisteredTargetsFromFilename = getRegisteredTargetsFromFilename;
9
12
  exports.getRegisteredTargetsFromId = getRegisteredTargetsFromId;
10
13
  exports.getVersion2Attributes = getVersion2Attributes;
@@ -27,6 +30,18 @@ const V2_TYPES = exports.V2_TYPES = {
27
30
  BA: 'BA'
28
31
  };
29
32
 
33
+ /**
34
+ * Returns a single registered entity associated with a given iframe interface ID. Returns undefined if no entity is found.
35
+ * @param {string} iframeInterfaceId
36
+ * @param {*} agentRef the agent reference
37
+ * @returns {import("../../loaders/api/register-api-types").RegisterAPI|undefined}
38
+ */
39
+ function getRegisteredEntityByIframeInterfaceId(iframeInterfaceId, agentRef) {
40
+ if (!isValid(iframeInterfaceId, agentRef)) return undefined;
41
+ const registeredEntities = agentRef.runtime.registeredEntities;
42
+ return registeredEntities?.find(entity => entity.metadata.target.iframeInterfaceId === iframeInterfaceId);
43
+ }
44
+
30
45
  /**
31
46
  * Returns the registered target associated with a given ID. Returns an empty array if no target is found.
32
47
  * @param {string|number} id
@@ -34,21 +49,69 @@ const V2_TYPES = exports.V2_TYPES = {
34
49
  * @returns {import("../../interfaces/registered-entity").RegisterAPIMetadataTarget[]}
35
50
  */
36
51
  function getRegisteredTargetsFromId(id, agentRef) {
37
- if (!id || !agentRef?.init.api.register.enabled) return [];
52
+ if (!isValid(id, agentRef)) return [];
38
53
  const registeredEntities = agentRef.runtime.registeredEntities;
39
54
  return registeredEntities?.filter(entity => String(entity.metadata.target.id) === String(id)).map(entity => entity.metadata.target) || [];
40
55
  }
41
56
 
42
57
  /**
43
58
  * 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.
59
+ * Multiple registrations that resolve to the same underlying script asset AND represent the same logical MFE (i.e.
60
+ * share the same customer-supplied `target.id`) are collapsed to a single target via
61
+ * {@link dedupeRegisteredEntitiesByAsset}, since auto-instrumented events (AJAX, JS errors, logs, WebSockets) should
62
+ * only be reported once per real occurrence when the same MFE was registered many times over -- not once per
63
+ * duplicate registration. Distinct MFEs (different `target.id`) that happen to share a script (e.g. two different
64
+ * MFEs both registered from the same inline `<script>` block) are intentionally NOT collapsed, since each is a
65
+ * genuinely different entity that should still receive its own copy of the matched event.
44
66
  * @param {string} filename
45
67
  * @param {*} agentRef
46
68
  * @returns {import("../../interfaces/registered-entity").RegisterAPIMetadataTarget[]}
47
69
  */
48
70
  function getRegisteredTargetsFromFilename(filename, agentRef) {
49
- if (!filename || !agentRef?.init.api.register.enabled) return [];
71
+ if (!isValid(filename, agentRef)) return [];
50
72
  const registeredEntities = agentRef.runtime.registeredEntities;
51
- return registeredEntities?.filter(entity => entity.metadata.timings?.asset?.endsWith(filename)).map(entity => entity.metadata.target) || [];
73
+ const matches = registeredEntities?.filter(entity => entity.metadata.timings?.asset?.endsWith(filename));
74
+ return dedupeRegisteredEntitiesByAsset(matches).map(entity => entity.metadata.target);
75
+ }
76
+
77
+ /**
78
+ * Collapses a list of registered entities down to one canonical entity per unique, defined `metadata.timings.asset` +
79
+ * `target.id` combination. Entities whose asset could not be resolved (undefined -- e.g. inline scripts or scripts
80
+ * not found in the resource timing buffer) are never deduped against each other or against resolved entities, since
81
+ * there is no signal that they represent the same underlying script. Entities that share an asset but have different
82
+ * `target.id`s are never deduped against each other either, since a differing id means the customer registered
83
+ * genuinely distinct MFEs (not duplicate registrations of the same one).
84
+ *
85
+ * Canonical selection per asset+id: prefer an entity whose target has not been deregistered (`target.blocked ===
86
+ * false`) over one that has; otherwise the first-encountered entity wins, for determinism.
87
+ * @param {Array} entities registered entities (each with `metadata.timings.asset` and `metadata.target`)
88
+ * @returns {Array} deduped list of entities, preserving relative order of first occurrence
89
+ */
90
+ function dedupeRegisteredEntitiesByAsset(entities) {
91
+ if (!entities?.length) return entities || [];
92
+ const byKey = new Map(); // `${asset}::${id}` -> canonical entity
93
+ const result = [];
94
+ for (const entity of entities) {
95
+ const asset = entity.metadata?.timings?.asset;
96
+ if (!asset) {
97
+ // can't safely dedupe unresolved-asset entities -- always keep as-is
98
+ result.push(entity);
99
+ continue;
100
+ }
101
+ const key = "".concat(asset, "::").concat(entity.metadata?.target?.id);
102
+ const existing = byKey.get(key);
103
+ if (!existing) {
104
+ byKey.set(key, entity);
105
+ result.push(entity);
106
+ } else if (existing.metadata.target?.blocked && !entity.metadata.target?.blocked) {
107
+ // swap in a non-deregistered target as the canonical one for this asset+id
108
+ const idx = result.indexOf(existing);
109
+ if (idx !== -1) result[idx] = entity;
110
+ byKey.set(key, entity);
111
+ }
112
+ // else: existing canonical entity wins (already non-blocked, or both blocked -- first wins); drop this duplicate
113
+ }
114
+ return result;
52
115
  }
53
116
 
54
117
  /**
@@ -107,7 +170,7 @@ function shouldDuplicate(target, aggregateInstance) {
107
170
  * @returns {Array} An array of targets found from the stack trace. If no targets are found or allowed, returns an array with undefined.
108
171
  */
109
172
  function findTargetsFromStackTrace(agentRef) {
110
- if (!agentRef?.init.api.register.enabled || !agentRef?.runtime?.registeredEntities?.length) return [undefined];
173
+ if (!isValid(true, agentRef) || !agentRef?.runtime?.registeredEntities?.length) return [undefined];
111
174
  const targets = [];
112
175
  try {
113
176
  var urls = (0, _scriptTracker.extractUrlsFromStack)((0, _scriptTracker.getDeepStackTrace)());
@@ -118,8 +181,29 @@ function findTargetsFromStackTrace(agentRef) {
118
181
  } catch (err) {
119
182
  // Silent catch to prevent errors from propagating
120
183
  }
121
- if (!targets.length) targets.push(undefined); // if we can't find any targets from the stack trace, return an array with undefined to signify the container agent is the target
122
- return targets;
184
+ const deduped = dedupeTargetsByInstance(targets);
185
+ if (!deduped.length) deduped.push(undefined); // if we can't find any targets from the stack trace, return an array with undefined to signify the container agent is the target
186
+ return deduped;
187
+ }
188
+
189
+ /**
190
+ * Removes duplicate targets from an array, keyed by `target.instance`. This guards against the same canonical
191
+ * target re-entering the array via multiple matched stack-frame URLs (e.g. a recursive call whose stack contains the
192
+ * same file at multiple depths). Entries with no `instance` (i.e. `undefined`, meaning "the container agent")
193
+ * naturally collapse to a single entry too, which is the desired behavior.
194
+ * @param {Array} targets
195
+ * @returns {Array} deduped list of targets, preserving relative order of first occurrence
196
+ */
197
+ function dedupeTargetsByInstance(targets) {
198
+ const seen = new Set();
199
+ const result = [];
200
+ for (const target of targets) {
201
+ const key = target?.instance;
202
+ if (seen.has(key)) continue;
203
+ seen.add(key);
204
+ result.push(target);
205
+ }
206
+ return result;
123
207
  }
124
208
 
125
209
  /**
@@ -131,4 +215,14 @@ function findTargetsFromStackTrace(agentRef) {
131
215
  */
132
216
  function supportsV2(aggregateInstance) {
133
217
  return aggregateInstance?.harvestEndpointVersion === 2;
218
+ }
219
+
220
+ /**
221
+ * Determines if the given identifier and agent reference are valid for use for entity lookups and other operations in the utils methods. This is a common check that is used across multiple methods in this module to ensure that the necessary data is present and that the register API is enabled before attempting to perform operations that depend on those things.
222
+ * @param {*} identifier The identifier to check.
223
+ * @param {*} agentRef The agent reference to check.
224
+ * @returns {boolean} Returns true if the identifier and agent reference are valid, false otherwise.
225
+ */
226
+ function isValid(identifier, agentRef) {
227
+ return !!identifier && !!agentRef?.init.api.register.enabled;
134
228
  }