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