@newrelic/browser-agent 1.311.0 → 1.312.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 (81) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/common/constants/agent-constants.js +4 -5
  3. package/dist/cjs/common/constants/env.cdn.js +2 -2
  4. package/dist/cjs/common/constants/env.npm.js +2 -2
  5. package/dist/cjs/common/util/script-tracker.js +2 -0
  6. package/dist/cjs/common/util/stringify.js +6 -21
  7. package/dist/cjs/common/util/v2.js +90 -6
  8. package/dist/cjs/common/wrap/wrap-fetch.js +10 -5
  9. package/dist/cjs/common/wrap/wrap-function.js +17 -9
  10. package/dist/cjs/common/wrap/wrap-logger.js +6 -4
  11. package/dist/cjs/common/wrap/wrap-xhr.js +3 -1
  12. package/dist/cjs/features/ajax/aggregate/index.js +24 -6
  13. package/dist/cjs/features/ajax/instrument/index.js +12 -10
  14. package/dist/cjs/features/generic_events/instrument/index.js +3 -3
  15. package/dist/cjs/features/logging/aggregate/index.js +12 -7
  16. package/dist/cjs/features/logging/instrument/index.js +6 -4
  17. package/dist/cjs/features/logging/shared/utils.js +4 -4
  18. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +6 -3
  19. package/dist/cjs/loaders/api/register.js +24 -23
  20. package/dist/cjs/loaders/api/wrapLogger.js +2 -2
  21. package/dist/esm/common/constants/agent-constants.js +4 -5
  22. package/dist/esm/common/constants/env.cdn.js +2 -2
  23. package/dist/esm/common/constants/env.npm.js +2 -2
  24. package/dist/esm/common/util/script-tracker.js +2 -2
  25. package/dist/esm/common/util/stringify.js +6 -21
  26. package/dist/esm/common/util/v2.js +86 -6
  27. package/dist/esm/common/wrap/wrap-fetch.js +10 -5
  28. package/dist/esm/common/wrap/wrap-function.js +17 -9
  29. package/dist/esm/common/wrap/wrap-logger.js +6 -4
  30. package/dist/esm/common/wrap/wrap-xhr.js +3 -1
  31. package/dist/esm/features/ajax/aggregate/index.js +24 -6
  32. package/dist/esm/features/ajax/instrument/index.js +12 -10
  33. package/dist/esm/features/generic_events/instrument/index.js +3 -3
  34. package/dist/esm/features/logging/aggregate/index.js +13 -8
  35. package/dist/esm/features/logging/instrument/index.js +6 -4
  36. package/dist/esm/features/logging/shared/utils.js +4 -4
  37. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +6 -3
  38. package/dist/esm/loaders/api/register.js +24 -23
  39. package/dist/esm/loaders/api/wrapLogger.js +2 -2
  40. package/dist/types/common/constants/agent-constants.d.ts.map +1 -1
  41. package/dist/types/common/util/script-tracker.d.ts +11 -0
  42. package/dist/types/common/util/script-tracker.d.ts.map +1 -1
  43. package/dist/types/common/util/stringify.d.ts.map +1 -1
  44. package/dist/types/common/util/v2.d.ts +37 -0
  45. package/dist/types/common/util/v2.d.ts.map +1 -1
  46. package/dist/types/common/wrap/wrap-fetch.d.ts +1 -1
  47. package/dist/types/common/wrap/wrap-fetch.d.ts.map +1 -1
  48. package/dist/types/common/wrap/wrap-function.d.ts +1 -1
  49. package/dist/types/common/wrap/wrap-function.d.ts.map +1 -1
  50. package/dist/types/common/wrap/wrap-logger.d.ts +1 -1
  51. package/dist/types/common/wrap/wrap-logger.d.ts.map +1 -1
  52. package/dist/types/common/wrap/wrap-xhr.d.ts +1 -1
  53. package/dist/types/common/wrap/wrap-xhr.d.ts.map +1 -1
  54. package/dist/types/features/ajax/aggregate/index.d.ts +2 -1
  55. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  56. package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
  57. package/dist/types/features/logging/aggregate/index.d.ts.map +1 -1
  58. package/dist/types/features/logging/instrument/index.d.ts.map +1 -1
  59. package/dist/types/features/logging/shared/utils.d.ts +2 -2
  60. package/dist/types/features/logging/shared/utils.d.ts.map +1 -1
  61. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +1 -0
  62. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  63. package/dist/types/loaders/api/register.d.ts.map +1 -1
  64. package/package.json +2 -2
  65. package/src/common/constants/agent-constants.js +4 -5
  66. package/src/common/util/script-tracker.js +2 -2
  67. package/src/common/util/stringify.js +6 -23
  68. package/src/common/util/v2.js +84 -7
  69. package/src/common/wrap/wrap-fetch.js +12 -5
  70. package/src/common/wrap/wrap-function.js +16 -9
  71. package/src/common/wrap/wrap-logger.js +6 -4
  72. package/src/common/wrap/wrap-xhr.js +3 -1
  73. package/src/features/ajax/aggregate/index.js +22 -6
  74. package/src/features/ajax/instrument/index.js +13 -10
  75. package/src/features/generic_events/instrument/index.js +3 -3
  76. package/src/features/logging/aggregate/index.js +13 -14
  77. package/src/features/logging/instrument/index.js +6 -4
  78. package/src/features/logging/shared/utils.js +4 -4
  79. package/src/features/soft_navigations/aggregate/ajax-node.js +6 -3
  80. package/src/loaders/api/register.js +19 -12
  81. package/src/loaders/api/wrapLogger.js +2 -2
@@ -54,7 +54,7 @@ if (globalScope.PerformanceObserver?.supportedEntryTypes.includes('resource')) {
54
54
  * @param {string} stack The error stack trace
55
55
  * @returns {string[]} Array of cleaned URLs found in the stack trace
56
56
  */
57
- function extractUrlsFromStack (stack) {
57
+ export function extractUrlsFromStack (stack) {
58
58
  if (!stack || typeof stack !== 'string') return []
59
59
 
60
60
  const urls = new Set()
@@ -80,7 +80,7 @@ function extractUrlsFromStack (stack) {
80
80
  * Returns a deep stack trace by temporarily increasing the stack trace limit.
81
81
  * @returns {Error.stack | undefined}
82
82
  */
83
- function getDeepStackTrace () {
83
+ export function getDeepStackTrace () {
84
84
  let stack
85
85
  try {
86
86
  const originalStackLimit = Error.stackTraceLimit
@@ -7,35 +7,18 @@ import { ee } from '../event-emitter/contextual-ee'
7
7
 
8
8
  /**
9
9
  * Returns a function for use as a replacer parameter in JSON.stringify() to handle circular references.
10
- * Uses an array to track the current ancestor chain, allowing the same object to appear
11
- * multiple times in the structure as long as it's not a circular reference.
12
10
  * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value MDN - Cyclical object value}
13
- * @returns {Function} A function that filters out circular references while allowing duplicate references.
11
+ * @returns {Function} A function that filters out values it has seen before.
14
12
  */
15
13
  const getCircularReplacer = () => {
16
- const stack = []
17
-
18
- return function (key, value) {
19
- if (stack.length > 0) {
20
- // Find where we are in the stack
21
- const thisPos = stack.indexOf(this)
22
- if (~thisPos) {
23
- // We're still in the stack, trim it
24
- stack.splice(thisPos + 1)
25
- } else {
26
- // We're not in the stack, add ourselves
27
- stack.push(this)
28
- }
29
-
30
- // Check if value is in the current ancestor chain
31
- if (~stack.indexOf(value)) {
14
+ const seen = new WeakSet()
15
+ return (key, value) => {
16
+ if (typeof value === 'object' && value !== null) {
17
+ if (seen.has(value)) {
32
18
  return
33
19
  }
34
- } else {
35
- // First call, initialize with root
36
- stack.push(value)
20
+ seen.add(value)
37
21
  }
38
-
39
22
  return value
40
23
  }
41
24
  }
@@ -3,6 +3,8 @@
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
 
6
+ import { extractUrlsFromStack, getDeepStackTrace } from './script-tracker'
7
+
6
8
  /**
7
9
  * @enum {string}
8
10
  * @readonly
@@ -14,6 +16,30 @@ export const V2_TYPES = {
14
16
  BA: 'BA'
15
17
  }
16
18
 
19
+ /**
20
+ * Returns the registered target associated with a given ID. Returns an empty array if no target is found.
21
+ * @param {string|number} id
22
+ * @param {*} agentRef the agent reference
23
+ * @returns {import("../../interfaces/registered-entity").RegisterAPIMetadataTarget[]}
24
+ */
25
+ export function getRegisteredTargetsFromId (id, agentRef) {
26
+ if (!id || !agentRef?.init.api.allow_registered_children) return []
27
+ const registeredEntities = agentRef.runtime.registeredEntities
28
+ return registeredEntities?.filter(entity => String(entity.metadata.target.id) === String(id)).map(entity => entity.metadata.target) || []
29
+ }
30
+
31
+ /**
32
+ * Returns the registered target(s) associated with a given filename if found in the resource timing API during registration. Returns an empty array if no target is found.
33
+ * @param {string} filename
34
+ * @param {*} agentRef
35
+ * @returns {import("../../interfaces/registered-entity").RegisterAPIMetadataTarget[]}
36
+ */
37
+ export function getRegisteredTargetsFromFilename (filename, agentRef) {
38
+ if (!filename || !agentRef?.init.api.allow_registered_children) return []
39
+ const registeredEntities = agentRef.runtime.registeredEntities
40
+ return registeredEntities?.filter(entity => entity.metadata.timings?.asset?.endsWith(filename)).map(entity => entity.metadata.target) || []
41
+ }
42
+
17
43
  /**
18
44
  * When given a valid target, returns an object with the V2 payload attributes. Returns an empty object otherwise.
19
45
  * @note Field names may change as the schema is finalized
@@ -23,7 +49,7 @@ export const V2_TYPES = {
23
49
  * @returns {Object} returns an empty object if args are not supplied or the aggregate instance is not supporting version 2
24
50
  */
25
51
  export function getVersion2Attributes (target, aggregateInstance) {
26
- if (aggregateInstance?.harvestEndpointVersion !== 2) return {}
52
+ if (!supportsV2(aggregateInstance)) return {}
27
53
  const containerAgentEntityGuid = aggregateInstance.agentRef.runtime.appMetadata.agents[0].entityGuid
28
54
  /** if there's no target, but we are in v2 mode, this means the data belongs to the container agent */
29
55
  if (!target) {
@@ -33,11 +59,62 @@ export function getVersion2Attributes (target, aggregateInstance) {
33
59
  }
34
60
  }
35
61
  /** otherwise, the data belongs to the target (MFE) and should be attributed as such */
36
- return {
37
- 'source.id': target.id,
38
- 'source.name': target.name,
39
- 'source.type': target.type,
40
- 'parent.id': target.parent?.id || containerAgentEntityGuid,
41
- 'parent.type': target.parent?.type || V2_TYPES.BA
62
+ return target.attributes
63
+ }
64
+
65
+ /**
66
+ * Returns the attributes used for duplicating data in version 2 of the harvest endpoint.
67
+ * If not valid for duplication, returns an empty object.
68
+ * @note BEST PRACTICE - Caller should call shouldDuplicate() before utilizing this method to determine if duplication attributes should be added to the event.
69
+ * @param {import("../../interfaces/registered-entity").RegisterAPIMetadataTarget} target
70
+ * @param {*} aggregateInstance the aggregate instance calling the method
71
+ * @returns {Object}
72
+ */
73
+ export function getVersion2DuplicationAttributes (target, aggregateInstance) {
74
+ if (!shouldDuplicate(target, aggregateInstance)) return {}
75
+ return { 'child.id': target.id, 'child.type': target.type, ...getVersion2Attributes(undefined, aggregateInstance) }
76
+ }
77
+
78
+ /**
79
+ * Determines if an event should be duplicated for a given target and aggregate instance. This is used to determine if duplication attributes should be added to an event and if the event should be sent to the soft nav feature for evaluation.
80
+ * @note This method is intended to be used in conjunction with getVersion2DuplicationAttributes and should be called before it to determine if duplication attributes should be added to an event.
81
+ * @param {import("../../interfaces/registered-entity").RegisterAPIMetadataTarget} target
82
+ * @param {*} aggregateInstance The aggregate instance calling the method. This is needed to check if duplication is enabled and if the harvest endpoint version supports it.
83
+ * @returns {boolean} returns true if the event should be duplicated for the target, false otherwise
84
+ */
85
+ export function shouldDuplicate (target, aggregateInstance) {
86
+ return !!target && !!supportsV2(aggregateInstance) && aggregateInstance.agentRef.init.api.duplicate_registered_data
87
+ }
88
+
89
+ /**
90
+ * Finds the registered targets from the stack trace for a given agent reference.
91
+ * @param {*} agentRef The agent reference to use for finding targets.
92
+ * @returns {Array} An array of targets found from the stack trace. If no targets are found or allowed, returns an array with undefined.
93
+ */
94
+ export function findTargetsFromStackTrace (agentRef) {
95
+ if (!agentRef?.init.api.allow_registered_children) return [undefined]
96
+
97
+ const targets = []
98
+ try {
99
+ var urls = extractUrlsFromStack(getDeepStackTrace())
100
+ let iterator = urls.length - 1
101
+ while (urls[iterator]) {
102
+ targets.push(...getRegisteredTargetsFromFilename(urls[iterator--], agentRef))
103
+ }
104
+ } catch (err) {
105
+ // Silent catch to prevent errors from propagating
42
106
  }
107
+ if (!targets.length) targets.push(undefined) // if we can't find any targets from the stack trace, return an array with undefined to signify the container agent is the target
108
+ return targets
109
+ }
110
+
111
+ /**
112
+ * Determines if the aggregate instance supports version 2 of the harvest endpoint. Nearly all the V2 logic "depends" on
113
+ * the harvest endpoint version, so this is the main gatekeeper method for whether or not V2 logic should be executed across the
114
+ * various functions in this module.
115
+ * @param {*} aggregateInstance The aggregate instance to check.
116
+ * @returns {boolean} Returns true if the aggregate instance supports version 2, false otherwise.
117
+ */
118
+ function supportsV2 (aggregateInstance) {
119
+ return aggregateInstance?.harvestEndpointVersion === 2
43
120
  }
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { ee as baseEE, contextId } from '../event-emitter/contextual-ee'
11
11
  import { globalScope } from '../constants/runtime'
12
+ import { findTargetsFromStackTrace } from '../util/v2'
12
13
 
13
14
  var prefix = 'fetch-'
14
15
  var bodyPrefix = prefix + 'body-'
@@ -27,7 +28,7 @@ const wrapped = {}
27
28
  * event emitter will be based.
28
29
  * @returns {Object} Scoped event emitter with a debug ID of `fetch`.
29
30
  */
30
- export function wrapFetch (sharedEE) {
31
+ export function wrapFetch (sharedEE, agentRef) {
31
32
  const ee = scopedEE(sharedEE)
32
33
  if (!(Req && Res && globalScope.fetch)) {
33
34
  return ee
@@ -44,13 +45,16 @@ export function wrapFetch (sharedEE) {
44
45
  })
45
46
  wrapPromiseMethod(globalScope, 'fetch', prefix)
46
47
 
47
- ee.on(prefix + 'end', function (err, res) {
48
+ ee.on(prefix + 'end', function (err, res, targets) {
48
49
  var ctx = this
50
+ // undefined target reports to container
51
+ ctx.targets = targets || [undefined]
49
52
  if (res) {
50
53
  var size = res.headers.get('content-length')
51
54
  if (size !== null) {
52
55
  ctx.rxSize = size
53
56
  }
57
+
54
58
  ee.emit(prefix + 'done', [null, res], ctx)
55
59
  } else {
56
60
  ee.emit(prefix + 'done', [err], ctx)
@@ -67,11 +71,14 @@ export function wrapFetch (sharedEE) {
67
71
  */
68
72
  function wrapPromiseMethod (target, name, prefix) {
69
73
  var fn = target[name]
74
+
70
75
  if (typeof fn === 'function') {
71
76
  target[name] = function () {
72
77
  var args = [...arguments]
73
78
 
74
- var ctx = {}
79
+ const ctx = {}
80
+ const targets = findTargetsFromStackTrace(agentRef)
81
+
75
82
  // we are wrapping args in an array so we can preserve the reference
76
83
  ee.emit(prefix + 'before-start', [args], ctx)
77
84
  var dtPayload
@@ -83,10 +90,10 @@ export function wrapFetch (sharedEE) {
83
90
 
84
91
  // Note we need to cast the returned (orig) Promise from native APIs into the current global Promise, which may or may not be our WrappedPromise.
85
92
  return origPromiseFromFetch.then(function (val) {
86
- ee.emit(prefix + 'end', [null, val], origPromiseFromFetch)
93
+ ee.emit(prefix + 'end', [null, val, targets], origPromiseFromFetch)
87
94
  return val
88
95
  }, function (err) {
89
- ee.emit(prefix + 'end', [err], origPromiseFromFetch)
96
+ ee.emit(prefix + 'end', [err, undefined, targets], origPromiseFromFetch)
90
97
  throw err
91
98
  })
92
99
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
 
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { ee } from '../event-emitter/contextual-ee'
11
11
  import { bundleId } from '../ids/bundle-id'
12
+ import { findTargetsFromStackTrace } from '../util/v2'
12
13
 
13
14
  export const flag = `nr@original:${bundleId}`
14
15
  const LONG_TASK_THRESHOLD = 50
@@ -34,7 +35,7 @@ export default createWrapperWithEmitter
34
35
  * @param {boolean} always - If `true`, emit events even if already emitting an event.
35
36
  * @returns {function} The wrapped function.
36
37
  */
37
- export function createWrapperWithEmitter (emitter, always) {
38
+ export function createWrapperWithEmitter (emitter, always, agentRef) {
38
39
  emitter || (emitter = ee)
39
40
 
40
41
  wrapFn.inPlace = inPlace
@@ -55,9 +56,10 @@ export function createWrapperWithEmitter (emitter, always) {
55
56
  * @param {function|object} getContext - The function or object that will serve as the 'this' context for handlers of events emitted by this wrapper.
56
57
  * @param {string} methodName - The name of the method being wrapped.
57
58
  * @param {boolean} bubble - If true, emitted events should also bubble up to the old emitter upon which the `emitter` in the current scope was based (if it defines one).
59
+ * @param {boolean} [evaluateStack] - If true, the wrapper will attempt to evaluate the stack of the executed wrapped function to find targets of the execution (ex. the MFE source of a console.log).
58
60
  * @returns {function} The wrapped function.
59
61
  */
60
- function wrapFn (fn, prefix, getContext, methodName, bubble) {
62
+ function wrapFn (fn, prefix, getContext, methodName, bubble, evaluateStack) {
61
63
  // Unless fn is both wrappable and unwrapped, return it unchanged.
62
64
  if (notWrappable(fn)) return fn
63
65
 
@@ -78,11 +80,16 @@ export function createWrapperWithEmitter (emitter, always) {
78
80
  var ctx
79
81
  var result
80
82
  let thrownError
83
+ let targets
81
84
 
82
85
  try {
83
86
  originalThis = this
84
87
  args = [...arguments]
85
88
 
89
+ // certain wrappers can inform the function wrapper to evaluate the stack of the executed wrapped function to find targets of the execution
90
+ // (e.g. wrap-logger can inform this method to find try to find the MFE source of a console.log)
91
+ targets = evaluateStack ? findTargetsFromStackTrace(agentRef) : [undefined] // undefined target always maps to the container agent
92
+
86
93
  if (typeof getContext === 'function') {
87
94
  ctx = getContext(args, originalThis)
88
95
  } else {
@@ -93,7 +100,7 @@ export function createWrapperWithEmitter (emitter, always) {
93
100
  }
94
101
 
95
102
  // Warning: start events may mutate args!
96
- safeEmit(prefix + 'start', [args, originalThis, methodName], ctx, bubble)
103
+ safeEmit(prefix + 'start', [args, originalThis, methodName, targets], ctx, bubble)
97
104
 
98
105
  const fnStartTime = performance.now()
99
106
  let fnEndTime
@@ -103,7 +110,7 @@ export function createWrapperWithEmitter (emitter, always) {
103
110
  return result
104
111
  } catch (err) {
105
112
  fnEndTime = performance.now()
106
- safeEmit(prefix + 'err', [args, originalThis, err], ctx, bubble)
113
+ safeEmit(prefix + 'err', [args, originalThis, err, targets], ctx, bubble)
107
114
  // rethrow error so we don't effect execution by observing.
108
115
  thrownError = err
109
116
  throw thrownError
@@ -120,10 +127,10 @@ export function createWrapperWithEmitter (emitter, always) {
120
127
  }
121
128
  // standalone long task message
122
129
  if (task.isLongTask) {
123
- safeEmit('long-task', [task, originalThis], ctx, bubble)
130
+ safeEmit('long-task', [task, originalThis, targets], ctx, bubble)
124
131
  }
125
132
  // -end message also includes the task execution info
126
- safeEmit(prefix + 'end', [args, originalThis, result], ctx, bubble)
133
+ safeEmit(prefix + 'end', [args, originalThis, result, targets], ctx, bubble)
127
134
  }
128
135
  }
129
136
  }
@@ -139,7 +146,7 @@ export function createWrapperWithEmitter (emitter, always) {
139
146
  * @param {boolean} [bubble=false] If `true`, emitted events should also bubble up to the old emitter upon which
140
147
  * the `emitter` in the current scope was based (if it defines one).
141
148
  */
142
- function inPlace (obj, methods, prefix, getContext, bubble) {
149
+ function inPlace (obj, methods, prefix, getContext, bubble, evaluateStack) {
143
150
  if (!prefix) prefix = ''
144
151
 
145
152
  // If prefix starts with '-' set this boolean to add the method name to the prefix before passing each one to wrap.
@@ -152,7 +159,7 @@ export function createWrapperWithEmitter (emitter, always) {
152
159
  // Unless fn is both wrappable and unwrapped, bail so we don't add extra properties with undefined values.
153
160
  if (notWrappable(fn)) continue
154
161
 
155
- obj[method] = wrapFn(fn, (prependMethodPrefix ? method + prefix : prefix), getContext, method, bubble)
162
+ obj[method] = wrapFn(fn, (prependMethodPrefix ? method + prefix : prefix), getContext, method, bubble, evaluateStack)
156
163
  }
157
164
  }
158
165
 
@@ -24,10 +24,10 @@ const contextMap = new Map()
24
24
  * @returns {Object} Scoped event emitter with a debug ID of `logger`.
25
25
  */
26
26
  // eslint-disable-next-line
27
- export function wrapLogger(sharedEE, parent, loggerFn, context, autoCaptured = true) {
27
+ export function wrapLogger(sharedEE, parent, loggerFn, context, autoCaptured = true, agentRef) {
28
28
  if (!(typeof parent === 'object' && !!parent && typeof loggerFn === 'string' && !!loggerFn && typeof parent[loggerFn] === 'function')) return warn(29)
29
29
  const ee = scopedEE(sharedEE)
30
- const wrapFn = wfn(ee)
30
+ const wrapFn = wfn(ee, undefined, agentRef)
31
31
 
32
32
  /**
33
33
  * This section contains the context that will be shared across all invoked calls of the wrapped function,
@@ -41,8 +41,10 @@ export function wrapLogger(sharedEE, parent, loggerFn, context, autoCaptured = t
41
41
  const contextLookupKey = parent[loggerFn]?.[flag] || parent[loggerFn]
42
42
  contextMap.set(contextLookupKey, ctx)
43
43
 
44
- /** observe calls to <loggerFn> and emit events prefixed with `wrap-logger-` */
45
- wrapFn.inPlace(parent, [loggerFn], 'wrap-logger-', () => contextMap.get(contextLookupKey))
44
+ /** observe calls to <loggerFn> and emit events prefixed with `wrap-logger-`
45
+ * inform the inplace wrapper to evaluate the stack for targets of the log execution,
46
+ * so that logs can be attributed to a matching MFE source if being used. */
47
+ wrapFn.inPlace(parent, [loggerFn], 'wrap-logger-', () => contextMap.get(contextLookupKey), undefined, true)
46
48
 
47
49
  return ee
48
50
  }
@@ -14,6 +14,7 @@ import { eventListenerOpts } from '../event-listener/event-listener-opts'
14
14
  import { createWrapperWithEmitter as wfn } from './wrap-function'
15
15
  import { globalScope } from '../constants/runtime'
16
16
  import { warn } from '../util/console'
17
+ import { findTargetsFromStackTrace } from '../util/v2'
17
18
 
18
19
  const wrapped = {}
19
20
  const XHR_PROPS = ['open', 'send'] // these are the specific funcs being wrapped on all XMLHttpRequests(.prototype)
@@ -25,7 +26,7 @@ const XHR_PROPS = ['open', 'send'] // these are the specific funcs being wrapped
25
26
  * @returns {Object} Scoped event emitter with a debug ID of `xhr`.
26
27
  */
27
28
  // eslint-disable-next-line
28
- export function wrapXhr (sharedEE) {
29
+ export function wrapXhr (sharedEE, agentRef) {
29
30
  var baseEE = sharedEE || contextualEE
30
31
  const ee = scopedEE(baseEE)
31
32
 
@@ -54,6 +55,7 @@ export function wrapXhr (sharedEE) {
54
55
  function newXHR (opts) {
55
56
  const xhr = new OrigXHR(opts)
56
57
  const context = ee.context(xhr)
58
+ context.targets = findTargetsFromStackTrace(agentRef)
57
59
 
58
60
  try {
59
61
  ee.emit('new-xhr', [xhr], context)
@@ -12,6 +12,7 @@ import { AggregateBase } from '../../utils/aggregate-base'
12
12
  import { parseGQL } from './gql'
13
13
  import { nullable, numeric, getAddStringContext, addCustomAttributes } from '../../../common/serialize/bel-serializer'
14
14
  import { gosNREUMOriginals } from '../../../common/window/nreum'
15
+ import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/util/v2'
15
16
 
16
17
  export class Aggregate extends AggregateBase {
17
18
  static featureName = FEATURE_NAME
@@ -30,8 +31,8 @@ export class Aggregate extends AggregateBase {
30
31
 
31
32
  registerHandler('returnAjax', event => this.events.add(event), this.featureName, this.ee)
32
33
 
33
- registerHandler('xhr', function () { // the EE-drain system not only switches "this" but also passes a new EventContext with info. Should consider platform refactor to another system which passes a mutable context around separately and predictably to avoid problems like this.
34
- classThis.storeXhr(...arguments, this) // this switches the context back to the class instance while passing the NR context as an argument -- see "ctx" in storeXhr
34
+ registerHandler('xhr', function (params, metrics, startTime, endTime, type, target) { // the EE-drain system not only switches "this" but also passes a new EventContext with info. Should consider platform refactor to another system which passes a mutable context around separately and predictably to avoid problems like this.
35
+ classThis.storeXhr(params, metrics, startTime, endTime, type, target, this) // this switches the context back to the class instance while passing the NR context as an argument -- see "ctx" in storeXhr
35
36
  }, this.featureName, this.ee)
36
37
 
37
38
  this.ee.on('long-task', (task, originator) => {
@@ -44,7 +45,7 @@ export class Aggregate extends AggregateBase {
44
45
  this.waitForFlags(([])).then(() => this.drain())
45
46
  }
46
47
 
47
- storeXhr (params, metrics, startTime, endTime, type, ctx) {
48
+ storeXhr (params, metrics, startTime, endTime, type, target, ctx) {
48
49
  metrics.time = startTime
49
50
 
50
51
  // send to session traces
@@ -108,11 +109,21 @@ export class Aggregate extends AggregateBase {
108
109
  })
109
110
  if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length)
110
111
 
112
+ /** make a copy of the event for the MFE target if it exists */
113
+ if (target) {
114
+ this.events.add({ ...event, targetAttributes: getVersion2Attributes(target, this) })
115
+ if (shouldDuplicate(target, this)) this.reportContainerEvent({ ...event, targetAttributes: getVersion2DuplicationAttributes(target, this) }, ctx)
116
+ } else {
117
+ this.reportContainerEvent(event, ctx)
118
+ }
119
+ }
120
+
121
+ reportContainerEvent (evt, ctx) {
111
122
  const softNavInUse = Boolean(this.agentRef.features?.[FEATURE_NAMES.softNav])
112
123
  if (softNavInUse) { // when SN is running, pass the event w/ info to it for evaluation -- either part of an interaction or is given back
113
- handle('ajax', [event, ctx], undefined, FEATURE_NAMES.softNav, this.ee)
124
+ handle('ajax', [evt, ctx], undefined, FEATURE_NAMES.softNav, this.ee)
114
125
  } else {
115
- this.events.add(event)
126
+ this.events.add(evt)
116
127
  }
117
128
  }
118
129
 
@@ -154,7 +165,12 @@ export class Aggregate extends AggregateBase {
154
165
 
155
166
  // add custom attributes
156
167
  // gql decorators are added as custom attributes to alleviate need for new BEL schema
157
- const attrParts = addCustomAttributes({ ...(jsAttributes || {}), ...(event.gql || {}) }, addString)
168
+ const attrParts = addCustomAttributes({
169
+ ...(jsAttributes || {}),
170
+ ...(event.gql || {}),
171
+ ...(event.targetAttributes || {}) // used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
172
+ }, addString)
173
+
158
174
  fields.unshift(numeric(attrParts.length))
159
175
 
160
176
  insert += fields.join(',')
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
  import { gosNREUMOriginals } from '../../../common/window/nreum'
@@ -59,8 +59,8 @@ export class Instrument extends InstrumentBase {
59
59
  // do nothing
60
60
  }
61
61
 
62
- wrapFetch(this.ee)
63
- wrapXhr(this.ee)
62
+ wrapFetch(this.ee, agentRef)
63
+ wrapXhr(this.ee, agentRef)
64
64
  subscribeToEvents(agentRef, this.ee, this.handler, this.dt)
65
65
 
66
66
  this.importAggregator(agentRef, () => import(/* webpackChunkName: "ajax-aggregate" */ '../aggregate/index.js'))
@@ -314,14 +314,11 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
314
314
  this.startTime = now()
315
315
  this.dt = dtPayload
316
316
 
317
- if (fetchArguments.length >= 1) this.target = fetchArguments[0]
318
- if (fetchArguments.length >= 2) this.opts = fetchArguments[1]
317
+ let [target, opts = {}] = fetchArguments
319
318
 
320
- var opts = this.opts || {}
321
- var target = this.target
322
319
  addUrl(this, extractUrl(target))
323
320
 
324
- var method = ('' + ((target && target instanceof origRequest && target.method) ||
321
+ const method = ('' + ((target && target instanceof origRequest && target.method) ||
325
322
  opts.method || 'GET')).toUpperCase()
326
323
  this.params.method = method
327
324
  this.body = opts.body
@@ -350,7 +347,8 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
350
347
  duration: now() - this.startTime
351
348
  }
352
349
 
353
- handler('xhr', [this.params, metrics, this.startTime, this.endTime, 'fetch'], this, FEATURE_NAMES.ajax)
350
+ const payload = [this.params, metrics, this.startTime, this.endTime, 'fetch']
351
+ this.targets.forEach(target => reportToAgg(payload, this, target))
354
352
  }
355
353
 
356
354
  // Create report for XHR request that has finished
@@ -377,7 +375,12 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
377
375
  // Always send cbTime, even if no noticeable time was taken.
378
376
  metrics.cbTime = this.cbTime
379
377
 
380
- handler('xhr', [params, metrics, this.startTime, this.endTime, 'xhr'], this, FEATURE_NAMES.ajax)
378
+ const payload = [params, metrics, this.startTime, this.endTime, 'xhr']
379
+ this.targets.forEach(target => reportToAgg(payload, this, target))
380
+ }
381
+
382
+ function reportToAgg (payload, context, target) {
383
+ handler('xhr', [...payload, target], context, FEATURE_NAMES.ajax)
381
384
  }
382
385
 
383
386
  function captureXhrData (ctx, xhr) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
 
@@ -48,8 +48,8 @@ export class Instrument extends InstrumentBase {
48
48
  let historyEE, websocketsEE
49
49
  if (websocketsEnabled) websocketsEE = wrapWebSocket(this.ee)
50
50
  if (isBrowserScope) {
51
- wrapFetch(this.ee)
52
- wrapXhr(this.ee)
51
+ wrapFetch(this.ee, agentRef)
52
+ wrapXhr(this.ee, agentRef)
53
53
  historyEE = wrapHistory(this.ee)
54
54
 
55
55
  if (agentRef.init.user_actions.enabled) {
@@ -13,7 +13,7 @@ import { applyFnToProps } from '../../../common/util/traverse'
13
13
  import { SESSION_EVENT_TYPES, SESSION_EVENTS } from '../../../common/session/constants'
14
14
  import { ABORT_REASONS } from '../../session_replay/constants'
15
15
  import { canEnableSessionTracking } from '../../utils/feature-gates'
16
- import { getVersion2Attributes } from '../../../common/util/v2'
16
+ import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/util/v2'
17
17
 
18
18
  const LOGGING_EVENT = 'Logging/Event/'
19
19
 
@@ -73,12 +73,6 @@ export class Aggregate extends AggregateBase {
73
73
 
74
74
  if (!attributes || typeof attributes !== 'object') attributes = {}
75
75
 
76
- attributes = {
77
- ...attributes,
78
- /** Specific attributes only supplied if harvesting to endpoint version 2 */
79
- ...(getVersion2Attributes(target, this))
80
- }
81
-
82
76
  if (typeof level === 'string') level = level.toUpperCase()
83
77
  if (!isValidLogLevel(level)) return warn(30, level)
84
78
  if (modeForThisLog < (LOGGING_MODE[level] || Infinity)) {
@@ -104,14 +98,19 @@ export class Aggregate extends AggregateBase {
104
98
  }
105
99
  if (typeof message !== 'string' || !message) return warn(32)
106
100
 
107
- const log = new Log(
108
- Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp)),
109
- message,
110
- attributes,
111
- level
112
- )
101
+ const addEvent = (attributes) => {
102
+ const log = new Log(
103
+ Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp)),
104
+ message,
105
+ attributes,
106
+ level
107
+ )
108
+
109
+ if (this.events.add(log)) this.reportSupportabilityMetric(LOGGING_EVENT + (autoCaptured ? 'Auto' : 'API') + '/Added')
110
+ }
113
111
 
114
- if (this.events.add(log)) this.reportSupportabilityMetric(LOGGING_EVENT + (autoCaptured ? 'Auto' : 'API') + '/Added')
112
+ addEvent({ ...attributes, ...getVersion2Attributes(target, this) })
113
+ if (shouldDuplicate(target, this)) addEvent({ ...attributes, ...getVersion2DuplicationAttributes(target, this) })
115
114
  }
116
115
 
117
116
  serializer (eventBuffer) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
  import { InstrumentBase } from '../../utils/instrument-base'
@@ -27,13 +27,15 @@ export class Instrument extends InstrumentBase {
27
27
 
28
28
  globals.forEach((method) => {
29
29
  isNative(globalScope.console[method])
30
- wrapLogger(instanceEE, globalScope.console, method, { level: method === 'log' ? 'info' : method })
30
+ wrapLogger(instanceEE, globalScope.console, method, { level: method === 'log' ? 'info' : method }, undefined, agentRef)
31
31
  })
32
32
 
33
33
  /** emitted by wrap-logger function */
34
- this.ee.on('wrap-logger-end', function handleLog ([message]) {
34
+ this.ee.on('wrap-logger-end', function handleLog ([message], _, __, targets = []) {
35
35
  const { level, customAttributes, autoCaptured } = this
36
- bufferLog(instanceEE, message, customAttributes, level, autoCaptured)
36
+ targets.forEach(target => {
37
+ bufferLog(instanceEE, message, customAttributes, level, autoCaptured, target)
38
+ })
37
39
  })
38
40
  this.importAggregator(agentRef, () => import(/* webpackChunkName: "logging-aggregate" */ '../aggregate'))
39
41
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
  import { handle } from '../../../common/event-emitter/handle'
@@ -14,11 +14,11 @@ import { LOGGING_EVENT_EMITTER_CHANNEL, LOG_LEVELS } from '../constants'
14
14
  * @param {{[key: string]: *}} customAttributes - The log's custom attributes if any
15
15
  * @param {enum} level - the log level enum
16
16
  * @param {boolean} [autoCaptured=true] - True if log was captured from auto wrapping. False if it was captured from the API manual usage.
17
- * @param {object=} target - the optional target provided by an api call
17
+ * @param {object=} targets - the optional targets found
18
18
  */
19
- export function bufferLog (ee, message, customAttributes = {}, level = LOG_LEVELS.INFO, autoCaptured = true, target, timestamp = now()) {
19
+ export function bufferLog (ee, message, customAttributes = {}, level = LOG_LEVELS.INFO, autoCaptured = true, targets, timestamp = now()) {
20
20
  handle(SUPPORTABILITY_METRIC_CHANNEL, [`API/logging/${level.toLowerCase()}/called`], undefined, FEATURE_NAMES.metrics, ee)
21
- handle(LOGGING_EVENT_EMITTER_CHANNEL, [timestamp, message, customAttributes, level, autoCaptured, target], undefined, FEATURE_NAMES.logging, ee)
21
+ handle(LOGGING_EVENT_EMITTER_CHANNEL, [timestamp, message, customAttributes, level, autoCaptured, targets], undefined, FEATURE_NAMES.logging, ee)
22
22
  }
23
23
 
24
24
  /**