Skip to content

Commit 74a8d4a

Browse files
feat: Allow nested registrations (#1616)
1 parent aa8c02f commit 74a8d4a

File tree

7 files changed

+122
-17
lines changed

7 files changed

+122
-17
lines changed

src/common/util/mfe.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export function hasValidValue (val) {
1717

1818
/**
1919
* When given a valid target, returns an object with the MFE payload attributes. Returns an empty object otherwise.
20+
* @note Field names may change as the schema is finalized
21+
*
2022
* @param {Object} [target] the registered target
2123
* @param {AggregateInstance} [aggregateInstance] the aggregate instance calling the method
2224
* @returns {{'mfe.id': *, 'mfe.name': String}|{}} returns an empty object if args are not supplied or the aggregate instance is not supporting version 2
@@ -31,9 +33,9 @@ export function getVersion2Attributes (target, aggregateInstance) {
3133
}
3234
}
3335
return {
34-
'mfe.id': target.id, // these field names may change as the schema is finalized
35-
'mfe.name': target.name, // these field names may change as the schema is finalized
36-
eventSource: 'MicroFrontendBrowserAgent', // these field names may change as the schema is finalized
37-
'parent.id': containerAgentEntityGuid
36+
'mfe.id': target.id,
37+
'mfe.name': target.name,
38+
eventSource: target.eventSource,
39+
'parent.id': target.parent?.id || containerAgentEntityGuid
3840
}
3941
}

src/loaders/api/register-api-types.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
* @property {(name: string, attributes?: object) => void} addPageAction - Add a page action for the registered entity.
99
* @property {(message: string, options?: { customAttributes?: object, level?: 'ERROR' | 'TRACE' | 'DEBUG' | 'INFO' | 'WARN'}) => void} log - Capture a log for the registered entity.
1010
* @property {(error: Error | string, customAttributes?: object) => void} noticeError - Notice an error for the registered entity.
11+
* @property {(target: RegisterAPIConstructor) => RegisterAPI} register - Record a custom event for the registered entity.
1112
* @property {(eventType: string, attributes?: Object) => void} recordCustomEvent - Record a custom event for the registered entity.
12-
* @property {(eventType: string, options?: {start: number, end: number, duration: number, customAttributes: object}) => {{start: number, end: number, duration: number, customAttributes: object}}} measure - Measures a task that is recorded as a BrowserPerformance event.
13+
* @property {(eventType: string, options?: {start: number, end: number, duration: number, customAttributes: object}) => ({start: number, end: number, duration: number, customAttributes: object})} measure - Measures a task that is recorded as a BrowserPerformance event.
1314
* @property {(value: string | null) => void} setApplicationVersion - Add an application.version attribute to all outgoing data for the registered entity.
1415
* @property {(name: string, value: string | number | boolean | null, persist?: boolean) => void} setCustomAttribute - Add a custom attribute to outgoing data for the registered entity.
1516
* @property {(value: string | null) => void} setUserId - Add an enduser.id attribute to all outgoing API data for the registered entity.
@@ -20,15 +21,17 @@
2021
* @typedef {Object} RegisterAPIConstructor
2122
* @property {string|number} id - The unique id for the registered entity. This will be assigned to any synthesized entities.
2223
* @property {string} name - The readable name for the registered entity. This will be assigned to any synthesized entities.
24+
* @property {string} [parentId] - The parentId for the registered entity. If none was supplied, it will assume the entity guid from the main agent.
2325
*/
2426

2527
/**
2628
* @typedef {Object} RegisterAPIMetadata
2729
* @property {Object} customAttributes - The custom attributes for the registered entity.
2830
* @property {Object} target - The options for the registered entity.
29-
* @property {string} target.licenseKey - The license key for the registered entity. If none was supplied, it will assume the license key from the main agent.
31+
* @property {string} [target.licenseKey] - The license key for the registered entity. If none was supplied, it will assume the license key from the main agent.
3032
* @property {string} target.id - The ID for the registered entity.
3133
* @property {string} target.name - The name returned for the registered entity.
34+
* @property {string} [target.parentId] - The parentId for the registered entity. If none was supplied, it will assume the entity guid from the main agent.
3235
*/
3336

3437
export default {}

src/loaders/api/register.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,28 +28,27 @@ import { recordCustomEvent } from './recordCustomEvent'
2828
*/
2929
export function setupRegisterAPI (agent) {
3030
setupAPI(REGISTER, function (target) {
31-
return buildRegisterApi(agent, target)
31+
return register(agent, target)
3232
}, agent)
3333
}
3434

3535
/**
3636
* Builds the api object that will be returned from the register api method.
3737
* Also conducts certain side-effects, such as harvesting a PageView event when triggered and gathering metadata for the registered entity.
3838
* @param {Object} agentRef the reference to the base agent instance
39-
* @param {Object} handlers the shared handlers to be used by both the base agent's API and the external target's API
40-
* @param {Object} target the target information to be used by the external target's API to send data to the correct location
41-
* @param {string} [target.licenseKey] the license key of the target to report data to
42-
* @param {string} target.id the entity ID of the target to report data to
43-
* @param {string} target.name the entity name of the target to report data to
39+
* @param {import('./register-api-types').RegisterAPIConstructor} target
40+
* @param {import('./register-api-types').RegisterAPIConstructor} [parent]
4441
* @returns {RegisterAPI} the api object to be returned from the register api method
4542
*/
46-
export function buildRegisterApi (agentRef, target) {
43+
function register (agentRef, target, parent) {
4744
const attrs = {}
4845
warn(54, 'newrelic.register')
4946

5047
target ||= {}
48+
target.eventSource = 'MicroFrontendBrowserAgent'
5149
target.licenseKey ||= agentRef.info.licenseKey // will inherit the license key from the container agent if not provided for brevity. A future state may dictate that we need different license keys to do different things.
5250
target.blocked = false
51+
target.parent = parent || {}
5352

5453
/** @type {Function} a function that is set and reports when APIs are triggered -- warns the customer of the invalid state */
5554
let invalidApiResponse = () => {}
@@ -85,6 +84,7 @@ export function buildRegisterApi (agentRef, target) {
8584
log: (message, options = {}) => report(log, [message, { ...options, customAttributes: { ...attrs, ...(options.customAttributes || {}) } }, agentRef], target),
8685
measure: (name, options = {}) => report(measure, [name, { ...options, customAttributes: { ...attrs, ...(options.customAttributes || {}) } }, agentRef], target),
8786
noticeError: (error, attributes = {}) => report(noticeError, [error, { ...attrs, ...attributes }, agentRef], target),
87+
register: (target = {}) => report(register, [agentRef, target], api.metadata.target),
8888
recordCustomEvent: (eventType, attributes = {}) => report(recordCustomEvent, [eventType, { ...attrs, ...attributes }, agentRef], target),
8989
setApplicationVersion: (value) => setLocalValue('application.version', value),
9090
setCustomAttribute: (key, value) => setLocalValue(key, value),
@@ -133,8 +133,8 @@ export function buildRegisterApi (agentRef, target) {
133133
const timestamp = now()
134134
handle(SUPPORTABILITY_METRIC_CHANNEL, [`API/register/${methodToCall.name}/called`], undefined, FEATURE_NAMES.metrics, agentRef.ee)
135135
try {
136-
const shouldDuplicate = agentRef.init.api.duplicate_registered_data
137-
if (shouldDuplicate === true || Array.isArray(shouldDuplicate)) {
136+
const shouldDuplicate = agentRef.init.api.duplicate_registered_data && methodToCall.name !== 'register'
137+
if (shouldDuplicate) {
138138
// also report to container by providing undefined target
139139
methodToCall(...args, undefined, timestamp)
140140
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<!--
3+
Copyright 2020 New Relic Corporation.
4+
PDX-License-Identifier: Apache-2.0
5+
-->
6+
<html>
7+
<head>
8+
<title>RUM Unit Test</title>
9+
{init} {config}
10+
<script>
11+
NREUM.init.feature_flags = ['register', 'register.jserrors', 'register.generic_events', 'register.ajax']
12+
</script>
13+
{loader}
14+
</head>
15+
<body>Instrumented</body>
16+
</html>

tests/dts/api.test-d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { BrowserAgent } from '../../dist/types/loaders/browser-agent'
22
import { MicroAgent } from '../../dist/types/loaders/micro-agent'
33
import { InteractionInstance, getContext, onEnd } from '../../dist/types/loaders/api/interaction-types'
44
import { expectType } from 'tsd'
5-
import { RegisterAPI, RegisterAPIMetadata } from '../../dist/types/interfaces/registered-entity'
5+
import { RegisterAPI, RegisterAPIConstructor, RegisterAPIMetadata } from '../../dist/types/interfaces/registered-entity'
66

77
const validOptions = {
88
info: {
@@ -60,7 +60,7 @@ expectType<MicroAgent>(microAgent)
6060
expectType<(name: string, trigger?: string) => InteractionInstance>(agent.interaction().setName)
6161

6262
// register APIs
63-
expectType<(target: {id: string|number, name: string}) => RegisterAPI>(agent.register)
63+
expectType<(target: {id: string|number, name: string, parentId?: string}) => RegisterAPI>(agent.register)
6464
const registeredEntity = agent.register({ id: 123, name: 'hello' })
6565
expectType<(name: string, attributes?: object) => void>(registeredEntity.addPageAction)
6666
expectType<(message: string, options?: { customAttributes?: object, level?: 'ERROR' | 'TRACE' | 'DEBUG' | 'INFO' | 'WARN'}) => void>(registeredEntity.log)
@@ -69,6 +69,7 @@ expectType<MicroAgent>(microAgent)
6969
expectType<(value: string | null) => void>(registeredEntity.setApplicationVersion)
7070
expectType<(name: string, value: string | number | boolean | null, persist?: boolean) => void>(registeredEntity.setCustomAttribute)
7171
expectType<(value: string | null) => void>(registeredEntity.setUserId)
72+
expectType<(target: RegisterAPIConstructor) => RegisterAPI>(registeredEntity.register)
7273
expectType<RegisterAPIMetadata>(registeredEntity.metadata)
7374
})
7475

tests/specs/api.e2e.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,49 @@ describe('newrelic api', () => {
99
})
1010

1111
describe('registered-entity', () => {
12+
it('should allow a nested register', async () => {
13+
const [mfeErrorsCapture] = await browser.testHandle.createNetworkCaptures('bamServer', [
14+
{ test: testMFEErrorsRequest }
15+
])
16+
await browser.url(await browser.testHandle.assetURL('test-builds/browser-agent-wrapper/registered-entity.html', { init: { feature_flags: ['register', 'register.jserrors'] } }))
17+
18+
await browser.execute(function () {
19+
window.agent1 = newrelic.register({
20+
id: 1,
21+
name: 'agent1'
22+
})
23+
window.agent2 = window.agent1.register({
24+
id: 2,
25+
name: 'agent2'
26+
})
27+
window.agent3 = window.agent2.register({
28+
id: 3,
29+
name: 'agent3'
30+
})
31+
// should get data as "agent2"
32+
window.agent1.noticeError('1')
33+
window.agent2.noticeError('2')
34+
window.agent3.noticeError('3')
35+
})
36+
37+
const errorsHarvests = await mfeErrorsCapture.waitForResult({ totalCount: 1 })
38+
39+
const containerAgentEntityGuid = await browser.execute(function () {
40+
return Object.values(newrelic.initializedAgents)[0].runtime.appMetadata.agents[0].entityGuid
41+
})
42+
43+
// should get ALL data as "agent2" since it replaced the name of agent 1 of the same id
44+
errorsHarvests.forEach(({ request: { query, body } }) => {
45+
const data = body.err
46+
data.forEach((err, idx) => {
47+
expect(err.custom['mfe.name']).toEqual('agent' + (idx + 1))
48+
if (idx === 0) expect(err.custom['parent.id']).toEqual(containerAgentEntityGuid) // first app should have container as its parent
49+
if (idx === 1) expect(err.custom['parent.id']).toEqual(1) // second app should have first app as its parent
50+
if (idx === 2) expect(err.custom['parent.id']).toEqual(2) // third app should have second app as its parent
51+
})
52+
})
53+
})
54+
1255
const featureFlags = [
1356
[],
1457
['register'],

tests/specs/npm/registered-entity.e2e.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,4 +229,44 @@ describe('registered-entity', () => {
229229
})
230230
})
231231
})
232+
233+
it('should allow a nested register', async () => {
234+
await browser.url(await browser.testHandle.assetURL('test-builds/browser-agent-wrapper/registered-entity.html', { init: { feature_flags: ['register', 'register.jserrors'] } }))
235+
236+
await browser.execute(function () {
237+
window.agent1 = new RegisteredEntity({
238+
id: 1,
239+
name: 'agent1'
240+
})
241+
window.agent2 = window.agent1.register({
242+
id: 2,
243+
name: 'agent2'
244+
})
245+
window.agent3 = window.agent2.register({
246+
id: 3,
247+
name: 'agent3'
248+
})
249+
// should get data as "agent2"
250+
window.agent1.noticeError('1')
251+
window.agent2.noticeError('2')
252+
window.agent3.noticeError('3')
253+
})
254+
255+
const errorsHarvests = await mfeErrorsCapture.waitForResult({ totalCount: 1 })
256+
257+
const containerAgentEntityGuid = await browser.execute(function () {
258+
return Object.values(newrelic.initializedAgents)[0].runtime.appMetadata.agents[0].entityGuid
259+
})
260+
261+
// should get ALL data as "agent2" since it replaced the name of agent 1 of the same id
262+
errorsHarvests.forEach(({ request: { query, body } }) => {
263+
const data = body.err
264+
data.forEach((err, idx) => {
265+
expect(err.custom['mfe.name']).toEqual('agent' + (idx + 1))
266+
if (idx === 0) expect(err.custom['parent.id']).toEqual(containerAgentEntityGuid) // first app should have container as its parent
267+
if (idx === 1) expect(err.custom['parent.id']).toEqual(1) // second app should have first app as its parent
268+
if (idx === 2) expect(err.custom['parent.id']).toEqual(2) // third app should have second app as its parent
269+
})
270+
})
271+
})
232272
})

0 commit comments

Comments
 (0)