Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/common/util/mfe.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export function hasValidValue (val) {

/**
* When given a valid target, returns an object with the MFE payload attributes. Returns an empty object otherwise.
* @note Field names may change as the schema is finalized
*
* @param {Object} [target] the registered target
* @param {AggregateInstance} [aggregateInstance] the aggregate instance calling the method
* @returns {{'mfe.id': *, 'mfe.name': String}|{}} returns an empty object if args are not supplied or the aggregate instance is not supporting version 2
Expand All @@ -31,9 +33,9 @@ export function getVersion2Attributes (target, aggregateInstance) {
}
}
return {
'mfe.id': target.id, // these field names may change as the schema is finalized
'mfe.name': target.name, // these field names may change as the schema is finalized
eventSource: 'MicroFrontendBrowserAgent', // these field names may change as the schema is finalized
'parent.id': containerAgentEntityGuid
'mfe.id': target.id,
'mfe.name': target.name,
eventSource: target.eventSource,
'parent.id': target.parent?.id || containerAgentEntityGuid
}
}
7 changes: 5 additions & 2 deletions src/loaders/api/register-api-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
* @property {(name: string, attributes?: object) => void} addPageAction - Add a page action for the registered entity.
* @property {(message: string, options?: { customAttributes?: object, level?: 'ERROR' | 'TRACE' | 'DEBUG' | 'INFO' | 'WARN'}) => void} log - Capture a log for the registered entity.
* @property {(error: Error | string, customAttributes?: object) => void} noticeError - Notice an error for the registered entity.
* @property {(target: RegisterAPIConstructor) => RegisterAPI} register - Record a custom event for the registered entity.
* @property {(eventType: string, attributes?: Object) => void} recordCustomEvent - Record a custom event for the registered entity.
* @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.
* @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.
* @property {(value: string | null) => void} setApplicationVersion - Add an application.version attribute to all outgoing data for the registered entity.
* @property {(name: string, value: string | number | boolean | null, persist?: boolean) => void} setCustomAttribute - Add a custom attribute to outgoing data for the registered entity.
* @property {(value: string | null) => void} setUserId - Add an enduser.id attribute to all outgoing API data for the registered entity.
Expand All @@ -20,15 +21,17 @@
* @typedef {Object} RegisterAPIConstructor
* @property {string|number} id - The unique id for the registered entity. This will be assigned to any synthesized entities.
* @property {string} name - The readable name for the registered entity. This will be assigned to any synthesized entities.
* @property {string} [parentId] - The parentId for the registered entity. If none was supplied, it will assume the entity guid from the main agent.
*/

/**
* @typedef {Object} RegisterAPIMetadata
* @property {Object} customAttributes - The custom attributes for the registered entity.
* @property {Object} target - The options for the registered entity.
* @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.
* @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.
* @property {string} target.id - The ID for the registered entity.
* @property {string} target.name - The name returned for the registered entity.
* @property {string} [target.parentId] - The parentId for the registered entity. If none was supplied, it will assume the entity guid from the main agent.
*/

export default {}
18 changes: 9 additions & 9 deletions src/loaders/api/register.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,27 @@ import { recordCustomEvent } from './recordCustomEvent'
*/
export function setupRegisterAPI (agent) {
setupAPI(REGISTER, function (target) {
return buildRegisterApi(agent, target)
return register(agent, target)
}, agent)
}

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

target ||= {}
target.eventSource = 'MicroFrontendBrowserAgent'
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.
target.blocked = false
target.parent = parent || {}

/** @type {Function} a function that is set and reports when APIs are triggered -- warns the customer of the invalid state */
let invalidApiResponse = () => {}
Expand Down Expand Up @@ -85,6 +84,7 @@ export function buildRegisterApi (agentRef, target) {
log: (message, options = {}) => report(log, [message, { ...options, customAttributes: { ...attrs, ...(options.customAttributes || {}) } }, agentRef], target),
measure: (name, options = {}) => report(measure, [name, { ...options, customAttributes: { ...attrs, ...(options.customAttributes || {}) } }, agentRef], target),
noticeError: (error, attributes = {}) => report(noticeError, [error, { ...attrs, ...attributes }, agentRef], target),
register: (target = {}) => report(register, [agentRef, target], api.metadata.target),
recordCustomEvent: (eventType, attributes = {}) => report(recordCustomEvent, [eventType, { ...attrs, ...attributes }, agentRef], target),
setApplicationVersion: (value) => setLocalValue('application.version', value),
setCustomAttribute: (key, value) => setLocalValue(key, value),
Expand Down Expand Up @@ -133,8 +133,8 @@ export function buildRegisterApi (agentRef, target) {
const timestamp = now()
handle(SUPPORTABILITY_METRIC_CHANNEL, [`API/register/${methodToCall.name}/called`], undefined, FEATURE_NAMES.metrics, agentRef.ee)
try {
const shouldDuplicate = agentRef.init.api.duplicate_registered_data
if (shouldDuplicate === true || Array.isArray(shouldDuplicate)) {
const shouldDuplicate = agentRef.init.api.duplicate_registered_data && methodToCall.name !== 'register'
if (shouldDuplicate) {
// also report to container by providing undefined target
methodToCall(...args, undefined, timestamp)
}
Expand Down
16 changes: 16 additions & 0 deletions tests/assets/register-api-enabled.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<!--
Copyright 2020 New Relic Corporation.
PDX-License-Identifier: Apache-2.0
-->
<html>
<head>
<title>RUM Unit Test</title>
{init} {config}
<script>
NREUM.init.feature_flags = ['register', 'register.jserrors', 'register.generic_events', 'register.ajax']
</script>
{loader}
</head>
<body>Instrumented</body>
</html>
5 changes: 3 additions & 2 deletions tests/dts/api.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BrowserAgent } from '../../dist/types/loaders/browser-agent'
import { MicroAgent } from '../../dist/types/loaders/micro-agent'
import { InteractionInstance, getContext, onEnd } from '../../dist/types/loaders/api/interaction-types'
import { expectType } from 'tsd'
import { RegisterAPI, RegisterAPIMetadata } from '../../dist/types/interfaces/registered-entity'
import { RegisterAPI, RegisterAPIConstructor, RegisterAPIMetadata } from '../../dist/types/interfaces/registered-entity'

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

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

Expand Down
43 changes: 43 additions & 0 deletions tests/specs/api.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,49 @@ describe('newrelic api', () => {
})

describe('registered-entity', () => {
it('should allow a nested register', async () => {
const [mfeErrorsCapture] = await browser.testHandle.createNetworkCaptures('bamServer', [
{ test: testMFEErrorsRequest }
])
await browser.url(await browser.testHandle.assetURL('test-builds/browser-agent-wrapper/registered-entity.html', { init: { feature_flags: ['register', 'register.jserrors'] } }))

await browser.execute(function () {
window.agent1 = newrelic.register({
id: 1,
name: 'agent1'
})
window.agent2 = window.agent1.register({
id: 2,
name: 'agent2'
})
window.agent3 = window.agent2.register({
id: 3,
name: 'agent3'
})
// should get data as "agent2"
window.agent1.noticeError('1')
window.agent2.noticeError('2')
window.agent3.noticeError('3')
})

const errorsHarvests = await mfeErrorsCapture.waitForResult({ totalCount: 1 })

const containerAgentEntityGuid = await browser.execute(function () {
return Object.values(newrelic.initializedAgents)[0].runtime.appMetadata.agents[0].entityGuid
})

// should get ALL data as "agent2" since it replaced the name of agent 1 of the same id
errorsHarvests.forEach(({ request: { query, body } }) => {
const data = body.err
data.forEach((err, idx) => {
expect(err.custom['mfe.name']).toEqual('agent' + (idx + 1))
if (idx === 0) expect(err.custom['parent.id']).toEqual(containerAgentEntityGuid) // first app should have container as its parent
if (idx === 1) expect(err.custom['parent.id']).toEqual(1) // second app should have first app as its parent
if (idx === 2) expect(err.custom['parent.id']).toEqual(2) // third app should have second app as its parent
})
})
})

const featureFlags = [
[],
['register'],
Expand Down
40 changes: 40 additions & 0 deletions tests/specs/npm/registered-entity.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,44 @@ describe('registered-entity', () => {
})
})
})

it('should allow a nested register', async () => {
await browser.url(await browser.testHandle.assetURL('test-builds/browser-agent-wrapper/registered-entity.html', { init: { feature_flags: ['register', 'register.jserrors'] } }))

await browser.execute(function () {
window.agent1 = new RegisteredEntity({
id: 1,
name: 'agent1'
})
window.agent2 = window.agent1.register({
id: 2,
name: 'agent2'
})
window.agent3 = window.agent2.register({
id: 3,
name: 'agent3'
})
// should get data as "agent2"
window.agent1.noticeError('1')
window.agent2.noticeError('2')
window.agent3.noticeError('3')
})

const errorsHarvests = await mfeErrorsCapture.waitForResult({ totalCount: 1 })

const containerAgentEntityGuid = await browser.execute(function () {
return Object.values(newrelic.initializedAgents)[0].runtime.appMetadata.agents[0].entityGuid
})

// should get ALL data as "agent2" since it replaced the name of agent 1 of the same id
errorsHarvests.forEach(({ request: { query, body } }) => {
const data = body.err
data.forEach((err, idx) => {
expect(err.custom['mfe.name']).toEqual('agent' + (idx + 1))
if (idx === 0) expect(err.custom['parent.id']).toEqual(containerAgentEntityGuid) // first app should have container as its parent
if (idx === 1) expect(err.custom['parent.id']).toEqual(1) // second app should have first app as its parent
if (idx === 2) expect(err.custom['parent.id']).toEqual(2) // third app should have second app as its parent
})
})
})
})
Loading