@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
@@ -22,6 +22,7 @@ var _constants2 = require("../../metrics/constants");
22
22
  var _now = require("../../../common/timing/now");
23
23
  var _denyList = require("../../../common/deny-list/deny-list");
24
24
  var _extractUrl = require("../../../common/url/extract-url");
25
+ var _addUrl = require("../../../common/url/add-url");
25
26
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } /**
26
27
  * Copyright 2020-2026 New Relic, Inc. All rights reserved.
27
28
  * SPDX-License-Identifier: Apache-2.0
@@ -63,7 +64,7 @@ class Instrument extends _instrumentBase.InstrumentBase {
63
64
  duration: Math.floor(resource.duration),
64
65
  cbTime: 0
65
66
  };
66
- addUrl(params, resource.name);
67
+ (0, _addUrl.addUrl)(params, resource.name);
67
68
  this.handler('xhr', [params, metrics, resource.startTime, resource.responseEnd, initiators[resource.initiatorType]], undefined, _features.FEATURE_NAMES.ajax);
68
69
  }
69
70
  });
@@ -129,7 +130,7 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
129
130
  this.params = {
130
131
  method: args[0]
131
132
  };
132
- addUrl(this, args[1]);
133
+ (0, _addUrl.addUrl)(this, args[1]);
133
134
  this.metrics = {};
134
135
  }
135
136
  function onOpenXhrEnd(args, xhr) {
@@ -305,7 +306,7 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
305
306
  this.startTime = (0, _now.now)();
306
307
  this.dt = dtPayload;
307
308
  let [target, opts = {}] = fetchArguments;
308
- addUrl(this, (0, _extractUrl.extractUrl)(target));
309
+ (0, _addUrl.addUrl)(this, (0, _extractUrl.extractUrl)(target));
309
310
  const method = ('' + (target && target instanceof origRequest && target.method || opts.method || 'GET')).toUpperCase();
310
311
  this.params.method = method;
311
312
  this.txSize = (0, _dataSize.dataSize)(opts.body || target?.body) || 0;
@@ -441,17 +442,6 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
441
442
  ctx.loadCaptureCalled = true;
442
443
  }
443
444
  }
444
- function addUrl(ctx, url) {
445
- var parsed = (0, _parseUrl.parseUrl)(url);
446
- var params = ctx.params || ctx;
447
- params.hostname = parsed.hostname;
448
- params.port = parsed.port;
449
- params.protocol = parsed.protocol;
450
- params.host = parsed.hostname + ':' + parsed.port;
451
- params.pathname = parsed.pathname;
452
- ctx.parsedOrigin = parsed;
453
- ctx.sameOrigin = parsed.sameOrigin;
454
- }
455
445
  function parseResponseHeaders(headerStr) {
456
446
  const headers = {};
457
447
  if (!headerStr) return headers;
@@ -116,6 +116,19 @@ class InstrumentBase extends _featureBase.FeatureBase {
116
116
  this.ee.emit('internal-error', [e]);
117
117
  (0, _handle.handle)(_agentConstants.SESSION_ERROR, [e], undefined, this.featureName, this.ee);
118
118
  }
119
+ if (agentRef.init.api.register.allow_iframe_bridge) {
120
+ try {
121
+ // This chunk doesn't exist in the lite build (see webpack IgnorePlugin config) since none
122
+ // of lite's features wire up agent.register -- guard against that rather than letting an
123
+ // unhandled rejection surface if this flag is ever set on a lite page.
124
+ const {
125
+ setupIframeMFEMessageListener
126
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require(/* webpackChunkName: "iframe-message-handler" */'../../loaders/configure/iframe-message-handler')));
127
+ setupIframeMFEMessageListener(agentRef);
128
+ } catch (e) {
129
+ (0, _console.warn)(23, e);
130
+ }
131
+ }
119
132
 
120
133
  /**
121
134
  * Note this try-catch differs from the one in Agent.run() in that it's placed later in a page's lifecycle and
@@ -0,0 +1,527 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.RegisteredIframeEntity = void 0;
7
+ var _webVitals = require("web-vitals");
8
+ var _runtime = require("../common/constants/runtime");
9
+ var _iframe = require("../common/dom/iframe");
10
+ var _now = require("../common/timing/now");
11
+ var _console = require("../common/util/console");
12
+ var _scriptTracker = require("../common/v2/script-tracker");
13
+ var _addUrl = require("../common/url/add-url");
14
+ var _uniqueId = require("../common/ids/unique-id");
15
+ var _iframeConstants = require("../common/constants/iframe-constants");
16
+ var _castError = require("../features/jserrors/shared/cast-error");
17
+ /**
18
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
19
+ * SPDX-License-Identifier: Apache-2.0
20
+ */
21
+
22
+ // libraries
23
+
24
+ // internal
25
+
26
+ const REGISTER = 'register'; // define it here to prevent importing the full list of constants for build size.
27
+ const VITALS = [[_webVitals.onCLS, 'cls'], [_webVitals.onLCP, 'lcp'], [_webVitals.onFCP, 'fcp'], [_webVitals.onINP, 'inp']];
28
+ const AJAX_INITIATOR_TYPES = {
29
+ xmlhttprequest: 'xhr',
30
+ fetch: 'fetch',
31
+ beacon: 'beacon'
32
+ };
33
+
34
+ /**
35
+ * @typedef {import('../loaders/api/register-api-types').RegisterAPI} RegisterAPI
36
+ * @typedef {import('../loaders/api/register-api-types').RegisterAPIMetadata} RegisterAPIMetadata
37
+ * @typedef {import('../loaders/api/register-api-types').RegisterAPIConstructor} RegisterAPIConstructor
38
+ */
39
+
40
+ /**
41
+ * @experimental
42
+ * IMPORTANT: This feature is being developed for use internally and is not in a public-facing production-ready state.
43
+ * It is not recommended for use in production environments and will not receive support for issues.
44
+ *
45
+ * An interface for registering an external caller to report through the base agent to a different target than the base agent.
46
+ */
47
+ class RegisteredIframeEntity {
48
+ /** @type {RegisterAPIMetadata} */
49
+ metadata = {
50
+ target: {},
51
+ timings: {},
52
+ customAttributes: {},
53
+ vitals: {
54
+ cls: {
55
+ value: null
56
+ },
57
+ lcp: {
58
+ value: null
59
+ },
60
+ fcp: {
61
+ value: null
62
+ },
63
+ inp: {
64
+ value: null
65
+ }
66
+ }
67
+ };
68
+
69
+ /** @private Map to store pending promise resolvers keyed by message ID */
70
+ #pendingMessages = new Map();
71
+ /** @private Unique ID for this iframe interface instance to correlate messages */
72
+ #iframeInterfaceId = (0, _uniqueId.generateUuid)();
73
+ /** @private Counter for generating unique message IDs */
74
+ #messageIdCounter = 0;
75
+ /** @private Promise that resolves when registration with parent completes */
76
+ #registrationPromise = null;
77
+ /** @private Resource timing observer used to seed AJAX state */
78
+ #resourceObserver = null;
79
+ /** @private Original target descriptor (serializable) for postMessage */
80
+ #targetDescriptor = null;
81
+ /**
82
+ * Whether this entity is blocked from sending further calls to the container. Backed by
83
+ * `this.metadata.target.blocked` (rather than an independent field) so that this always
84
+ * reflects the container's blocked state too, including future syncs from response metadata --
85
+ * there is only ever one source of truth for "blocked", not one flag per side.
86
+ * @returns {boolean}
87
+ */
88
+ get blocked() {
89
+ return !!this.metadata.target.blocked;
90
+ }
91
+ set blocked(value) {
92
+ this.metadata.target.blocked = value;
93
+ }
94
+
95
+ /** @private Parent window origin for secure postMessage */
96
+ #parentOrigin = (() => {
97
+ try {
98
+ return _runtime.globalScope?.location?.ancestorOrigins?.[0] || (_runtime.globalScope?.document?.referrer ? new URL(_runtime.globalScope.document.referrer).origin : '*');
99
+ } catch (e) {
100
+ return '*';
101
+ }
102
+ })();
103
+
104
+ /**
105
+ *
106
+ * @param {RegisterAPIConstructor} opts The options for setting up the registered iframe entity.
107
+ */
108
+ constructor(opts) {
109
+ (0, _console.warn)(54);
110
+ // Store original descriptor for postMessage (before any function merging)
111
+ this.metadata.target = this.#targetDescriptor = opts;
112
+ if (!_runtime.isBrowserScope || !(0, _iframe.isIFrameWindow)(_runtime.globalScope)) {
113
+ (0, _console.warn)(72);
114
+ this.blocked = true;
115
+ return;
116
+ }
117
+ if (this.#parentOrigin === '*') {
118
+ // If the parent's origin cannot be determined, fail closed rather than allow postMessage
119
+ // traffic to/from any origin
120
+ (0, _console.warn)(78);
121
+ this.blocked = true;
122
+ return;
123
+ }
124
+
125
+ // Store the registration promise so other methods can wait for it
126
+ this.#registrationPromise = this.#register(opts);
127
+ this.#registrationPromise.then(() => {
128
+ const timings = (0, _scriptTracker.findScriptTimings)();
129
+ // Send initial timing values, skipping ones still at their "not yet known" default
130
+ // (e.g. reportedAt: undefined, fetchStart: 0) to avoid pointless postMessage calls
131
+ for (const [key, value] of Object.entries(timings)) {
132
+ if (key !== 'correlation' && value) {
133
+ this.#postTimingToAgent(key, value);
134
+ }
135
+ }
136
+ // Proxy the timings object to watch for updates to fetchStart, fetchEnd, asset, type
137
+ this.metadata.timings = new Proxy(timings, {
138
+ set: (target, key, value) => {
139
+ const changed = target[key] !== value;
140
+ target[key] = value;
141
+
142
+ // Send updates for these 4 properties when they change
143
+ if (changed && this.metadata.target.id && key !== 'correlation') {
144
+ this.#postTimingToAgent(key, value);
145
+ }
146
+ return true;
147
+ }
148
+ });
149
+ }).catch(() => {});
150
+ this.#setupErrorListeners();
151
+ this.#setupVitalsListeners();
152
+ this.#setupAjaxObserver();
153
+ this.#setupResponseListener();
154
+ this.#bindPublicMethods();
155
+ }
156
+
157
+ /**
158
+ * Registers this entity with the parent agent, then seeds initial timings and FCP.
159
+ * @private
160
+ * @param {RegisterAPIConstructor} opts
161
+ * @returns {Promise<void>}
162
+ */
163
+ async #register(opts) {
164
+ try {
165
+ const response = await this.#postMethodToAgent(REGISTER, [opts]);
166
+ if (response.metadata) Object.assign(this.metadata, response.metadata);
167
+ return response;
168
+ } catch (err) {
169
+ (0, _console.warn)(73, err);
170
+ this.blocked = true;
171
+ throw err;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Wires up global error/rejection listeners that funnel into noticeError.
177
+ * @private
178
+ */
179
+ #setupErrorListeners() {
180
+ _runtime.globalScope.addEventListener('error', err => {
181
+ this.noticeError((0, _castError.castErrorEvent)(err));
182
+ });
183
+ _runtime.globalScope.addEventListener('unhandledrejection', event => {
184
+ this.noticeError((0, _castError.castPromiseRejectionEvent)(event));
185
+ });
186
+ }
187
+
188
+ /**
189
+ * Wires up web-vitals callbacks to report vitals updates to the parent.
190
+ * @private
191
+ */
192
+ #setupVitalsListeners() {
193
+ VITALS.forEach(([vitalFn, property]) => {
194
+ vitalFn(({
195
+ value
196
+ }) => {
197
+ this.metadata.vitals[property].value = value;
198
+ this.#postMessageToParent(_iframeConstants.IFRAME_VITALS_UPDATE, {
199
+ entries: [{
200
+ property,
201
+ value
202
+ }]
203
+ });
204
+ }, {
205
+ reportAllChanges: property === 'cls' || property === 'inp'
206
+ });
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Instruments ajax using buffered resource timing so pre-registration entries are included.
212
+ * @private
213
+ */
214
+ #setupAjaxObserver() {
215
+ if (!_runtime.globalScope.PerformanceObserver?.supportedEntryTypes?.includes('resource')) return;
216
+ this.#resourceObserver = new _runtime.globalScope.PerformanceObserver(list => {
217
+ // Batched into a single postMessage per observer callback rather than one per entry --
218
+ // with buffered: true this callback can fire with dozens of pre-existing entries at once,
219
+ // and each postMessage carries its own await/registration/dispatch overhead independent of
220
+ // payload size, so sending them individually multiplies that overhead for no benefit.
221
+ const entries = list.getEntries().map(resource => this.#buildAjaxPayload(resource)).filter(Boolean);
222
+ if (!entries.length) return;
223
+ this.#postMessageToParent(_iframeConstants.IFRAME_AJAX, {
224
+ entries
225
+ });
226
+ });
227
+ this.#resourceObserver.observe({
228
+ type: 'resource',
229
+ buffered: true
230
+ });
231
+ }
232
+
233
+ /**
234
+ * Builds the AJAX event payload for a single resource timing entry, if it looks like a network request.
235
+ * @private
236
+ * @param {PerformanceResourceTiming} resource
237
+ * @returns {object|undefined}
238
+ */
239
+ #buildAjaxPayload(resource) {
240
+ if (!(resource.initiatorType in AJAX_INITIATOR_TYPES)) return;
241
+ // Cross-origin requests without a Timing-Allow-Origin response header report responseStatus (and
242
+ // transferSize) as 0 per spec -- that's a browser privacy restriction, not evidence the request
243
+ // failed, so it should still be reported rather than dropped. status: 0 is already the established
244
+ // "unknown" convention elsewhere in the ajax feature (see features/ajax/instrument/index.js).
245
+ const params = {
246
+ status: resource.responseStatus
247
+ };
248
+ const metrics = {
249
+ rxSize: resource.transferSize,
250
+ duration: Math.floor(resource.duration),
251
+ cbTime: 0
252
+ };
253
+ (0, _addUrl.addUrl)(params, resource.name);
254
+ return {
255
+ params,
256
+ metrics,
257
+ start: resource.startTime,
258
+ end: resource.responseEnd,
259
+ initiatorType: AJAX_INITIATOR_TYPES[resource.initiatorType]
260
+ };
261
+ }
262
+
263
+ /**
264
+ * Listens for postMessage responses from the parent window and routes them to pending resolvers.
265
+ * @private
266
+ */
267
+ #setupResponseListener() {
268
+ _runtime.globalScope.addEventListener('message', event => {
269
+ if (this.blocked) return;
270
+ // Validate message structure
271
+ if (event.data?.type !== _iframeConstants.IFRAME_API_RESPONSE) return;
272
+
273
+ // Validate iframeInterfaceId first to confirm this message actually claims to be addressed
274
+ // to this instance -- messageIds are only unique per-instance, so we must not act on one
275
+ // (including rejecting) until we know it's actually meant for us.
276
+ if (event.data.iframeInterfaceId !== this.#iframeInterfaceId) {
277
+ (0, _console.warn)(75);
278
+ return;
279
+ }
280
+
281
+ // Validate origin now that we know the message claims to be for us. Reject the pending
282
+ // call immediately rather than leaving it to time out, since we know exactly which
283
+ // messageId this response was for.
284
+ if (event.origin !== this.#parentOrigin) {
285
+ (0, _console.warn)(74, event.origin);
286
+ this.#closePending({
287
+ messageId: event.data.messageId,
288
+ error: 'Rejected message from unauthorized origin'
289
+ });
290
+ return;
291
+ }
292
+ this.#closePending(event.data);
293
+ });
294
+ }
295
+
296
+ /**
297
+ * Explicitly binds API methods as own properties for better console visibility.
298
+ * @private
299
+ */
300
+ #bindPublicMethods() {
301
+ this.addPageAction = this.addPageAction.bind(this);
302
+ this.deregister = this.deregister.bind(this);
303
+ this.recordCustomEvent = this.recordCustomEvent.bind(this);
304
+ this.measure = this.measure.bind(this);
305
+ this.setCustomAttribute = this.setCustomAttribute.bind(this);
306
+ this.noticeError = this.noticeError.bind(this);
307
+ this.setUserId = this.setUserId.bind(this);
308
+ this.setApplicationVersion = this.setApplicationVersion.bind(this);
309
+ this.log = this.log.bind(this);
310
+ }
311
+
312
+ /**
313
+ * Low-level helper to send postMessage to parent window with error handling
314
+ * @private
315
+ * @param {string} type - The message type to send
316
+ * @param {object} data - The message payload to send
317
+ * @param {boolean} [bypassRegistration=false] - Whether to bypass waiting for registration
318
+ * @param {boolean} [needsResponse=false] - Whether to wait for a response from the parent
319
+ * @returns {Promise<void>}
320
+ */
321
+ async #postMessageToParent(type, data, bypassRegistration = false, needsResponse = false) {
322
+ if (this.blocked) return;
323
+ const timestamp = (0, _now.now)();
324
+ try {
325
+ await (bypassRegistration ? Promise.resolve() : this.#registrationPromise);
326
+ const messageId = ++this.#messageIdCounter;
327
+ const pending = needsResponse ? this.#openPending(messageId) : Promise.resolve();
328
+ _runtime.globalScope.parent.postMessage({
329
+ type,
330
+ target: this.#targetDescriptor,
331
+ timestamp,
332
+ iframeInterfaceId: this.#iframeInterfaceId,
333
+ messageId,
334
+ ...data
335
+ }, this.#parentOrigin);
336
+ return await pending;
337
+ } catch (err) {
338
+ // If the postMessage never responded with a new message, it will surface here -- but, this could be expected, as many messages are sent to the parent that don't require a response.
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Sends a timing property update message to the parent window
344
+ * @private
345
+ * @param {string} property - The property name that changed
346
+ * @param {*} value - The new value
347
+ */
348
+ #postTimingToAgent(property, value) {
349
+ this.#postMessageToParent(_iframeConstants.IFRAME_TIMING_UPDATE, {
350
+ entries: [{
351
+ property,
352
+ value
353
+ }]
354
+ });
355
+ }
356
+
357
+ /**
358
+ * Sends a message to the parent window's agent using postMessage API
359
+ * @private
360
+ * @param {string} method The API method name to invoke
361
+ * @param {Array} args The arguments to pass to the method
362
+ * @returns {Promise<any>} Promise that resolves with the response from the agent
363
+ */
364
+ async #postMethodToAgent(method, args) {
365
+ return await this.#postMessageToParent(_iframeConstants.IFRAME_API, {
366
+ entries: [{
367
+ method,
368
+ args
369
+ }]
370
+ }, method === REGISTER, true);
371
+ }
372
+
373
+ /**
374
+ * Opens a pending entry for a message awaiting a response, auto-rejecting after a timeout.
375
+ * @private
376
+ * @param {number} messageId
377
+ * @returns {Promise<any>}
378
+ */
379
+ #openPending(messageId) {
380
+ const resolvers = {};
381
+ const pending = new Promise((resolve, reject) => {
382
+ resolvers.resolve = resolve;
383
+ resolvers.reject = reject;
384
+ });
385
+ this.#pendingMessages.set(messageId, resolvers);
386
+ // Timeout after 10 seconds
387
+ setTimeout(() => this.#closePending({
388
+ messageId,
389
+ error: 'Timed out'
390
+ }), 10000);
391
+ return pending;
392
+ }
393
+
394
+ /**
395
+ * Resolves or rejects a pending message by ID, based on an incoming response (or timeout).
396
+ * @private
397
+ * @param {{messageId: number, error?: string, result?: any, metadata?: object}} event
398
+ */
399
+ #closePending(event = {}) {
400
+ const {
401
+ messageId,
402
+ error,
403
+ result,
404
+ metadata
405
+ } = event;
406
+ const pending = this.#pendingMessages.get(messageId);
407
+ if (pending) {
408
+ if (error) pending.reject(new Error(error));else pending.resolve({
409
+ result,
410
+ metadata
411
+ });
412
+ this.#pendingMessages.delete(messageId);
413
+ }
414
+ }
415
+
416
+ // ---------------------------------------------------------------------------
417
+ // Public API
418
+ // ---------------------------------------------------------------------------
419
+
420
+ /**
421
+ * Reports a browser PageAction event along with a name and optional attributes to the registered target.
422
+ * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/addpageaction/}
423
+ * @param {string} name Name or category of the action. Reported as the actionName attribute.
424
+ * @param {object} [attributes] JSON object with one or more key/value pairs. For example: {key:"value"}. The key is reported as its own PageAction attribute with the specified values.
425
+ */
426
+ addPageAction(name, attributes) {
427
+ this.#postMethodToAgent('addPageAction', [name, attributes]);
428
+ }
429
+
430
+ /**
431
+ * @experimental
432
+ * IMPORTANT: This feature is being developed for use internally and is not in a public-facing production-ready state.
433
+ * It is not recommended for use in production environments and will not receive support for issues.
434
+ *
435
+ * Deregister the registered entity (this), which blocks its use and captures end of life timings.
436
+ * @returns {Promise<void>}
437
+ */
438
+ async deregister() {
439
+ try {
440
+ this.#resourceObserver?.disconnect();
441
+ } catch (err) {}
442
+ this.#resourceObserver = null;
443
+ try {
444
+ const response = await this.#postMethodToAgent('deregister', []);
445
+ if (response?.metadata) Object.assign(this.metadata, response.metadata);
446
+ } finally {
447
+ // Always end up blocked locally, even if the round trip to the container never resolved --
448
+ // deregistering is a one-way decision and further local calls should stop regardless.
449
+ this.blocked = true;
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Records a custom event with a specified eventType and attributes.
455
+ * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/recordCustomEvent/}
456
+ * @param {string} eventType The eventType to store the event as.
457
+ * @param {Object} [attributes] JSON object with one or more key/value pairs. For example: {key:"value"}.
458
+ */
459
+ recordCustomEvent(eventType, attributes) {
460
+ this.#postMethodToAgent('recordCustomEvent', [eventType, attributes]);
461
+ }
462
+
463
+ /**
464
+ * Measures a task that is recorded as a BrowserPerformance event.
465
+ * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/measure/}
466
+ * @param {string} name The name of the task
467
+ * @param {{start?: number|PerformanceMark, end?: number|PerformanceMark, customAttributes?: object}} [options] An object used to control the way the measure API operates
468
+ * @returns {Promise<{start: number, end: number, duration: number, customAttributes: object}>} Measurement details
469
+ */
470
+ async measure(name, options) {
471
+ return (await this.#postMethodToAgent('measure', [name, options])).result;
472
+ }
473
+
474
+ /**
475
+ * Adds a user-defined attribute name and value to subsequent events on the page for the registered target. Note -- the persist flag does not work with the register API.
476
+ * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/setcustomattribute/}
477
+ * @param {string} name Name of the attribute. Appears as column in the PageView event. It will also appear as a column in the PageAction event if you are using it.
478
+ * @param {string|number|boolean|null} value Value of the attribute. Appears as the value in the named attribute column in the PageView event. It will appear as a column in the PageAction event if you are using it. Custom attribute values cannot be complex objects, only simple types such as Strings, Integers and Booleans. Passing a null value unsets any existing attribute of the same name.
479
+ * @param {boolean} [persist] Default false. If set to true, the name-value pair will also be set into the browser's storage API. Then on the following instrumented pages that load within the same session, the pair will be re-applied as a custom attribute.
480
+ */
481
+ setCustomAttribute(name, value, persist) {
482
+ this.#postMethodToAgent('setCustomAttribute', [name, value, persist]);
483
+ }
484
+
485
+ /**
486
+ * Identifies a browser error without disrupting your app's operations for the registered target.
487
+ * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/noticeerror/}
488
+ * @param {Error|string} error Provide a meaningful error message that you can use when analyzing data on browser's JavaScript errors page.
489
+ * @param {object} [customAttributes] An object containing name/value pairs representing custom attributes.
490
+ */
491
+ noticeError(error, customAttributes) {
492
+ this.#postMethodToAgent('noticeError', [(0, _castError.castError)(error), customAttributes]);
493
+ }
494
+
495
+ /**
496
+ * Adds a user-defined identifier string to subsequent events on the page for the registered target.
497
+ * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/setuserid/}
498
+ * @param {string|null} value A string identifier for the end-user, useful for tying all browser events to specific users. The value parameter does not have to be unique. If IDs should be unique, the caller is responsible for that validation. Passing a null value unsets any existing user ID.
499
+ * @param {boolean} [resetSession=false] Optional param. Should not be used from a registered entity context. To reset a session when updating user id, must be initiated by the main agent.
500
+ */
501
+ setUserId(value, resetSession = false) {
502
+ this.#postMethodToAgent('setUserId', [value, resetSession]);
503
+ }
504
+
505
+ /**
506
+ * Adds a user-defined application version string to subsequent events on the page for the registered target.
507
+ * This decorates all payloads with an attribute of `application.version` which is queryable in NR1.
508
+ * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/setapplicationversion/}
509
+ * @param {string|null} value A string identifier for the application version, useful for
510
+ * tying all browser events to a specific release tag. The value parameter does not
511
+ * have to be unique. Passing a null value unsets any existing value.
512
+ */
513
+ setApplicationVersion(value) {
514
+ this.#postMethodToAgent('setApplicationVersion', [value]);
515
+ }
516
+
517
+ /**
518
+ * Capture a single log for the registered target.
519
+ * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/log/}
520
+ * @param {string} message String to be captured as log message
521
+ * @param {{customAttributes?: object, level?: 'ERROR'|'TRACE'|'DEBUG'|'INFO'|'WARN'}} [options] customAttributes defaults to `{}` if not assigned, level defaults to `info` if not assigned.
522
+ */
523
+ log(message, options) {
524
+ this.#postMethodToAgent('log', [message, options]);
525
+ }
526
+ }
527
+ exports.RegisteredIframeEntity = RegisteredIframeEntity;
@@ -79,12 +79,12 @@ function register(agentRef, target) {
79
79
  if (typeof target.tags !== 'object' || target.tags === null || Array.isArray(target.tags)) target.tags = {};
80
80
  target.parent ??= {
81
81
  get id() {
82
- return agentRef.runtime.appMetadata.agents[0].entityGuid;
82
+ return agentRef.runtime.appMetadata.agents?.[0].entityGuid;
83
83
  },
84
- // getter because this is asyncronously set
84
+ // getter because this is asynchronously set
85
85
  type: _utils.V2_TYPES.BA
86
86
  };
87
- const timings = (0, _scriptTracker.findScriptTimings)();
87
+ const timings = (0, _scriptTracker.findScriptTimings)(target);
88
88
 
89
89
  // Track MFE vitals for this entity
90
90
  const vitals = (0, _mfeVitals.trackMFEVitals)(target, timings);
@@ -177,7 +177,10 @@ function register(agentRef, target) {
177
177
  },
178
178
  target,
179
179
  timings,
180
- vitals
180
+ vitals,
181
+ events: {
182
+ latestTimestamp: undefined
183
+ }
181
184
  }
182
185
  };
183
186
 
@@ -274,8 +277,9 @@ function register(agentRef, target) {
274
277
  const report = (methodToCall, args, target) => {
275
278
  /** Even if we are blocked, if registering we should still return a child register API so nested API calls do not throw errors */
276
279
  if (isBlocked() && methodToCall !== register) return;
277
- /** set the timestamp before the async part of waiting for the rum response for better accuracy */
278
- const timestamp = (0, _now.now)();
280
+ /** use the timestamp captured inside the iframe for this call, if one was supplied (see iframe-message-handler.js); otherwise fall back to now(). Consume it immediately so a stale value can't leak into a later call that isn't preceded by a fresh iframe message (e.g. deregister() via page unload) */
281
+ const timestamp = api.metadata.events.latestTimestamp ?? (0, _now.now)();
282
+ api.metadata.events.latestTimestamp = undefined;
279
283
  const methodName = METHOD_NAMES.get(methodToCall) || 'unknown';
280
284
  (0, _handle.handle)(_constants.SUPPORTABILITY_METRIC_CHANNEL, ["API/register/".concat(methodName, "/called")], undefined, _features.FEATURE_NAMES.metrics, agentRef.ee);
281
285
  try {
@@ -14,6 +14,8 @@ var _publicPath = require("./public-path");
14
14
  var _contextualEe = require("../../common/event-emitter/contextual-ee");
15
15
  var _globalEvent = require("../../common/dispatch/global-event");
16
16
  var _loaderConfig = require("../../common/config/loader-config");
17
+ var _handle = require("../../common/event-emitter/handle");
18
+ var _iframeConstants = require("../../common/constants/iframe-constants");
17
19
  /**
18
20
  * Copyright 2020-2026 New Relic, Inc. All rights reserved.
19
21
  * SPDX-License-Identifier: Apache-2.0
@@ -80,6 +82,18 @@ function configure(agent, opts = {}, loaderType, forceDrain) {
80
82
  feature: undefined,
81
83
  data: agent.config
82
84
  });
85
+
86
+ // Set up iframe postMessage listener for registered entities
87
+ if (agent.init.api.register.allow_iframe_bridge) {
88
+ _runtime2.globalScope.addEventListener('message', event => {
89
+ // Pre-filter here rather than in setupIframeMFEMessageListener, so unrelated MessageEvents
90
+ // (there can be many, from any script on the page) aren't buffered/held onto in memory
91
+ // while waiting for the iframe bridge's lazy chunk to load and drain the buffer.
92
+ if (typeof event.data?.type === 'string' && event.data.type.startsWith(_iframeConstants.prefix)) {
93
+ (0, _handle.handle)('iframe-message', [event], undefined, 'IFRAME', agent.ee);
94
+ }
95
+ });
96
+ }
83
97
  agent.runtime.configured = true;
84
98
  }
85
99
  }