Skip to content

Commit c7558a0

Browse files
feat: Automatically Detect MFE Logs (#1721)
1 parent 7d37ede commit c7558a0

17 files changed

Lines changed: 232 additions & 64 deletions

File tree

src/common/util/v2.js

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const V2_TYPES = {
1717
}
1818

1919
/**
20-
* Returns the registered target associated with a given ID. Returns undefined if not found.
20+
* Returns the registered target associated with a given ID. Returns an empty array if no target is found.
2121
* @param {string|number} id
2222
* @param {*} agentRef the agent reference
2323
* @returns {import("../../interfaces/registered-entity").RegisterAPIMetadataTarget[]}
@@ -29,7 +29,7 @@ export function getRegisteredTargetsFromId (id, agentRef) {
2929
}
3030

3131
/**
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 not found.
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.
3333
* @param {string} filename
3434
* @param {*} agentRef
3535
* @returns {import("../../interfaces/registered-entity").RegisterAPIMetadataTarget[]}
@@ -49,7 +49,7 @@ export function getRegisteredTargetsFromFilename (filename, agentRef) {
4949
* @returns {Object} returns an empty object if args are not supplied or the aggregate instance is not supporting version 2
5050
*/
5151
export function getVersion2Attributes (target, aggregateInstance) {
52-
if (aggregateInstance?.harvestEndpointVersion !== 2) return {}
52+
if (!supportsV2(aggregateInstance)) return {}
5353
const containerAgentEntityGuid = aggregateInstance.agentRef.runtime.appMetadata.agents[0].entityGuid
5454
/** if there's no target, but we are in v2 mode, this means the data belongs to the container agent */
5555
if (!target) {
@@ -63,22 +63,36 @@ export function getVersion2Attributes (target, aggregateInstance) {
6363
}
6464

6565
/**
66-
* Returns the attributes used for duplicating data in version 2 of the harvest endpoint. If not valid for duplication, returns an empty object.
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.
6769
* @param {import("../../interfaces/registered-entity").RegisterAPIMetadataTarget} target
6870
* @param {*} aggregateInstance the aggregate instance calling the method
6971
* @returns {Object}
7072
*/
7173
export function getVersion2DuplicationAttributes (target, aggregateInstance) {
72-
if (aggregateInstance?.harvestEndpointVersion !== 2 || !shouldDuplicate(target, aggregateInstance?.agentRef)) return {}
74+
if (!shouldDuplicate(target, aggregateInstance)) return {}
7375
return { 'child.id': target.id, 'child.type': target.type, ...getVersion2Attributes(undefined, aggregateInstance) }
7476
}
7577

76-
export function shouldDuplicate (target, agentRef) {
77-
return !!target && agentRef.init.api.duplicate_registered_data
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
7887
}
7988

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+
*/
8094
export function findTargetsFromStackTrace (agentRef) {
81-
if (!agentRef?.init.api.allow_registered_children) return []
95+
if (!agentRef?.init.api.allow_registered_children) return [undefined]
8296

8397
const targets = []
8498
try {
@@ -90,5 +104,17 @@ export function findTargetsFromStackTrace (agentRef) {
90104
} catch (err) {
91105
// Silent catch to prevent errors from propagating
92106
}
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
93108
return targets
94109
}
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
120+
}

src/common/wrap/wrap-fetch.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,6 @@ export function wrapFetch (sharedEE, agentRef) {
7878

7979
const ctx = {}
8080
const targets = findTargetsFromStackTrace(agentRef)
81-
// undefined target reports to container
82-
if (!targets.length) targets.push(undefined)
8381

8482
// we are wrapping args in an array so we can preserve the reference
8583
ee.emit(prefix + 'before-start', [args], ctx)

src/common/wrap/wrap-function.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import { ee } from '../event-emitter/contextual-ee'
1111
import { bundleId } from '../ids/bundle-id'
12+
import { findTargetsFromStackTrace } from '../util/v2'
1213

1314
export const flag = `nr@original:${bundleId}`
1415
const LONG_TASK_THRESHOLD = 50
@@ -34,7 +35,7 @@ export default createWrapperWithEmitter
3435
* @param {boolean} always - If `true`, emit events even if already emitting an event.
3536
* @returns {function} The wrapped function.
3637
*/
37-
export function createWrapperWithEmitter (emitter, always) {
38+
export function createWrapperWithEmitter (emitter, always, agentRef) {
3839
emitter || (emitter = ee)
3940

4041
wrapFn.inPlace = inPlace
@@ -55,9 +56,10 @@ export function createWrapperWithEmitter (emitter, always) {
5556
* @param {function|object} getContext - The function or object that will serve as the 'this' context for handlers of events emitted by this wrapper.
5657
* @param {string} methodName - The name of the method being wrapped.
5758
* @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).
5860
* @returns {function} The wrapped function.
5961
*/
60-
function wrapFn (fn, prefix, getContext, methodName, bubble) {
62+
function wrapFn (fn, prefix, getContext, methodName, bubble, evaluateStack) {
6163
// Unless fn is both wrappable and unwrapped, return it unchanged.
6264
if (notWrappable(fn)) return fn
6365

@@ -78,11 +80,16 @@ export function createWrapperWithEmitter (emitter, always) {
7880
var ctx
7981
var result
8082
let thrownError
83+
let targets
8184

8285
try {
8386
originalThis = this
8487
args = [...arguments]
8588

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+
8693
if (typeof getContext === 'function') {
8794
ctx = getContext(args, originalThis)
8895
} else {
@@ -93,7 +100,7 @@ export function createWrapperWithEmitter (emitter, always) {
93100
}
94101

95102
// 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)
97104

98105
const fnStartTime = performance.now()
99106
let fnEndTime
@@ -103,7 +110,7 @@ export function createWrapperWithEmitter (emitter, always) {
103110
return result
104111
} catch (err) {
105112
fnEndTime = performance.now()
106-
safeEmit(prefix + 'err', [args, originalThis, err], ctx, bubble)
113+
safeEmit(prefix + 'err', [args, originalThis, err, targets], ctx, bubble)
107114
// rethrow error so we don't effect execution by observing.
108115
thrownError = err
109116
throw thrownError
@@ -120,10 +127,10 @@ export function createWrapperWithEmitter (emitter, always) {
120127
}
121128
// standalone long task message
122129
if (task.isLongTask) {
123-
safeEmit('long-task', [task, originalThis], ctx, bubble)
130+
safeEmit('long-task', [task, originalThis, targets], ctx, bubble)
124131
}
125132
// -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)
127134
}
128135
}
129136
}
@@ -139,7 +146,7 @@ export function createWrapperWithEmitter (emitter, always) {
139146
* @param {boolean} [bubble=false] If `true`, emitted events should also bubble up to the old emitter upon which
140147
* the `emitter` in the current scope was based (if it defines one).
141148
*/
142-
function inPlace (obj, methods, prefix, getContext, bubble) {
149+
function inPlace (obj, methods, prefix, getContext, bubble, evaluateStack) {
143150
if (!prefix) prefix = ''
144151

145152
// 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) {
152159
// Unless fn is both wrappable and unwrapped, bail so we don't add extra properties with undefined values.
153160
if (notWrappable(fn)) continue
154161

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)
156163
}
157164
}
158165

src/common/wrap/wrap-logger.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ const contextMap = new Map()
2424
* @returns {Object} Scoped event emitter with a debug ID of `logger`.
2525
*/
2626
// 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) {
2828
if (!(typeof parent === 'object' && !!parent && typeof loggerFn === 'string' && !!loggerFn && typeof parent[loggerFn] === 'function')) return warn(29)
2929
const ee = scopedEE(sharedEE)
30-
const wrapFn = wfn(ee)
30+
const wrapFn = wfn(ee, undefined, agentRef)
3131

3232
/**
3333
* 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
4141
const contextLookupKey = parent[loggerFn]?.[flag] || parent[loggerFn]
4242
contextMap.set(contextLookupKey, ctx)
4343

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)
4648

4749
return ee
4850
}

src/common/wrap/wrap-xhr.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ export function wrapXhr (sharedEE, agentRef) {
5656
const xhr = new OrigXHR(opts)
5757
const context = ee.context(xhr)
5858
context.targets = findTargetsFromStackTrace(agentRef)
59-
// undefined target reports to container
60-
if (!context.targets.length) context.targets.push(undefined)
6159

6260
try {
6361
ee.emit('new-xhr', [xhr], context)

src/features/ajax/aggregate/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export class Aggregate extends AggregateBase {
112112
/** make a copy of the event for the MFE target if it exists */
113113
if (target) {
114114
this.events.add({ ...event, targetAttributes: getVersion2Attributes(target, this) })
115-
if (shouldDuplicate(target, this.agentRef)) this.reportContainerEvent({ ...event, targetAttributes: getVersion2DuplicationAttributes(target, this) }, ctx)
115+
if (shouldDuplicate(target, this)) this.reportContainerEvent({ ...event, targetAttributes: getVersion2DuplicationAttributes(target, this) }, ctx)
116116
} else {
117117
this.reportContainerEvent(event, ctx)
118118
}

src/features/logging/aggregate/index.js

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { applyFnToProps } from '../../../common/util/traverse'
1313
import { SESSION_EVENT_TYPES, SESSION_EVENTS } from '../../../common/session/constants'
1414
import { ABORT_REASONS } from '../../session_replay/constants'
1515
import { canEnableSessionTracking } from '../../utils/feature-gates'
16-
import { getVersion2Attributes } from '../../../common/util/v2'
16+
import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/util/v2'
1717

1818
const LOGGING_EVENT = 'Logging/Event/'
1919

@@ -73,12 +73,6 @@ export class Aggregate extends AggregateBase {
7373

7474
if (!attributes || typeof attributes !== 'object') attributes = {}
7575

76-
attributes = {
77-
...attributes,
78-
/** Specific attributes only supplied if harvesting to endpoint version 2 */
79-
...(getVersion2Attributes(target, this))
80-
}
81-
8276
if (typeof level === 'string') level = level.toUpperCase()
8377
if (!isValidLogLevel(level)) return warn(30, level)
8478
if (modeForThisLog < (LOGGING_MODE[level] || Infinity)) {
@@ -104,14 +98,19 @@ export class Aggregate extends AggregateBase {
10498
}
10599
if (typeof message !== 'string' || !message) return warn(32)
106100

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+
}
113111

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) })
115114
}
116115

117116
serializer (eventBuffer) {

src/features/logging/instrument/index.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ export class Instrument extends InstrumentBase {
2727

2828
globals.forEach((method) => {
2929
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)
3131
})
3232

3333
/** 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 = []) {
3535
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+
})
3739
})
3840
this.importAggregator(agentRef, () => import(/* webpackChunkName: "logging-aggregate" */ '../aggregate'))
3941
}

src/features/logging/shared/utils.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ import { LOGGING_EVENT_EMITTER_CHANNEL, LOG_LEVELS } from '../constants'
1414
* @param {{[key: string]: *}} customAttributes - The log's custom attributes if any
1515
* @param {enum} level - the log level enum
1616
* @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
1818
*/
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()) {
2020
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)
2222
}
2323

2424
/**

src/loaders/api/register.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ function register (agentRef, target, parent) {
130130
setUserId: (value) => setLocalValue('enduser.id', value),
131131
/** metadata */
132132
metadata: {
133-
customAttributes: attrs,
133+
get customAttributes () { return attrs },
134134
target,
135135
timings
136136
}

0 commit comments

Comments
 (0)