@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
@@ -78,11 +78,11 @@ function register (agentRef, target) {
78
78
  target.blocked = false
79
79
  if (typeof target.tags !== 'object' || target.tags === null || Array.isArray(target.tags)) target.tags = {}
80
80
  target.parent ??= {
81
- get id () { return agentRef.runtime.appMetadata.agents[0].entityGuid }, // getter because this is asyncronously set
81
+ get id () { return agentRef.runtime.appMetadata.agents?.[0].entityGuid }, // getter because this is asynchronously set
82
82
  type: V2_TYPES.BA
83
83
  }
84
84
 
85
- const timings = findScriptTimings()
85
+ const timings = findScriptTimings(target)
86
86
 
87
87
  // Track MFE vitals for this entity
88
88
  const vitals = trackMFEVitals(target, timings)
@@ -154,7 +154,10 @@ function register (agentRef, target) {
154
154
  get customAttributes () { return attrs },
155
155
  target,
156
156
  timings,
157
- vitals
157
+ vitals,
158
+ events: {
159
+ latestTimestamp: undefined
160
+ }
158
161
  }
159
162
  }
160
163
 
@@ -234,8 +237,9 @@ function register (agentRef, target) {
234
237
  const report = (methodToCall, args, target) => {
235
238
  /** Even if we are blocked, if registering we should still return a child register API so nested API calls do not throw errors */
236
239
  if (isBlocked() && methodToCall !== register) return
237
- /** set the timestamp before the async part of waiting for the rum response for better accuracy */
238
- const timestamp = now()
240
+ /** 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) */
241
+ const timestamp = api.metadata.events.latestTimestamp ?? now()
242
+ api.metadata.events.latestTimestamp = undefined
239
243
  const methodName = METHOD_NAMES.get(methodToCall) || 'unknown'
240
244
  handle(SUPPORTABILITY_METRIC_CHANNEL, [`API/register/${methodName}/called`], undefined, FEATURE_NAMES.metrics, agentRef.ee)
241
245
  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.
@@ -81,6 +83,18 @@ export function configure (agent, opts = {}, loaderType, forceDrain) {
81
83
  data: agent.config
82
84
  })
83
85
 
86
+ // Set up iframe postMessage listener for registered entities
87
+ if (agent.init.api.register.allow_iframe_bridge) {
88
+ 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(iframePrefix)) {
93
+ handle('iframe-message', [event], undefined, 'IFRAME', agent.ee)
94
+ }
95
+ })
96
+ }
97
+
84
98
  agent.runtime.configured = true
85
99
  }
86
100
  }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import { warn } from '../../common/util/console'
6
+ import { getRegisteredEntityByIframeInterfaceId } from '../../common/v2/utils'
7
+ import { IFRAME_TIMING_UPDATE, IFRAME_API, IFRAME_API_RESPONSE, IFRAME_VITALS_UPDATE, IFRAME_AJAX } from '../../common/constants/iframe-constants'
8
+ import { REGISTER } from '../api/constants'
9
+ import { handle } from '../../common/event-emitter/handle'
10
+ import { FEATURE_NAMES } from '../features/features'
11
+ import { stringify } from '../../common/util/stringify'
12
+ import { registerHandler } from '../../common/event-emitter/register-handler'
13
+ import { drain } from '../../common/drain/drain'
14
+ import { isBrowserScope } from '../../common/constants/runtime'
15
+
16
+ /**
17
+ * 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.
18
+ * @param {MessageEvent} event - The message event containing the method call or registration request
19
+ * @param {Object} agent - The browser agent instance
20
+ * @returns {Object|undefined} - The registered entity if it exists and the origin is valid, otherwise undefined
21
+ */
22
+ function getValidEntity (event, agent) {
23
+ try {
24
+ const { iframeInterfaceId } = event.data
25
+ if (!iframeInterfaceId) return
26
+ const entity = getRegisteredEntityByIframeInterfaceId(iframeInterfaceId, agent)
27
+ if (!isValidOrigin(event, entity)) return
28
+ return entity
29
+ } catch (e) {
30
+ // couldnt get entity or validate origin, just let the method return undefined
31
+ }
32
+ }
33
+
34
+ function isSafeProperty (obj, property) {
35
+ return property in obj && typeof property === 'string' && !['__proto__', 'constructor', 'prototype'].includes(property)
36
+ }
37
+
38
+ /**
39
+ * Handles timing property updates from iframes. Mutates the timings object on the entity's metadata with the new value for the specified property.
40
+ * @param {MessageEvent} event - The message event containing the timing update
41
+ * @param {Object} agent - The browser agent instance
42
+ * @returns
43
+ */
44
+ function handleTimingUpdate (event, agent) {
45
+ const entity = getValidEntity(event, agent)
46
+ if (!entity) return
47
+
48
+ if (!entity.metadata.timings) return
49
+
50
+ event.data.entries?.forEach(({ property, value }) => {
51
+ if (isSafeProperty(entity.metadata.timings, property)) {
52
+ entity.metadata.timings[property] = value
53
+ }
54
+ })
55
+ }
56
+
57
+ /**
58
+ * Handles vitals property updates from iframes. Mutates the vitals object on the entity's metadata with the new value for the specified property.
59
+ * @param {MessageEvent} event - The message event containing the vitals update
60
+ * @param {Object} agent - The browser agent instance
61
+ * @returns
62
+ */
63
+ function handleVitalsUpdate (event, agent) {
64
+ const entity = getValidEntity(event, agent)
65
+ if (!entity) return
66
+
67
+ if (!entity.metadata?.vitals) return
68
+
69
+ event.data.entries?.forEach(({ property, value }) => {
70
+ if (isSafeProperty(entity.metadata.vitals, property) && (!!Number(value) || value === 0)) {
71
+ entity.metadata.vitals[property].value = value
72
+ }
73
+ })
74
+ }
75
+
76
+ /**
77
+ * Handles ajax events from iframes and forwards them to the agent's event emitter
78
+ * @param {MessageEvent} event - The message event containing the method call or registration request
79
+ * @param {Object} agent - The browser agent instance
80
+ * @returns
81
+ */
82
+ function handleAjax (event, agent) {
83
+ const entity = getValidEntity(event, agent)
84
+ if (!entity) return
85
+ const { entries } = event.data
86
+ entries?.forEach(({ params, metrics, start, end, initiatorType }) => {
87
+ handle('xhr', [params, metrics, start, end, initiatorType, entity.metadata.target], undefined, FEATURE_NAMES.ajax, agent.ee)
88
+ })
89
+ }
90
+
91
+ /**
92
+ * Validates that the origin of the message event matches the expected origin for the registered entity
93
+ * @param {MessageEvent} event - The message event containing the method call or registration request
94
+ * @param {Object} entity - The registered entity associated with the iframeInterfaceId from the event data
95
+ * @returns {boolean}
96
+ */
97
+ function isValidOrigin (event, entity) {
98
+ if (!entity) return false
99
+ try {
100
+ // "null" is the literal serialization the spec uses for every opaque origin (sandboxed iframes
101
+ // without allow-same-origin, data:/javascript: URLs, etc.) -- it does not uniquely identify a
102
+ // single origin, so treating two "null" origins as a match would let an unrelated opaque-origin
103
+ // frame impersonate the one that actually registered.
104
+ if (event.origin === 'null' || entity.metadata.target.iframeOrigin === 'null') return false
105
+ return event.origin === entity.metadata.target.iframeOrigin
106
+ } catch (e) {
107
+ warn(77, e)
108
+ return false
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Handles both entity registration and method calls on existing entities
114
+ * @async
115
+ * @param {MessageEvent} event - The message event containing the method call or registration request
116
+ * @param {Object} agent - The browser agent instance
117
+ * @returns {Promise<{entity: Object|null, result: any}>}
118
+ */
119
+ export async function handleMethodCall (event, agent) {
120
+ const { target, iframeInterfaceId, timestamp } = event.data
121
+ // Method calls are always sent one-at-a-time (each has its own postMessage response), so only
122
+ // the first entry is ever populated -- but the payload shape stays consistent with every other
123
+ // iframe message type (timing/vitals/ajax), which all wrap their data in an `entries` array.
124
+ const [{ method, args } = {}] = event.data.entries || []
125
+ const output = { entity: null, result: null }
126
+ // Registration of a new entity needs to be handled differently than method calls on existing entities
127
+ if (method === REGISTER) {
128
+ // "null" is the opaque-origin serialization (sandboxed iframes, data:/javascript: URLs) -- it
129
+ // doesn't identify a single origin, so an entity registered from one can never be safely
130
+ // distinguished from another later on (see isValidOrigin). Reject up front rather than
131
+ // registering an entity that would silently fail every subsequent call.
132
+ if (event.origin === 'null') {
133
+ warn(74, event.origin)
134
+ return output
135
+ }
136
+
137
+ const iframeDomains = agent.init.api.register.iframe_domains
138
+ if (iframeDomains.length && !iframeDomains.includes(event.origin)) {
139
+ warn(74, event.origin)
140
+ return output
141
+ }
142
+ const targetData = args?.[0] || target || {}
143
+ if (!agent[REGISTER]) {
144
+ warn(35, REGISTER)
145
+ return output
146
+ }
147
+
148
+ const freshTarget = {
149
+ id: targetData.id,
150
+ name: targetData.name,
151
+ type: targetData.type,
152
+ version: targetData.version,
153
+ tags: targetData.tags
154
+ }
155
+
156
+ output.entity = agent[REGISTER](freshTarget)
157
+ output.entity.metadata.target.iframeInterfaceId = iframeInterfaceId
158
+ output.entity.metadata.target.iframeOrigin = event.origin
159
+
160
+ return output
161
+ }
162
+
163
+ const entity = getRegisteredEntityByIframeInterfaceId(iframeInterfaceId, agent)
164
+ if (!isValidOrigin(event, entity)) {
165
+ warn(76)
166
+ return output
167
+ }
168
+
169
+ output.entity = entity
170
+ const methodFn = entity[method]
171
+ if (!isSafeProperty(entity, method) || typeof methodFn !== 'function') {
172
+ warn(35, method)
173
+ return output
174
+ }
175
+
176
+ // Supply the timestamp captured inside the iframe at call time (from event.data.timestamp)
177
+ entity.metadata.events.latestTimestamp = timestamp
178
+ output.result = await methodFn.apply(entity, args || [])
179
+ return output
180
+ }
181
+
182
+ /**
183
+ * Serializes entity metadata for postMessage response.
184
+ * Strips out any non-serializable properties and returns a plain object.
185
+ * postMessage cant handle complex objects that cause DataCloneErrors to throw like functions, circular references, etc.
186
+ * @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.
187
+ * @returns {Object} - The serialized metadata object
188
+ */
189
+ function serializeMetadata (entity) {
190
+ const meta = entity.metadata
191
+
192
+ return JSON.parse(stringify(meta))
193
+ }
194
+
195
+ /**
196
+ * Sends a postMessage response to the iframe
197
+ * @param {MessageEvent} event - The message event containing the method call or registration request
198
+ * @param {Object} payload - The response payload to send back to the iframe
199
+ */
200
+ function sendResponse (event, payload) {
201
+ event.source.postMessage({
202
+ type: IFRAME_API_RESPONSE,
203
+ messageId: event.data.messageId,
204
+ iframeInterfaceId: event.data.iframeInterfaceId,
205
+ ...payload
206
+ }, event.origin)
207
+ }
208
+
209
+ /**
210
+ * Sets up a postMessage listener to handle API calls and timing updates from iframes
211
+ * @param {Object} agent The agent instance
212
+ */
213
+ export function setupIframeMFEMessageListener (agent) {
214
+ if (!isBrowserScope || !window.addEventListener || agent.runtime.listeningForIframeMessages) return
215
+
216
+ registerHandler('iframe-message', async (event) => {
217
+ if (!event.data) return
218
+
219
+ switch (event.data.type) {
220
+ case IFRAME_TIMING_UPDATE:
221
+ handleTimingUpdate(event, agent)
222
+ return
223
+ case IFRAME_VITALS_UPDATE:
224
+ handleVitalsUpdate(event, agent)
225
+ return
226
+ case IFRAME_AJAX:
227
+ handleAjax(event, agent)
228
+ return
229
+ case IFRAME_API:
230
+ try {
231
+ if (!event.source || !event.data.messageId || !event.data.entries?.[0]?.method) return
232
+ const { entity, result } = await handleMethodCall(event, agent)
233
+ const metadata = entity ? serializeMetadata(entity) : undefined
234
+ sendResponse(event, { result, metadata })
235
+ } catch (error) {
236
+ const errorMessage = error?.message || String(error)
237
+ sendResponse(event, { error: errorMessage })
238
+ }
239
+ }
240
+ }, 'IFRAME', agent.ee)
241
+
242
+ drain(agent, 'IFRAME', true) // drain any buffered iframe messages that were received before the listener was set up
243
+ agent.runtime.listeningForIframeMessages = true
244
+ }