Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
36358c2
basics in place
metal-messiah Jun 5, 2025
436aa15
fix jest tests
metal-messiah Jun 7, 2025
7530c53
clean up
metal-messiah Jun 7, 2025
4dbf3c0
basic changes in place
metal-messiah Jun 9, 2025
03c217a
fixing tests
metal-messiah Jun 10, 2025
4258391
fix tests
metal-messiah Jun 10, 2025
a0bf6dc
Merge branch 'main' into mfe-refactor
metal-messiah Jun 10, 2025
cb6ba74
fix for firefox and safari
metal-messiah Jun 10, 2025
b40e76a
fix for all browsers
metal-messiah Jun 10, 2025
9d78f33
fix jest tests
metal-messiah Jun 12, 2025
a8a1281
Merge branch 'main' into mfe-refactor
metal-messiah Jun 12, 2025
0cc29eb
report SM when event buffer is exceeded
metal-messiah Jun 16, 2025
4ddaa4e
fix merge conflicts
metal-messiah Aug 29, 2025
4a05827
apply changes to match new specs
metal-messiah Aug 29, 2025
467423f
add exp.
metal-messiah Aug 29, 2025
34800dd
support endpoint version 2
metal-messiah Aug 29, 2025
6955682
add tests for version 2
metal-messiah Aug 30, 2025
19359cc
format payloads to fit v2 spec
metal-messiah Sep 2, 2025
691a94f
fix merge conflicts
metal-messiah Sep 2, 2025
305d756
cleanup
metal-messiah Sep 3, 2025
f4b859c
add mechanisms to prevent issues before consumer GA
metal-messiah Sep 9, 2025
ef139cb
Merge branch 'main' into mfe-refactor
metal-messiah Sep 12, 2025
2baf639
Merge branch 'main' into mfe-refactor
metal-messiah Sep 15, 2025
bad82e7
Merge branch 'main' into mfe-refactor
metal-messiah Sep 25, 2025
1f7599e
fix jest tests after merge conflicts
metal-messiah Sep 25, 2025
70dacd7
add retry where advised
metal-messiah Sep 25, 2025
405c80f
stabilize tests
metal-messiah Sep 30, 2025
71522e4
fix merge conflicts
metal-messiah Sep 30, 2025
4bfa4c5
Fix types test
metal-messiah Sep 30, 2025
fe3b075
Merge branch 'main' into mfe-refactor
metal-messiah Oct 13, 2025
d18c923
fix broken test
metal-messiah Oct 13, 2025
05209f4
add feature flag system
metal-messiah Oct 14, 2025
645ba33
Merge branch 'main' into mfe-refactor
metal-messiah Oct 14, 2025
249a05f
add feature flag controls
metal-messiah Oct 14, 2025
816693e
add boosted tests
metal-messiah Oct 14, 2025
04cce98
fix test
metal-messiah Oct 16, 2025
a10d95d
clarify test checks
metal-messiah Oct 16, 2025
99a3219
update server constants to match agent defaults
metal-messiah Oct 20, 2025
173e57d
fix merge conflicts
metal-messiah Oct 21, 2025
911ea8a
address pr comments
metal-messiah Oct 21, 2025
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: 5 additions & 5 deletions .github/actions/build-ab/templates/postamble.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ if (!!newrelic && !!newrelic.log) {
}

// Browser AI-Hackthon POC Prep
try{
try {
const automatedHints = ['Headless', 'PhantomJS', 'Selenium', 'WebDriver', 'Puppeteer', 'Playwright']
const botHints = ['bot', 'spider', 'crawler', 'scraper', 'robot', 'Googlebot', 'Bingbot', 'Slurp', 'DuckDuckBot', 'Baiduspider', 'YandexBot', 'AhrefsBot', 'SemrushBot', 'Exabot', 'facebot', 'ia_archiver', 'facebookexternalhit', 'Twitterbot', 'LinkedInBot', 'Slackbot', 'Discordbot', 'Pinterestbot', 'WhatsApp', 'TelegramBot', 'GoogleAdsBot', 'BingPreview']

const automatedMatches = []
const botMatches = []

Expand All @@ -81,7 +81,7 @@ try{

const userAgentString = window.navigator.userAgent

if (userAgentString){
if (userAgentString) {
automatedHints.map(x => x.toLowerCase()).forEach((hint) => {
if (userAgentString.toLowerCase().includes(hint)) {
automatedMatches.push(hint);
Expand All @@ -107,7 +107,7 @@ try{
newrelic.setCustomAttribute('userAgent', window.navigator.userAgent);
}
} catch (e) {
newrelic.noticeError(new Error("NRBA: swallowed preamble error", {cause: e}));
newrelic.noticeError(new Error("NRBA: swallowed preamble error", { cause: e }));
}

try {
Expand Down Expand Up @@ -140,5 +140,5 @@ try {
pressureObserver.observe("cpu", { sampleInterval: 1000 });
}
} catch (e) {
newrelic.noticeError(new Error("NRBA: swallowed preamble error", {cause: e}));
newrelic.noticeError(new Error("NRBA: swallowed preamble error", { cause: e }));
}
2 changes: 1 addition & 1 deletion .github/actions/build-ab/templates/released.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// config
window.NREUM={
init: {
feature_flags: ['soft_nav', 'user_frustrations'],
feature_flags: ['soft_nav', 'user_frustrations', 'register'], // add jserrors and generic events once the consumer(s) support it
distributed_tracing: {
enabled: true
},
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/pull-request-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ on:
description: 'Run the latest version of browsers only instead of all versions'
default: true
type: boolean
boosted:
description: 'Enable boosted testing (uses more resources)'
default: false
type: boolean
pull_request:

jobs:
Expand Down Expand Up @@ -261,6 +265,7 @@ jobs:
with:
ref: 'refs/pull/${{ fromJSON(needs.find-pull-request.outputs.pull-request-target).pr_number }}/merge'
latest-only: ${{ inputs.latest-only }}
boosted: ${{ inputs.boosted }}
secrets: inherit

complete-status-comment-pull-request:
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/wdio-all-browsers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ on:
description: 'Run the latest version of browsers only instead of all versions'
default: true
type: boolean
boosted:
description: 'Enable boosted testing (uses more resources)'
default: false
type: boolean
secrets:
LAMBDA_USERNAME:
required: true
Expand All @@ -24,27 +28,31 @@ jobs:
with:
ref: ${{ inputs.ref || github.ref }}
browser-target: ${{ inputs.latest-only == true && 'chrome@latest' || 'chrome@*' }}
concurrency: ${{ inputs.boosted == true && 20 || 10 }}
secrets: inherit

firefox:
uses: ./.github/workflows/wdio-single-browser.yml
with:
ref: ${{ inputs.ref || github.ref }}
browser-target: ${{ inputs.latest-only == true && 'firefox@latest' || 'firefox@*' }}
concurrency: ${{ inputs.boosted == true && 20 || 10 }}
secrets: inherit

safari:
uses: ./.github/workflows/wdio-single-browser.yml
with:
ref: ${{ inputs.ref || github.ref }}
browser-target: ${{ inputs.latest-only == true && 'safari@latest' || 'safari@*' }}
concurrency: ${{ inputs.boosted == true && 20 || 10 }}
secrets: inherit

edge:
uses: ./.github/workflows/wdio-single-browser.yml
with:
ref: ${{ inputs.ref || github.ref }}
browser-target: ${{ inputs.latest-only == true && 'edge@latest' || 'edge@*' }}
concurrency: ${{ inputs.boosted == true && 20 || 10 }}
secrets: inherit

ios:
Expand All @@ -53,6 +61,7 @@ jobs:
ref: ${{ inputs.ref || github.ref }}
browser-target: ${{ inputs.latest-only == true && 'ios@latest' || 'ios@*' }}
additional-flags: --timeout 120000 --session-timeout 240000
concurrency: ${{ inputs.boosted == true && 20 || 10 }}
secrets: inherit

android:
Expand All @@ -61,6 +70,7 @@ jobs:
ref: ${{ inputs.ref || github.ref }}
browser-target: ${{ inputs.latest-only == true && 'android@latest' || 'android@*' }}
additional-flags: --timeout 120000 --session-timeout 240000
concurrency: ${{ inputs.boosted == true && 20 || 10 }}
secrets: inherit

ios-webview:
Expand Down
3 changes: 1 addition & 2 deletions docs/warning-codes.md
Comment thread
metal-messiah marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
### 47
`We tried to access a stylesheet's contents but failed due to browser security. For best results, ensure that cross-domain CSS assets are decorated with "crossorigin='anonymous'" attribution or are otherwise publicly accessible.`
### 48
`Supplied an invalid API target. Must be an <Object> that contains licenseKey and applicationID properties.`
`Supplied an invalid API target. Must be an <Object> that contains id and name properties.`
### 49
`Supplied API target is missing an entityGuid. Some APIs may not behave correctly without a valid entityGuid (ex. logs).`
### 50
Expand Down Expand Up @@ -129,4 +129,3 @@
`A single event was larger than the maximum allowed payload size`
### 64
`Required globals have been mutated before being accessed by the browser agent. This can cause issues and should be avoided.`

2 changes: 1 addition & 1 deletion src/common/config/init-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
* @property {Object} [metrics]
* @property {boolean} [metrics.enabled] - Turn on/off the metrics feature (on by default).
* @property {boolean} [metrics.autoStart] - If true, the agent will automatically start the metrics feature. Otherwise, it will be in a deferred state until the `start` API method is called.
* @property {Array<Object>} [obfuscate] - Array of regexp and corresponding replacement patterns for obfuscating data.
* @property {{regex: RegExp | string, replacement: string}[]} [obfuscate] - Array of regexp and corresponding replacement patterns for obfuscating data.
* @property {Object} [page_action]
* @property {boolean} [page_action.enabled] - Must be true to allow PageAction events to be captured.
* @property {Object} [page_view_event]
Expand Down
2 changes: 1 addition & 1 deletion src/common/config/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const InitModelFn = () => {
return {
ajax: { deny_list: undefined, block_internal: true, enabled: true, autoStart: true },
api: {
allow_registered_children: true,
allow_registered_children: false,
Comment thread
cwli24 marked this conversation as resolved.
Outdated
duplicate_registered_data: false
},
distributed_tracing: {
Expand Down
2 changes: 1 addition & 1 deletion src/common/config/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const RuntimeModel = {
customTransaction: undefined,
denyList: undefined,
disabled: false,
entityManager: undefined,
harvester: undefined,
isolatedBacklog: false,
isRecording: false, // true when actively recording, false when paused or stopped
Expand All @@ -37,6 +36,7 @@ const RuntimeModel = {
releaseIds: {},
session: undefined,
timeKeeper: undefined,
registeredEntities: [],
/** a proxy is set in agent-session to track jsAttributes changes for harvesting mechanics */
jsAttributesMetadata: { bytes: 0 },
get harvestCount () { return ++_harvestCount }
Expand Down
10 changes: 10 additions & 0 deletions src/common/constants/agent-constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@
* Copyright 2020-2025 New Relic, Inc. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import { FEATURE_NAMES } from '../../loaders/features/features'

export const IDEAL_PAYLOAD_SIZE = 16000
export const MAX_PAYLOAD_SIZE = 1000000
export const DEFAULT_KEY = 'NR_CONTAINER_AGENT'
export const SESSION_ERROR = 'SESSION_ERROR'

export const SUPPORTS_REGISTERED_ENTITIES = {
[FEATURE_NAMES.logging]: true,
// flip other features here when they are supported by DEM consumers
[FEATURE_NAMES.genericEvents]: false,
[FEATURE_NAMES.jserrors]: false,
[FEATURE_NAMES.ajax]: false
}
2 changes: 1 addition & 1 deletion src/common/drain/drain.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ function drainGroup (agentIdentifier, group, activateGroup = true) {

if (!baseEE.isolatedBacklog) delete handlers[group]
baseEE.backlog[group] = null
baseEE.emit('drain-' + group, []) // TODO: Code exists purely for a unit test and needs to be refined
baseEE.emit('drain-' + group, []) // Informs the feature that it has drained its backlog of events, this kicks off an immediate harvest to capture any load-driven events that were buffered.
}

/**
Expand Down
59 changes: 27 additions & 32 deletions src/common/harvest/harvester.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,34 +55,30 @@ export class Harvester {
* @returns {boolean} True if 1+ network call was made. Note that this does not mean or guarantee that it was successful (or that all were in the case of more than 1).
*/
triggerHarvestFor (aggregateInst, localOpts = {}) {
if (aggregateInst.blocked) return false
const output = { ranSend: false, payload: undefined, endpointVersion: aggregateInst.harvestEndpointVersion || 1 }
if (aggregateInst.blocked) return output

const submitMethod = getSubmitMethod(localOpts)
if (!submitMethod) return false
if (!submitMethod) return output

const shouldRetryOnFail = !localOpts.isFinalHarvest && submitMethod === xhrMethod // always retry all features harvests except for final
let dataToSendArr; let ranSend = false
if (!localOpts.directSend) { // primarily used by rum call to bypass makeHarvestPayload by providing payload directly
dataToSendArr = aggregateInst.makeHarvestPayload(shouldRetryOnFail, localOpts) // be sure the 'this' of makeHarvestPayload is the aggregate w/ access to its harvestOpts
if (!dataToSendArr) return false // can be undefined if storage is empty or preharvest checks failed
} else dataToSendArr = [localOpts.directSend]

dataToSendArr.forEach(({ targetApp, payload }) => {
if (!payload) return

send(this.agentRef, {
endpoint: FEATURE_TO_ENDPOINT[aggregateInst.featureName],
targetApp,
payload,
localOpts,
submitMethod,
cbFinished,
raw: aggregateInst.harvestOpts.raw,
featureName: aggregateInst.featureName
})
ranSend = true
output.payload = !localOpts.directSend ? aggregateInst.makeHarvestPayload(shouldRetryOnFail, localOpts) : localOpts.directSend?.payload // features like PVE can define the payload directly, bypassing the makeHarvestPayload logic

if (!output.payload) return output

send(this.agentRef, {
endpoint: FEATURE_TO_ENDPOINT[aggregateInst.featureName],
payload: output.payload,
localOpts,
submitMethod,
cbFinished,
raw: aggregateInst.harvestOpts.raw,
featureName: aggregateInst.featureName,
endpointVersion: output.endpointVersion
})
return ranSend
output.ranSend = true

return output

/**
* This is executed immediately after harvest sends the data via XHR, or if there's nothing to send. Note that this excludes on unloading / sendBeacon.
Expand Down Expand Up @@ -112,22 +108,22 @@ const warnings = {}
* @param {NetworkSendSpec} param0 Specification for sending data
* @returns {boolean} True if a network call was made. Note that this does not mean or guarantee that it was successful.
*/
export function send (agentRef, { endpoint, targetApp, payload, localOpts = {}, submitMethod, cbFinished, raw, featureName }) {
export function send (agentRef, { endpoint, payload, localOpts = {}, submitMethod, cbFinished, raw, featureName, endpointVersion = 1 }) {
if (!agentRef.info.errorBeacon) return false

let { body, qs } = cleanPayload(payload)

if (Object.keys(body).length === 0 && !localOpts.sendEmptyBody) { // if there's no body to send, just run onfinish stuff and return
if (cbFinished) cbFinished({ sent: false, targetApp })
if (cbFinished) cbFinished({ sent: false })
return false
}

const protocol = agentRef.init.ssl === false ? 'http' : 'https'
const perceivedBeacon = agentRef.init.proxy.beacon || agentRef.info.errorBeacon
const url = raw
? `${protocol}://${perceivedBeacon}/${endpoint}`
: `${protocol}://${perceivedBeacon}${endpoint !== RUM ? '/' + endpoint : ''}/1/${targetApp.licenseKey}`
const baseParams = !raw ? baseQueryString(agentRef, qs, endpoint, targetApp.applicationID) : ''
: `${protocol}://${perceivedBeacon}${endpoint !== RUM ? '/' + endpoint : ''}/${endpointVersion}/${agentRef.info.licenseKey}`
const baseParams = !raw ? baseQueryString(agentRef, qs, endpoint) : ''
let payloadParams = obj(qs, agentRef.runtime.maxBytes)
if (baseParams === '' && payloadParams.startsWith('&')) {
payloadParams = payloadParams.substring(1)
Expand Down Expand Up @@ -159,7 +155,7 @@ export function send (agentRef, { endpoint, targetApp, payload, localOpts = {},
result.addEventListener('loadend', function () {
// `this` here in block refers to the XHR object in this scope, do not change the anon function to an arrow function
// status 0 refers to a local error, such as CORS or network failure, or a blocked request by the browser (e.g. adblocker)
const cbResult = { sent: this.status !== 0, status: this.status, retry: shouldRetry(this.status), fullUrl, xhr: this, targetApp }
const cbResult = { sent: this.status !== 0, status: this.status, retry: shouldRetry(this.status), fullUrl, xhr: this }
if (localOpts.needResponse) cbResult.responseText = this.responseText
cbFinished(cbResult)

Expand All @@ -169,7 +165,7 @@ export function send (agentRef, { endpoint, targetApp, payload, localOpts = {},
} else if (submitMethod === fetchMethod) {
result.then(async function (response) {
const status = response.status
const cbResult = { sent: true, status, retry: shouldRetry(status), fullUrl, fetchResponse: response, targetApp }
const cbResult = { sent: true, status, retry: shouldRetry(status), fullUrl, fetchResponse: response }
if (localOpts.needResponse) cbResult.responseText = await response.text()
cbFinished(cbResult)
/** temporary audit of consistency of harvest metadata flags */
Expand Down Expand Up @@ -207,7 +203,6 @@ export function send (agentRef, { endpoint, targetApp, payload, localOpts = {},
data: {
endpoint,
headers,
targetApp,
payload,
submitMethod: getSubmitMethodName(),
raw,
Expand Down Expand Up @@ -264,14 +259,14 @@ function cleanPayload (payload = {}) {
}

// The stuff that gets sent every time.
function baseQueryString (agentRef, qs, endpoint, applicationID) {
function baseQueryString (agentRef, qs, endpoint) {
const ref = agentRef.runtime.obfuscator.obfuscateString(cleanURL('' + globalScope.location))
const session = agentRef.runtime.session
const hr = !!session?.state.sessionReplaySentFirstChunk && session?.state.sessionReplayMode === 1 && endpoint !== JSERRORS
const ht = !!session?.state.traceHarvestStarted && session?.state.sessionTraceMode === 1 && ![LOGS, BLOBS].includes(endpoint)

const qps = [
'a=' + applicationID,
'a=' + agentRef.info.applicationID,
param('sa', (agentRef.info.sa ? '' + agentRef.info.sa : '')),
param('v', VERSION),
transactionNameParam(),
Expand Down
34 changes: 34 additions & 0 deletions src/common/util/mfe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright 2020-2025 New Relic, Inc. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

/**
* @param {Object} [target] - the target to be validated
* @returns {boolean}
*/
export function isValidMFETarget (target = {}) {
return !!(target.id && target.name)
}

/**
* When given a valid target, returns an object with the MFE payload attributes. Returns an empty object otherwise.
* @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
*/
export function getVersion2Attributes (target, aggregateInstance) {
if (aggregateInstance?.harvestEndpointVersion !== 2) return {}
const containerAgentEntityGuid = aggregateInstance.agentRef.runtime.appMetadata.agents[0].entityGuid
if (!isValidMFETarget(target)) {
return {
'entity.guid': containerAgentEntityGuid
}
}
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
Comment thread
metal-messiah marked this conversation as resolved.
Outdated
}
}
27 changes: 0 additions & 27 deletions src/common/util/target.js

This file was deleted.

Loading
Loading