Skip to content

Commit 498cc41

Browse files
committed
add tests
1 parent 7090606 commit 498cc41

7 files changed

Lines changed: 446 additions & 105 deletions

File tree

docs/warning-codes.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,14 @@
144144
### 71
145145
`An invalid feature mode was detected and set to "off".`
146146
### 72
147-
`RegisteredIframeEntity failed to transmit API data from iframe to window context`
147+
`RegisteredIframeEntity failed to transmit API data from an iframe to window context`
148148
### 73
149149
`RegisteredIframeEntity failed to register with window context`
150150
### 74
151151
`RegisteredIframeEntity rejected message from unauthorized origin`
152152
### 75
153153
`RegisteredIframeEntity rejected message with mismatched iframeInterfaceId`
154+
### 76
155+
`Agent rejected post message, could not match with existing entity`
156+
### 77
157+
`Agent rejected post message, could not validate origin`
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const prefix = 'newrelic-iframe-'
2+
3+
export const IFRAME_TIMING_UPDATE = prefix + 'timing-update'
4+
export const IFRAME_API = prefix + 'api'
5+
export const IFRAME_API_RESPONSE = prefix + 'api-response'
6+
export const IFRAME_VITALS_UPDATE = prefix + 'vitals-update'
7+
export const IFRAME_AJAX = prefix + 'ajax'

src/interfaces/registered-iframe-entity.js

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import { now } from '../common/timing/now'
1414
import { warn } from '../common/util/console'
1515
import { findScriptTimings } from '../common/v2/script-tracker'
1616
import { parseUrl } from '../common/url/parse-url'
17+
import { IFRAME_TIMING_UPDATE, IFRAME_API, IFRAME_API_RESPONSE, IFRAME_VITALS_UPDATE, IFRAME_AJAX } from '../common/constants/iframe-constants'
18+
import { castErrorEvent, castError } from '../features/jserrors/shared/cast-error'
19+
20+
const REGISTER = 'register' // define it here for now to prevent importing the full list of constants for build size.
1721

1822
/**
1923
* @typedef {import('../loaders/api/register-api-types').RegisterAPI} RegisterAPI
@@ -60,13 +64,13 @@ export class RegisteredIframeEntity {
6064
this.metadata.target = this.#targetDescriptor = opts
6165

6266
if (!isIFrameWindow(window)) {
63-
warn(71, 'Must be in iframe context to use this interface')
67+
warn(72)
6468
this.blocked = true
6569
return
6670
}
6771

6872
// Store the registration promise so other methods can wait for it
69-
this.#registrationPromise = this.#postMethodToAgent('register', [opts])
73+
this.#registrationPromise = this.#postMethodToAgent(REGISTER, [opts])
7074
.then(response => {
7175
// Merge updated metadata from parent (includes full target info with server-generated IDs)
7276
if (response?.metadata) {
@@ -104,24 +108,24 @@ export class RegisteredIframeEntity {
104108
})
105109

106110
globalScope.addEventListener('error', err => {
107-
this.noticeError(err)
108-
})
109-
110-
onCLS(({ value }) => {
111-
// send message to parent with CLS value and attributes
112-
}, { reportAllChanges: true })
113-
114-
onLCP(({ value }) => {
115-
// send message to parent with LCP value and attributes
111+
this.noticeError(castErrorEvent(err))
116112
})
117113

118-
onFCP(({ value }) => {
119-
// send message to parent with FCP value and attributes
114+
globalScope.addEventListener('unhandledrejection', event => {
115+
this.noticeError(castPromiseRejectionEvent(event))
120116
})
121117

122-
onINP(({ value }) => {
123-
// send message to parent with INP value and attributes
118+
const vitals = [[onCLS, 'cls'], [onLCP, 'lcp'], [onFCP, 'fcp'], [onINP, 'inp']]
119+
vitals.forEach(([vitalFn, property]) => {
120+
vitalFn(({ value }) => {
121+
// send message to parent with vital value and attributes
122+
this.#postMessageToParent(IFRAME_VITALS_UPDATE, {
123+
property,
124+
value
125+
})
126+
}, { reportAllChanges: property === 'cls' })
124127
})
128+
125129

126130
// Instrument ajax here using buffered resource timing so pre-registration entries are included.
127131
const initiators = { xmlhttprequest: 'xhr', fetch: 'fetch', beacon: 'beacon' }
@@ -130,9 +134,13 @@ export class RegisteredIframeEntity {
130134
const params = { status: resource.responseStatus }
131135
const metrics = { rxSize: resource.transferSize, duration: Math.floor(resource.duration), cbTime: 0 }
132136
addUrl(params, resource.name)
133-
// this.handler('xhr', [params, metrics, resource.startTime, resource.responseEnd, initiators[resource.initiatorType]], undefined, FEATURE_NAMES.ajax)
134-
135-
// send message to parent with ajax resource timing data HERE
137+
this.#postMessageToParent(IFRAME_AJAX, {
138+
params,
139+
metrics,
140+
start: resource.startTime,
141+
end: resource.responseEnd,
142+
initiatorType: initiators[resource.initiatorType]
143+
})
136144
}
137145
}
138146

@@ -145,7 +153,7 @@ export class RegisteredIframeEntity {
145153

146154
globalScope.addEventListener('message', (event) => {
147155
// Validate message structure
148-
if (event.data?.type !== 'newrelic-iframe-api-response') return
156+
if (event.data?.type !== IFRAME_API_RESPONSE) return
149157

150158
// Validate origin if we have specific allowed origins
151159
if (!this.#isAllowedOrigin(event.origin)) {
@@ -155,7 +163,7 @@ export class RegisteredIframeEntity {
155163

156164
// Validate iframeInterfaceId to prevent spoofing (message must be for this instance)
157165
if (event.data.iframeInterfaceId !== this.#iframeInterfaceId) {
158-
warn(75, 'Rejected message with mismatched iframeInterfaceId')
166+
warn(75)
159167
return
160168
}
161169

@@ -190,15 +198,18 @@ export class RegisteredIframeEntity {
190198
/**
191199
* Low-level helper to send postMessage to parent window with error handling
192200
* @private
193-
* @param {object} message - The message object to send
201+
* @param {string} type - The message type to send
202+
* @param {object} data - The message payload to send
203+
* @param {number} [timestamp=now()] - The relative timestamp of the message trigger. defaults to the postMessage call time if not provided.
204+
* @param {{resolve: Function, reject: Function}} [resolvers] - Optional promise resolvers for handling response
194205
*/
195-
#postMessageToParent (type, data, resolvers) {
206+
#postMessageToParent (type, data, timestamp = now(), resolvers) {
196207
try {
197208
this.#openPending(data.messageId, resolvers)
198209
globalScope.parent.postMessage({
199210
type,
200211
target: this.#targetDescriptor,
201-
timestamp: now(),
212+
timestamp,
202213
iframeInterfaceId: this.#iframeInterfaceId,
203214
...data
204215
}, this.#parentOrigin)
@@ -216,7 +227,7 @@ export class RegisteredIframeEntity {
216227
#postTimingToAgent (property, value) {
217228
if (this.blocked) return
218229

219-
this.#postMessageToParent('newrelic-iframe-timing-update', {
230+
this.#postMessageToParent(IFRAME_TIMING_UPDATE, {
220231
property,
221232
value
222233
})
@@ -232,20 +243,21 @@ export class RegisteredIframeEntity {
232243
*/
233244
#postMethodToAgent (method, args) {
234245
if (this.blocked) {
235-
return Promise.reject(new Error('Iframe interface is blocked'))
246+
const msg = 'Iframe interface is blocked'
247+
warn(72, msg)
248+
return Promise.reject(new Error(msg))
236249
}
237-
238-
// For all methods except register itself, wait for registration to complete first
239-
const waitPromise = method === 'register' ? Promise.resolve() : this.#registrationPromise
250+
const timestamp = now() // For all methods except register itself, wait for registration to complete first
251+
const waitPromise = method === REGISTER ? Promise.resolve() : this.#registrationPromise
240252

241253
return waitPromise.then(() => new Promise((resolve, reject) => {
242254
try {
243255
const messageId = ++this.#messageIdCounter
244-
this.#postMessageToParent('newrelic-iframe-api', {
256+
this.#postMessageToParent(IFRAME_API, {
245257
messageId,
246258
method,
247259
args
248-
}, {
260+
}, timestamp, {
249261
resolve,
250262
reject
251263
})
@@ -291,7 +303,7 @@ export class RegisteredIframeEntity {
291303
@returns {Promise<import('../loaders/api/register-api-types').RegisterAPI>} Returns a promise that resolves with an object that contains the available API methods and configurations to use with the external caller. See loaders/api/api.js for more information.
292304
*/
293305
register (target) {
294-
return this.#postMethodToAgent('register', [target])
306+
return this.#postMethodToAgent(REGISTER, [target])
295307
}
296308

297309
/**
@@ -350,7 +362,7 @@ export class RegisteredIframeEntity {
350362
* @param {object} [customAttributes] An object containing name/value pairs representing custom attributes.
351363
*/
352364
noticeError (error, customAttributes) {
353-
this.#postMethodToAgent('noticeError', [error, customAttributes])
365+
this.#postMethodToAgent('noticeError', [castError(error), customAttributes])
354366
}
355367

356368
/**

src/loaders/api/register.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function register (agentRef, target) {
7878
target.blocked = false
7979
if (typeof target.tags !== 'object' || target.tags === null || Array.isArray(target.tags)) target.tags = {}
8080
target.parent ??= {
81-
get id () { return agentRef.runtime.appMetadata.agents[0].entityGuid }, // getter because this is asyncronously set
81+
get id () { return agentRef.runtime.appMetadata.agents?.[0].entityGuid }, // getter because this is asyncronously set
8282
type: V2_TYPES.BA
8383
}
8484

0 commit comments

Comments
 (0)