Skip to content
Draft
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
12 changes: 12 additions & 0 deletions docs/warning-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,15 @@
`A session replay payload failed to send and is being retried. Recording is paused during the retry period, and will resume when a successful harvest is made. Some replay activity may be missed during retry phases.`
### 71
`An invalid feature mode was detected and set to "off".`
### 72
`RegisteredIframeEntity failed to transmit API data from an iframe to window context`
### 73
`RegisteredIframeEntity failed to register with window context`
### 74
`RegisteredIframeEntity rejected message from unauthorized origin`
### 75
`RegisteredIframeEntity rejected message with mismatched iframeInterfaceId`
### 76
`Agent rejected post message, could not match with existing entity`
### 77
`Agent rejected post message, could not validate origin`
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
"interfaces/registered-entity": [
"dist/types/interfaces/registered-entity.d.ts"
],
"interfaces/registered-iframe-entity": [
"dist/types/interfaces/registered-iframe-entity.d.ts"
],
"features/ajax": [
"dist/types/features/ajax/instrument/index.d.ts"
],
Expand Down Expand Up @@ -98,6 +101,11 @@
"require": "./dist/cjs/interfaces/registered-entity.js",
"default": "./dist/esm/interfaces/registered-entity.js"
},
"./interfaces/registered-iframe-entity": {
"types": "./dist/types/interfaces/registered-iframe-entity.d.ts",
"require": "./dist/cjs/interfaces/registered-iframe-entity.js",
"default": "./dist/esm/interfaces/registered-iframe-entity.js"
},
"./features/ajax": {
"types": "./dist/types/features/ajax/instrument/index.d.ts",
"require": "./dist/cjs/features/ajax/instrument/index.js",
Expand Down
15 changes: 13 additions & 2 deletions src/common/config/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ const InitModelFn = () => {
feature_flags: [],
experimental: {
register: false,
resources: false
resources: false,
iframe_bridge: false,
iframe_domains: []
},
mask_selector: '*',
block_selector: '[data-nr-block]',
Expand Down Expand Up @@ -53,7 +55,16 @@ const InitModelFn = () => {
register: {
get enabled () { return hiddenState.feature_flags.includes(FEATURE_FLAGS.REGISTER) || hiddenState.experimental.register },
set enabled (val) { hiddenState.experimental.register = val },
duplicate_data_to_container: false
duplicate_data_to_container: false,
// experimental iframe bridge feature
get allow_iframe_bridge () { return hiddenState.feature_flags.includes(FEATURE_FLAGS.IFRAME_BRIDGE) || hiddenState.experimental.iframe_bridge },
set allow_iframe_bridge (val) { hiddenState.experimental.iframe_bridge = val },
get iframe_domains () { return hiddenState.experimental.iframe_domains },
set iframe_domains (val) {
if (Array.isArray(val)) hiddenState.experimental.iframe_domains = val
else warn(1, val)
}

}
},
browser_consent_mode: { enabled: false },
Expand Down
7 changes: 7 additions & 0 deletions src/common/constants/iframe-constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const prefix = 'newrelic-iframe-'

export const IFRAME_TIMING_UPDATE = prefix + 'timing-update'
export const IFRAME_API = prefix + 'api'
export const IFRAME_API_RESPONSE = prefix + 'api-response'
export const IFRAME_VITALS_UPDATE = prefix + 'vitals-update'
export const IFRAME_AJAX = prefix + 'ajax'
18 changes: 18 additions & 0 deletions src/common/url/add-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import { parseUrl } from './parse-url.js'

export function addUrl (ctx, url) {
var parsed = parseUrl(url)
var params = ctx.params || ctx

params.hostname = parsed.hostname
params.port = parsed.port
params.protocol = parsed.protocol
params.host = parsed.hostname + ':' + parsed.port
params.pathname = parsed.pathname
ctx.parsedOrigin = parsed
ctx.sameOrigin = parsed.sameOrigin
}
6 changes: 4 additions & 2 deletions src/common/v2/mfe-vitals.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,12 @@ export function trackMFEVitals (id, timings) {

const vitals = {
fcp: {
get value () { return getTimeRelativeToScriptStart(fcpObservedAt) }
get value () { return getTimeRelativeToScriptStart(fcpObservedAt) },
set value (v) { fcpObservedAt = v }
},
lcp: {
get value () { return getTimeRelativeToScriptStart(lcpObservedAt) }
get value () { return getTimeRelativeToScriptStart(lcpObservedAt) },
set value (v) { lcpObservedAt = v }
},
cls: {
value: null
Expand Down
55 changes: 35 additions & 20 deletions src/common/v2/script-tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { now } from '../timing/now'
import { cleanURL } from '../url/clean-url'
import { chrome, chromeEval, gecko } from '../util/browser-stack-matchers'
import { ScriptCorrelation } from './script-correlation'
import { timingFactory } from './timing-factory'

/**
* @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
Expand Down Expand Up @@ -196,6 +197,26 @@ function applyPerformanceEntry (timings, entry) {
timings.type = entry.initiatorType
}

/**
* Subscribes to late resource timing emissions for a script URL.
* @param {RegisterAPITimings} timings - The timings object to update
* @param {string} mfeScriptUrl - The script URL to match
*/
function subscribeToLatePerformanceEntry (timings, mfeScriptUrl) {
if (!globalScope.PerformanceObserver?.supportedEntryTypes?.includes('resource')) return

poSubscribers.push({
addedAt: now(),
test: (entry) => {
if (entryMatchesUrl(entry, mfeScriptUrl)) {
applyPerformanceEntry(timings, entry)
return true
}
return false
}
})
}

/**
* Uses the stack of the initiator function, returns script timing information if a script can be found with the resource timing API matching the URL found in the stack.
* @returns {RegisterAPITimings} Object containing script fetch start and end times, and the asset URL if found
Expand Down Expand Up @@ -223,35 +244,29 @@ export function findScriptTimings () {
// Get correlation data
timings.correlation = findCorrelation(mfeScriptUrl)

// Use correlation's performance entry if available, otherwise check live performance API
const performanceEntry = timings.correlation?.performance.value || performance.getEntriesByType('resource').find(e => entryMatchesUrl(e, mfeScriptUrl))
// Use correlation's performance entry if available, otherwise wait for the buffered observer to surface it.
const performanceEntry = timings.correlation?.performance.value

if (performanceEntry) {
applyPerformanceEntry(timings, performanceEntry)
} else if (wasPreloaded(mfeScriptUrl)) {
// Handle preloaded scripts that may report late
timings.asset = mfeScriptUrl
timings.type = 'preload'

// Subscribe to late performance observer callbacks
poSubscribers.push({
addedAt: now(),
test: (entry) => {
if (entryMatchesUrl(entry, mfeScriptUrl)) {
applyPerformanceEntry(timings, entry)
return true
}
return false
}
})
} else {
const isPreloaded = wasPreloaded(mfeScriptUrl)

// Handle preloaded scripts and any late resource emissions through the shared buffered observer.
if (isPreloaded) {
timings.asset = mfeScriptUrl
timings.type = 'preload'
}

subscribeToLatePerformanceEntry(timings, mfeScriptUrl)
}

/*
* Use getters here because the correlation data may arrive after this function returns the timing object, and we want to provide the most up-to-date timing information possible when the getters are accessed at harvest time.
* The getters will fall back to fetchEnd if correlation data isn't available yet, which is our best approximation for script execution start when actual script timings can not be determined.
*/
Object.defineProperty(timings, 'scriptStart', { get: () => timings.correlation?.script.start || timings.fetchEnd })
Object.defineProperty(timings, 'scriptEnd', { get: () => timings.correlation?.script.end || timings.registeredAt })
Object.defineProperty(timings, 'scriptStart', timingFactory(() => timings.correlation?.script.start || timings.fetchEnd))
Object.defineProperty(timings, 'scriptEnd', timingFactory(() => timings.correlation?.script.end || timings.registeredAt))
} catch (error) {
// Don't let stack parsing errors break anything
}
Expand Down
10 changes: 10 additions & 0 deletions src/common/v2/timing-factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
export function timingFactory (getLazyValue = () => {}, override) {
return {
get: () => (override !== undefined ? override : getLazyValue()),
set: (newValue) => { override = newValue }
}
}
28 changes: 25 additions & 3 deletions src/common/v2/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,26 @@ export const V2_TYPES = {
BA: 'BA'
}

/**
* Returns a single registered entity associated with a given iframe interface ID. Returns undefined if no entity is found.
* @param {string} iframeInterfaceId
* @param {*} agentRef the agent reference
* @returns {import("../../loaders/api/register-api-types").RegisterAPI|undefined}
*/
export function getRegisteredEntityByIframeInterfaceId (iframeInterfaceId, agentRef) {
if (!isValid(iframeInterfaceId, agentRef)) return undefined
const registeredEntities = agentRef.runtime.registeredEntities
return registeredEntities?.find(entity => entity.metadata.target.iframeInterfaceId === iframeInterfaceId)
}

/**
* Returns the registered target associated with a given ID. Returns an empty array if no target is found.
* @param {string|number} id
* @param {*} agentRef the agent reference
* @returns {import("../../interfaces/registered-entity").RegisterAPIMetadataTarget[]}
*/
export function getRegisteredTargetsFromId (id, agentRef) {
if (!id || !agentRef?.init.api.register.enabled) return []
if (!isValid(id, agentRef)) return []
const registeredEntities = agentRef.runtime.registeredEntities
return registeredEntities?.filter(entity => String(entity.metadata.target.id) === String(id)).map(entity => entity.metadata.target) || []
}
Expand All @@ -35,7 +47,7 @@ export function getRegisteredTargetsFromId (id, agentRef) {
* @returns {import("../../interfaces/registered-entity").RegisterAPIMetadataTarget[]}
*/
export function getRegisteredTargetsFromFilename (filename, agentRef) {
if (!filename || !agentRef?.init.api.register.enabled) return []
if (!isValid(filename, agentRef)) return []
const registeredEntities = agentRef.runtime.registeredEntities
return registeredEntities?.filter(entity => entity.metadata.timings?.asset?.endsWith(filename)).map(entity => entity.metadata.target) || []
}
Expand Down Expand Up @@ -92,7 +104,7 @@ export function shouldDuplicate (target, aggregateInstance) {
* @returns {Array} An array of targets found from the stack trace. If no targets are found or allowed, returns an array with undefined.
*/
export function findTargetsFromStackTrace (agentRef) {
if (!agentRef?.init.api.register.enabled) return [undefined]
if (!isValid(true, agentRef)) return [undefined]

const targets = []
try {
Expand All @@ -118,3 +130,13 @@ export function findTargetsFromStackTrace (agentRef) {
function supportsV2 (aggregateInstance) {
return aggregateInstance?.harvestEndpointVersion === 2
}

/**
* Determines if the given identifier and agent reference are valid for use for entity lookups and other operations in the utils methods. This is a common check that is used across multiple methods in this module to ensure that the necessary data is present and that the register API is enabled before attempting to perform operations that depend on those things.
* @param {*} identifier The identifier to check.
* @param {*} agentRef The agent reference to check.
* @returns {boolean} Returns true if the identifier and agent reference are valid, false otherwise.
*/
function isValid (identifier, agentRef) {
return !!identifier && !!agentRef?.init.api.register.enabled
}
14 changes: 1 addition & 13 deletions src/features/ajax/instrument/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { SUPPORTABILITY_METRIC } from '../../metrics/constants'
import { now } from '../../../common/timing/now'
import { hasUndefinedHostname } from '../../../common/deny-list/deny-list'
import { extractUrl } from '../../../common/url/extract-url'
import { addUrl } from '../../../common/url/add-url'

var handlers = ['load', 'error', 'abort', 'timeout']
var handlersLen = handlers.length
Expand Down Expand Up @@ -481,19 +482,6 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
}
}

function addUrl (ctx, url) {
var parsed = parseUrl(url)
var params = ctx.params || ctx

params.hostname = parsed.hostname
params.port = parsed.port
params.protocol = parsed.protocol
params.host = parsed.hostname + ':' + parsed.port
params.pathname = parsed.pathname
ctx.parsedOrigin = parsed
ctx.sameOrigin = parsed.sameOrigin
}

function parseResponseHeaders (headerStr) {
const headers = {}
if (!headerStr) return headers
Expand Down
5 changes: 5 additions & 0 deletions src/features/utils/instrument-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ export class InstrumentBase extends FeatureBase {
handle(SESSION_ERROR, [e], undefined, this.featureName, this.ee)
}

if (agentRef.init.api.register.allow_iframe_bridge) {
const { setupIframeMFEMessageListener } = await import(/* webpackChunkName: "iframe-message-handler" */ '../../loaders/configure/iframe-message-handler')
setupIframeMFEMessageListener(agentRef)
}

/**
* Note this try-catch differs from the one in Agent.run() in that it's placed later in a page's lifecycle and
* it's only responsible for aborting its one specific feature, rather than all.
Expand Down
Loading
Loading