@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
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.handleMethodCall = handleMethodCall;
7
+ exports.setupIframeMFEMessageListener = setupIframeMFEMessageListener;
8
+ var _console = require("../../common/util/console");
9
+ var _utils = require("../../common/v2/utils");
10
+ var _iframeConstants = require("../../common/constants/iframe-constants");
11
+ var _constants = require("../api/constants");
12
+ var _handle = require("../../common/event-emitter/handle");
13
+ var _features = require("../features/features");
14
+ var _stringify = require("../../common/util/stringify");
15
+ var _registerHandler = require("../../common/event-emitter/register-handler");
16
+ var _drain = require("../../common/drain/drain");
17
+ var _runtime = require("../../common/constants/runtime");
18
+ /**
19
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
20
+ * SPDX-License-Identifier: Apache-2.0
21
+ */
22
+
23
+ /**
24
+ * Retrieves the registered entity associated with the iframeInterfaceId from the event data, and validates that the origin of the message event matches the expected origin for that entity.
25
+ * @param {MessageEvent} event - The message event containing the method call or registration request
26
+ * @param {Object} agent - The browser agent instance
27
+ * @returns {Object|undefined} - The registered entity if it exists and the origin is valid, otherwise undefined
28
+ */
29
+ function getValidEntity(event, agent) {
30
+ try {
31
+ const {
32
+ iframeInterfaceId
33
+ } = event.data;
34
+ if (!iframeInterfaceId) return;
35
+ const entity = (0, _utils.getRegisteredEntityByIframeInterfaceId)(iframeInterfaceId, agent);
36
+ if (!isValidOrigin(event, entity)) return;
37
+ return entity;
38
+ } catch (e) {
39
+ // couldnt get entity or validate origin, just let the method return undefined
40
+ }
41
+ }
42
+ function isSafeProperty(obj, property) {
43
+ return property in obj && typeof property === 'string' && !['__proto__', 'constructor', 'prototype'].includes(property);
44
+ }
45
+
46
+ /**
47
+ * Handles timing property updates from iframes. Mutates the timings object on the entity's metadata with the new value for the specified property.
48
+ * @param {MessageEvent} event - The message event containing the timing update
49
+ * @param {Object} agent - The browser agent instance
50
+ * @returns
51
+ */
52
+ function handleTimingUpdate(event, agent) {
53
+ const entity = getValidEntity(event, agent);
54
+ if (!entity) return;
55
+ if (!entity.metadata.timings) return;
56
+ event.data.entries?.forEach(({
57
+ property,
58
+ value
59
+ }) => {
60
+ if (isSafeProperty(entity.metadata.timings, property)) {
61
+ entity.metadata.timings[property] = value;
62
+ }
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Handles vitals property updates from iframes. Mutates the vitals object on the entity's metadata with the new value for the specified property.
68
+ * @param {MessageEvent} event - The message event containing the vitals update
69
+ * @param {Object} agent - The browser agent instance
70
+ * @returns
71
+ */
72
+ function handleVitalsUpdate(event, agent) {
73
+ const entity = getValidEntity(event, agent);
74
+ if (!entity) return;
75
+ if (!entity.metadata?.vitals) return;
76
+ event.data.entries?.forEach(({
77
+ property,
78
+ value
79
+ }) => {
80
+ if (isSafeProperty(entity.metadata.vitals, property) && (!!Number(value) || value === 0)) {
81
+ entity.metadata.vitals[property].value = value;
82
+ }
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Handles ajax events from iframes and forwards them to the agent's event emitter
88
+ * @param {MessageEvent} event - The message event containing the method call or registration request
89
+ * @param {Object} agent - The browser agent instance
90
+ * @returns
91
+ */
92
+ function handleAjax(event, agent) {
93
+ const entity = getValidEntity(event, agent);
94
+ if (!entity) return;
95
+ const {
96
+ entries
97
+ } = event.data;
98
+ entries?.forEach(({
99
+ params,
100
+ metrics,
101
+ start,
102
+ end,
103
+ initiatorType
104
+ }) => {
105
+ (0, _handle.handle)('xhr', [params, metrics, start, end, initiatorType, entity.metadata.target], undefined, _features.FEATURE_NAMES.ajax, agent.ee);
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Validates that the origin of the message event matches the expected origin for the registered entity
111
+ * @param {MessageEvent} event - The message event containing the method call or registration request
112
+ * @param {Object} entity - The registered entity associated with the iframeInterfaceId from the event data
113
+ * @returns {boolean}
114
+ */
115
+ function isValidOrigin(event, entity) {
116
+ if (!entity) return false;
117
+ try {
118
+ // "null" is the literal serialization the spec uses for every opaque origin (sandboxed iframes
119
+ // without allow-same-origin, data:/javascript: URLs, etc.) -- it does not uniquely identify a
120
+ // single origin, so treating two "null" origins as a match would let an unrelated opaque-origin
121
+ // frame impersonate the one that actually registered.
122
+ if (event.origin === 'null' || entity.metadata.target.iframeOrigin === 'null') return false;
123
+ return event.origin === entity.metadata.target.iframeOrigin;
124
+ } catch (e) {
125
+ (0, _console.warn)(77, e);
126
+ return false;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Handles both entity registration and method calls on existing entities
132
+ * @async
133
+ * @param {MessageEvent} event - The message event containing the method call or registration request
134
+ * @param {Object} agent - The browser agent instance
135
+ * @returns {Promise<{entity: Object|null, result: any}>}
136
+ */
137
+ async function handleMethodCall(event, agent) {
138
+ const {
139
+ target,
140
+ iframeInterfaceId,
141
+ timestamp
142
+ } = event.data;
143
+ // Method calls are always sent one-at-a-time (each has its own postMessage response), so only
144
+ // the first entry is ever populated -- but the payload shape stays consistent with every other
145
+ // iframe message type (timing/vitals/ajax), which all wrap their data in an `entries` array.
146
+ const [{
147
+ method,
148
+ args
149
+ } = {}] = event.data.entries || [];
150
+ const output = {
151
+ entity: null,
152
+ result: null
153
+ };
154
+ // Registration of a new entity needs to be handled differently than method calls on existing entities
155
+ if (method === _constants.REGISTER) {
156
+ // "null" is the opaque-origin serialization (sandboxed iframes, data:/javascript: URLs) -- it
157
+ // doesn't identify a single origin, so an entity registered from one can never be safely
158
+ // distinguished from another later on (see isValidOrigin). Reject up front rather than
159
+ // registering an entity that would silently fail every subsequent call.
160
+ if (event.origin === 'null') {
161
+ (0, _console.warn)(74, event.origin);
162
+ return output;
163
+ }
164
+ const iframeDomains = agent.init.api.register.iframe_domains;
165
+ if (iframeDomains.length && !iframeDomains.includes(event.origin)) {
166
+ (0, _console.warn)(74, event.origin);
167
+ return output;
168
+ }
169
+ const targetData = args?.[0] || target || {};
170
+ if (!agent[_constants.REGISTER]) {
171
+ (0, _console.warn)(35, _constants.REGISTER);
172
+ return output;
173
+ }
174
+ const freshTarget = {
175
+ id: targetData.id,
176
+ name: targetData.name,
177
+ type: targetData.type,
178
+ version: targetData.version,
179
+ tags: targetData.tags
180
+ };
181
+ output.entity = agent[_constants.REGISTER](freshTarget);
182
+ output.entity.metadata.target.iframeInterfaceId = iframeInterfaceId;
183
+ output.entity.metadata.target.iframeOrigin = event.origin;
184
+ return output;
185
+ }
186
+ const entity = (0, _utils.getRegisteredEntityByIframeInterfaceId)(iframeInterfaceId, agent);
187
+ if (!isValidOrigin(event, entity)) {
188
+ (0, _console.warn)(76);
189
+ return output;
190
+ }
191
+ output.entity = entity;
192
+ const methodFn = entity[method];
193
+ if (!isSafeProperty(entity, method) || typeof methodFn !== 'function') {
194
+ (0, _console.warn)(35, method);
195
+ return output;
196
+ }
197
+
198
+ // Supply the timestamp captured inside the iframe at call time (from event.data.timestamp)
199
+ entity.metadata.events.latestTimestamp = timestamp;
200
+ output.result = await methodFn.apply(entity, args || []);
201
+ return output;
202
+ }
203
+
204
+ /**
205
+ * Serializes entity metadata for postMessage response.
206
+ * Strips out any non-serializable properties and returns a plain object.
207
+ * postMessage cant handle complex objects that cause DataCloneErrors to throw like functions, circular references, etc.
208
+ * @param {Object} entity - The registered entity whose metadata is to be serialized. Uses a pattern of stringify and parse to remove any non-serializable properties from the metadata object.
209
+ * @returns {Object} - The serialized metadata object
210
+ */
211
+ function serializeMetadata(entity) {
212
+ const meta = entity.metadata;
213
+ return JSON.parse((0, _stringify.stringify)(meta));
214
+ }
215
+
216
+ /**
217
+ * Sends a postMessage response to the iframe
218
+ * @param {MessageEvent} event - The message event containing the method call or registration request
219
+ * @param {Object} payload - The response payload to send back to the iframe
220
+ */
221
+ function sendResponse(event, payload) {
222
+ event.source.postMessage({
223
+ type: _iframeConstants.IFRAME_API_RESPONSE,
224
+ messageId: event.data.messageId,
225
+ iframeInterfaceId: event.data.iframeInterfaceId,
226
+ ...payload
227
+ }, event.origin);
228
+ }
229
+
230
+ /**
231
+ * Sets up a postMessage listener to handle API calls and timing updates from iframes
232
+ * @param {Object} agent The agent instance
233
+ */
234
+ function setupIframeMFEMessageListener(agent) {
235
+ if (!_runtime.isBrowserScope || !window.addEventListener || agent.runtime.listeningForIframeMessages) return;
236
+ (0, _registerHandler.registerHandler)('iframe-message', async event => {
237
+ if (!event.data) return;
238
+ switch (event.data.type) {
239
+ case _iframeConstants.IFRAME_TIMING_UPDATE:
240
+ handleTimingUpdate(event, agent);
241
+ return;
242
+ case _iframeConstants.IFRAME_VITALS_UPDATE:
243
+ handleVitalsUpdate(event, agent);
244
+ return;
245
+ case _iframeConstants.IFRAME_AJAX:
246
+ handleAjax(event, agent);
247
+ return;
248
+ case _iframeConstants.IFRAME_API:
249
+ try {
250
+ if (!event.source || !event.data.messageId || !event.data.entries?.[0]?.method) return;
251
+ const {
252
+ entity,
253
+ result
254
+ } = await handleMethodCall(event, agent);
255
+ const metadata = entity ? serializeMetadata(entity) : undefined;
256
+ sendResponse(event, {
257
+ result,
258
+ metadata
259
+ });
260
+ } catch (error) {
261
+ const errorMessage = error?.message || String(error);
262
+ sendResponse(event, {
263
+ error: errorMessage
264
+ });
265
+ }
266
+ }
267
+ }, 'IFRAME', agent.ee);
268
+ (0, _drain.drain)(agent, 'IFRAME', true); // drain any buffered iframe messages that were received before the listener was set up
269
+ agent.runtime.listeningForIframeMessages = true;
270
+ }
@@ -23,7 +23,9 @@ const InitModelFn = () => {
23
23
  feature_flags: [],
24
24
  experimental: {
25
25
  register: false,
26
- resources: false
26
+ resources: false,
27
+ iframe_bridge: false,
28
+ iframe_domains: []
27
29
  },
28
30
  mask_selector: '*',
29
31
  block_selector: '[data-nr-block]',
@@ -63,7 +65,20 @@ const InitModelFn = () => {
63
65
  set enabled(val) {
64
66
  hiddenState.experimental.register = val;
65
67
  },
66
- duplicate_data_to_container: false
68
+ duplicate_data_to_container: false,
69
+ // experimental iframe bridge feature
70
+ get allow_iframe_bridge() {
71
+ return hiddenState.feature_flags.includes(FEATURE_FLAGS.IFRAME_BRIDGE) || hiddenState.experimental.iframe_bridge;
72
+ },
73
+ set allow_iframe_bridge(val) {
74
+ hiddenState.experimental.iframe_bridge = val;
75
+ },
76
+ get iframe_domains() {
77
+ return hiddenState.experimental.iframe_domains;
78
+ },
79
+ set iframe_domains(val) {
80
+ if (Array.isArray(val)) hiddenState.experimental.iframe_domains = val;else warn(1, val);
81
+ }
67
82
  }
68
83
  },
69
84
  browser_consent_mode: {
@@ -57,7 +57,8 @@ const RuntimeModel = {
57
57
  },
58
58
  get harvestCount() {
59
59
  return ++_harvestCount;
60
- }
60
+ },
61
+ listeningForIframeMessages: false
61
62
  };
62
63
  export const mergeRuntime = runtime => {
63
64
  const modeledObject = getModeledObject(runtime, RuntimeModel);
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.319.0";
14
+ export const VERSION = "1.320.0";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.319.0";
14
+ export const VERSION = "1.320.0";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ export const prefix = 'newrelic-iframe-';
6
+ export const IFRAME_TIMING_UPDATE = prefix + 'timing-update';
7
+ export const IFRAME_API = prefix + 'api';
8
+ export const IFRAME_API_RESPONSE = prefix + 'api-response';
9
+ export const IFRAME_VITALS_UPDATE = prefix + 'vitals-update';
10
+ export const IFRAME_AJAX = prefix + 'ajax';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import { parseUrl } from './parse-url.js';
6
+ export function addUrl(ctx, url) {
7
+ var parsed = parseUrl(url);
8
+ var params = ctx.params || ctx;
9
+ params.hostname = parsed.hostname;
10
+ params.port = parsed.port;
11
+ params.protocol = parsed.protocol;
12
+ params.host = parsed.hostname + ':' + parsed.port;
13
+ params.pathname = parsed.pathname;
14
+ ctx.parsedOrigin = parsed;
15
+ ctx.sameOrigin = parsed.sameOrigin;
16
+ }
@@ -16,39 +16,39 @@ import { dispatchGlobalEvent } from '../dispatch/global-event';
16
16
  *
17
17
  * | Code | Message |
18
18
  * |------|---------|
19
- * | 1 | An error occurred while setting a property of a Configurable |
20
- * | 2 | An error occurred while setting a Configurable |
21
- * | 3 | Setting a Configurable requires an object as input |
22
- * | 4 | Setting a Configurable requires a model to set its initial properties |
19
+ * | 1 | An error occurred while setting a property of a Configurable. |
20
+ * | 2 | An error occurred while setting a Configurable. |
21
+ * | 3 | Setting a Configurable requires an object as input. |
22
+ * | 4 | Setting a Configurable requires a model to set its initial properties. |
23
23
  * | 5 | An invalid session_replay.mask_selector was provided. \* will be used. |
24
- * | 6 | An invalid session_replay.block_selector was provided and will not be used |
25
- * | 7 | An invalid session_replay.mask_input_option was provided and will not be used |
26
- * | 8 | Shared context requires an object as input |
27
- * | 9 | An error occurred while setting SharedContext |
28
- * | 10 | Failed to read from storage API |
29
- * | 11 | Failed to write to the storage API |
24
+ * | 6 | An invalid session_replay.block_selector was provided and will not be used. |
25
+ * | 7 | An invalid session_replay.mask_input_option was provided and will not be used. |
26
+ * | 8 | Shared context requires an object as input. |
27
+ * | 9 | An error occurred while setting SharedContext. |
28
+ * | 10 | Failed to read from storage API. |
29
+ * | 11 | Failed to write to the storage API. |
30
30
  * | 12 | An obfuscation replacement rule was detected missing a "regex" value. |
31
- * | 13 | An obfuscation replacement rule contains a "regex" value with an invalid type (must be a string or RegExp) |
32
- * | 14 | An obfuscation replacement rule contains a "replacement" value with an invalid type (must be a string) |
33
- * | 15 | An error occurred while intercepting XHR |
34
- * | 16 | Could not cast log message to string |
31
+ * | 13 | An obfuscation replacement rule contains a "regex" value with an invalid type (must be a string or RegExp). |
32
+ * | 14 | An obfuscation replacement rule contains a "replacement" value with an invalid type (must be a string). |
33
+ * | 15 | An error occurred while intercepting XHR. |
34
+ * | 16 | Could not cast log message to string. |
35
35
  * | 17 | Could not calculate New Relic server time. Agent shutting down. |
36
36
  * | 18 | RUM call failed. Agent shutting down. |
37
37
  * | 19 | SPA scheduler is not initialized. Saved interaction is not sent! |
38
38
  * | 20 | A problem occurred when starting up session manager. This page will not start or extend any session. |
39
39
  * | 21 | Failed to initialize the agent. Could not determine the runtime environment. |
40
40
  * | 22 | Failed to initialize all enabled instrument classes (agent aborted) - |
41
- * | 23 | An unexpected issue occurred |
41
+ * | 23 | An unexpected issue occurred. |
42
42
  * | 24 | Something prevented the agent from instrumenting. |
43
43
  * | 25 | Something prevented the agent from being downloaded. |
44
44
  * | 26 | Failed to initialize instrument classes. |
45
45
  * | 27 | Downloading runtime APIs failed... |
46
46
  * | 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. |
47
- * | 29 | Failed to wrap logger: invalid argument(s) |
48
- * | 30 | Invalid log level |
49
- * | 31 | Ignored log: Log is larger than maximum payload size |
50
- * | 32 | Ignored log: Invalid message |
51
- * | 33 | Session Replay Aborted |
47
+ * | 29 | Failed to wrap logger: invalid argument(s). |
48
+ * | 30 | Invalid log level. |
49
+ * | 31 | Ignored log: Log is larger than maximum payload size. |
50
+ * | 32 | Ignored log: Invalid message. |
51
+ * | 33 | Session Replay Aborted. |
52
52
  * | 34 | Downloading and initializing a feature failed... |
53
53
  * | 35 | Call to agent api failed. The API is not currently initialized. |
54
54
  * | 36 | A feature is enabled but one or more dependent features have not been initialized. This may cause unintended consequences or missing data... |
@@ -57,7 +57,7 @@ import { dispatchGlobalEvent } from '../dispatch/global-event';
57
57
  * | 39 | Failed to execute setCustomAttribute. Name must be a string type. |
58
58
  * | 40 | Failed to execute setCustomAttribute. Non-null value must be a string, number or boolean type. |
59
59
  * | 41 | Failed to execute setUserId. Non-null value must be a string type. |
60
- * | 42 | Failed to execute setApplicationVersion. Expected <String \| null> |
60
+ * | 42 | Failed to execute setApplicationVersion. Expected <String \| null>. |
61
61
  * | 43 | Agent not configured properly. |
62
62
  * | 44 | Invalid object passed to generic event aggregate. Missing "eventType". |
63
63
  * | 45 | An internal agent process failed to execute. |
@@ -66,27 +66,34 @@ import { dispatchGlobalEvent } from '../dispatch/global-event';
66
66
  * | 48 | Supplied an invalid API target. Must be an <Object> that contains valid (string) id and name properties. |
67
67
  * | 49 | Supplied API target is missing an entityGuid. Some APIs may not behave correctly without a valid entityGuid (ex. logs). |
68
68
  * | 50 | Failed to connect. Cannot allow registered API. |
69
- * | 51 | Container agent is not available to register with. Can not connect |
69
+ * | 51 | Container agent is not available to register with. Can not connect. |
70
70
  * | 52 | Unexpected problem encountered. There should be at least one app for harvest! |
71
- * | 53 | Did not receive a valid entityGuid from connection response |
72
- * | 54 | An experimental feature is being used. Support can not be offered for issues |
73
- * | 55 | Register API has been disabled on the container agent |
74
- * | 56 | Could not find a matching entity to store data |
71
+ * | 53 | Did not receive a valid entityGuid from connection response. |
72
+ * | 54 | An experimental feature is being used. Support can not be offered for issues. |
73
+ * | 55 | Register API has been disabled on the container agent. |
74
+ * | 56 | Could not find a matching entity to store data. |
75
75
  * | 57 | Failed to execute measure. Arguments must have valid types. |
76
76
  * | 58 | Failed to execute measure. Resulting duration must be non-negative. |
77
- * | 59 | Session replay harvested before a session trace payload could be sent. This could be problematic for replays that rely on a trace |
78
- * | 60 | Session trace aborted |
77
+ * | 59 | Session replay harvested before a session trace payload could be sent. This could be problematic for replays that rely on a trace. |
78
+ * | 60 | Session trace aborted. |
79
79
  * | 61 | Timestamps must be non-negative and end time cannot be before start time. |
80
- * | 62 | Timestamp must be a unix timestamp greater than the page origin time |
81
- * | 63 | A single event was larger than the maximum allowed payload size |
80
+ * | 62 | Timestamp must be a unix timestamp greater than the page origin time. |
81
+ * | 63 | A single event was larger than the maximum allowed payload size. |
82
82
  * | 64 | Required globals have been mutated before being accessed by the browser agent. This can cause issues and should be avoided. |
83
- * | 65 | Consent API argument must be boolean or undefined |
84
- * | 66 | A new agent session has started |
83
+ * | 65 | Consent API argument must be boolean or undefined. |
84
+ * | 66 | A new agent session has started. |
85
85
  * | 67 | The "spa" feature has been deprecated and disabled. Please use/import "soft_navigations" instead for tracking of BrowserInteraction data. |
86
86
  * | 68 | API has been deregistered and can no longer be used. Call "register" API again with credentials to start over. |
87
- * | 69 | More than one Browser agent is running on the page |
87
+ * | 69 | More than one Browser agent is running on the page. |
88
88
  * | 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. |
89
89
  * | 71 | An invalid feature mode was detected and set to "off". |
90
+ * | 72 | RegisteredIframeEntity failed to transmit API data from an iframe to window context. |
91
+ * | 73 | RegisteredIframeEntity failed to register with window context. |
92
+ * | 74 | RegisteredIframeEntity rejected message from unauthorized origin. |
93
+ * | 75 | RegisteredIframeEntity rejected message with mismatched iframeInterfaceId. |
94
+ * | 76 | Agent rejected post message, could not match with existing entity. |
95
+ * | 77 | Agent rejected post message, could not validate origin. |
96
+ * | 78 | RegisteredIframeEntity could not determine parent origin and will not register, to avoid trusting messages from any origin. |
90
97
  *
91
98
  * @param {number} code The warning code to emit, which will be used to link to the warning code documentation
92
99
  * @param {*} [secondary] Secondary data to include, usually an extra message, error or object
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { globalScope, isBrowserScope } from '../constants/runtime';
7
7
  import { now } from '../timing/now';
8
+ import { CORRELATION_STALE_THRESHOLD_MS } from './script-tracker-constants';
8
9
 
9
10
  /**
10
11
  * @typedef {import('../../loaders/api/register-api-types').RegisterAPITimings} RegisterAPITimings
@@ -134,11 +135,17 @@ export function trackMFEVitals(target, timings) {
134
135
  fcp: {
135
136
  get value() {
136
137
  return getTimeRelativeToScriptStart(fcpObservedAt);
138
+ },
139
+ set value(v) {
140
+ fcpObservedAt = v;
137
141
  }
138
142
  },
139
143
  lcp: {
140
144
  get value() {
141
145
  return getTimeRelativeToScriptStart(lcpObservedAt);
146
+ },
147
+ set value(v) {
148
+ lcpObservedAt = v;
142
149
  }
143
150
  },
144
151
  cls: {
@@ -152,11 +159,11 @@ export function trackMFEVitals(target, timings) {
152
159
  if (!target || !isBrowserScope || !globalScope.MutationObserver || !globalScope.PerformanceObserver) return vitals;
153
160
  const observers = [];
154
161
 
155
- // If FCP hasn't been observed within 10 seconds, give up and shut down all observers.
162
+ // If FCP hasn't been observed within this window, give up and shut down all observers.
156
163
  // Once FCP is observed, the other vitals are left to record until their natural lifespan ends.
157
164
  setTimeout(() => {
158
165
  if (!fcpObservedAt) vitals.disconnect();
159
- }, 10000);
166
+ }, CORRELATION_STALE_THRESHOLD_MS);
160
167
  const populateVitalMinimums = () => {
161
168
  fcpObservedAt ??= now();
162
169
  lcpObservedAt ??= now();
@@ -18,6 +18,8 @@ export class ScriptCorrelation {
18
18
  constructor(url) {
19
19
  /** @type {string} The cleaned URL of the script */
20
20
  this.url = url;
21
+ /** @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. */
22
+ this.claimedBy = new Set();
21
23
  }
22
24
 
23
25
  /**
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /** @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`. */
7
+ export const CORRELATION_STALE_THRESHOLD_MS = 10000;
@@ -8,9 +8,12 @@ import { now } from '../timing/now';
8
8
  import { cleanURL } from '../url/clean-url';
9
9
  import { chrome, chromeEval, gecko } from '../util/browser-stack-matchers';
10
10
  import { ScriptCorrelation } from './script-correlation';
11
+ import { CORRELATION_STALE_THRESHOLD_MS } from './script-tracker-constants';
12
+ import { timingFactory } from './timing-factory';
11
13
 
12
14
  /**
13
15
  * @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
16
+ * @typedef {import('../../loaders/api/register-api-types').RegisterAPITarget} RegisterAPITarget
14
17
  */
15
18
 
16
19
  /** export for testing purposes */
@@ -22,7 +25,7 @@ try {
22
25
  }
23
26
 
24
27
  /** @type {(entry: PerformanceEntry) => boolean} - A shared function to determine if a performance entry is a valid script or link resource for evaluation */
25
- const validEntryCriteria = entry => entry.initiatorType === 'script' || ['link', 'fetch'].includes(entry.initiatorType) && entry.name.endsWith('.js');
28
+ const validEntryCriteria = entry => entry.initiatorType === 'script' || ['link', 'fetch'].includes(entry.initiatorType) && cleanURL(entry.name).endsWith('.js');
26
29
 
27
30
  /** @type {Map<string, ScriptCorrelation>} - Central registry for script correlations containing both DOM and Performance data */
28
31
  export const scriptCorrelations = new Map();
@@ -197,11 +200,32 @@ function applyPerformanceEntry(timings, entry) {
197
200
  timings.type = entry.initiatorType;
198
201
  }
199
202
 
203
+ /**
204
+ * Subscribes to late resource timing emissions for a script URL.
205
+ * @param {RegisterAPITimings} timings - The timings object to update
206
+ * @param {string} mfeScriptUrl - The script URL to match
207
+ */
208
+ function subscribeToLatePerformanceEntry(timings, mfeScriptUrl) {
209
+ if (!globalScope.PerformanceObserver?.supportedEntryTypes?.includes('resource')) return;
210
+ poSubscribers.push({
211
+ addedAt: now(),
212
+ test: entry => {
213
+ if (entryMatchesUrl(entry, mfeScriptUrl)) {
214
+ applyPerformanceEntry(timings, entry);
215
+ return true;
216
+ }
217
+ return false;
218
+ }
219
+ });
220
+ }
221
+
200
222
  /**
201
223
  * 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.
224
+ * @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.
202
225
  * @returns {RegisterAPITimings} Object containing script fetch start and end times, and the asset URL if found
203
226
  */
204
- export function findScriptTimings() {
227
+ export function findScriptTimings(target) {
228
+ const mfeId = target?.id;
205
229
  const timings = {
206
230
  registeredAt: now(),
207
231
  reportedAt: undefined,
@@ -231,38 +255,39 @@ export function findScriptTimings() {
231
255
  // Get correlation data
232
256
  timings.correlation = findCorrelation(mfeScriptUrl);
233
257
 
234
- // Use correlation's performance entry if available, otherwise check live performance API
235
- const performanceEntry = timings.correlation?.performance.value || performance.getEntriesByType('resource').find(e => entryMatchesUrl(e, mfeScriptUrl));
258
+ // Use correlation's performance entry if available, otherwise check the live performance API before falling back to the buffered observer.
259
+ const performanceEntry = timings.correlation?.performance.value || globalScope.performance?.getEntriesByType('resource')?.find(e => entryMatchesUrl(e, mfeScriptUrl));
236
260
  if (performanceEntry) {
237
261
  applyPerformanceEntry(timings, performanceEntry);
238
- } else if (wasPreloaded(mfeScriptUrl)) {
239
- // Handle preloaded scripts that may report late
240
- timings.asset = mfeScriptUrl;
241
- timings.type = 'preload';
262
+ } else {
263
+ const isPreloaded = wasPreloaded(mfeScriptUrl);
242
264
 
243
- // Subscribe to late performance observer callbacks
244
- poSubscribers.push({
245
- addedAt: now(),
246
- test: entry => {
247
- if (entryMatchesUrl(entry, mfeScriptUrl)) {
248
- applyPerformanceEntry(timings, entry);
249
- return true;
250
- }
251
- return false;
252
- }
253
- });
265
+ // Handle preloaded scripts and any late resource emissions through the shared buffered observer.
266
+ if (isPreloaded) {
267
+ timings.asset = mfeScriptUrl;
268
+ timings.type = 'preload';
269
+ }
270
+ subscribeToLatePerformanceEntry(timings, mfeScriptUrl);
254
271
  }
255
272
 
256
- /*
257
- * 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.
258
- * 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.
259
- */
260
- Object.defineProperty(timings, 'scriptStart', {
261
- get: () => timings.correlation?.script.start || timings.fetchEnd
262
- });
263
- Object.defineProperty(timings, 'scriptEnd', {
264
- get: () => timings.correlation?.script.end || timings.registeredAt
265
- });
273
+ // A correlation can be reused across multiple `register()` calls for the same script URL (e.g. an SPA
274
+ // remounting the same MFE without the script actually reloading). When that happens, its dom/performance
275
+ // timings still describe the *original* load, not this one. Detect that case so scriptStart/scriptEnd
276
+ // below can ignore the stale data instead of reporting it as if it were fresh.
277
+ const correlation = timings.correlation;
278
+ const alreadyClaimedByThisMFE = !!mfeId && !!correlation?.claimedBy.has(mfeId);
279
+ if (correlation && mfeId) correlation.claimedBy.add(mfeId);
280
+ const isCorrelationStale = () => {
281
+ const correlationStart = correlation?.script.start;
282
+ if (!alreadyClaimedByThisMFE || !correlationStart) return false;
283
+ const staleness = timings.registeredAt - correlationStart;
284
+ return staleness > CORRELATION_STALE_THRESHOLD_MS;
285
+ };
286
+
287
+ // 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.
288
+ // 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.
289
+ Object.defineProperty(timings, 'scriptStart', timingFactory(() => isCorrelationStale() ? timings.registeredAt : correlation?.script.start ?? timings.fetchEnd));
290
+ Object.defineProperty(timings, 'scriptEnd', timingFactory(() => isCorrelationStale() ? timings.registeredAt : correlation?.script.end ?? timings.registeredAt));
266
291
  } catch (error) {
267
292
  // Don't let stack parsing errors break anything
268
293
  }