@newrelic/browser-agent 1.319.0 → 1.320.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/cjs/common/config/init.js +17 -2
- package/dist/cjs/common/config/runtime.js +2 -1
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/constants/iframe-constants.js +16 -0
- package/dist/cjs/common/url/add-url.js +23 -0
- package/dist/cjs/common/util/console.js +40 -33
- package/dist/cjs/common/v2/mfe-vitals.js +9 -2
- package/dist/cjs/common/v2/script-correlation.js +2 -0
- package/dist/cjs/common/v2/script-tracker-constants.js +13 -0
- package/dist/cjs/common/v2/script-tracker.js +54 -29
- package/dist/cjs/common/v2/timing-factory.js +18 -0
- package/dist/cjs/common/v2/utils.js +100 -6
- package/dist/cjs/features/ajax/instrument/index.js +4 -14
- package/dist/cjs/features/utils/instrument-base.js +13 -0
- package/dist/cjs/interfaces/registered-iframe-entity.js +527 -0
- package/dist/cjs/loaders/api/register.js +10 -6
- package/dist/cjs/loaders/configure/configure.js +14 -0
- package/dist/cjs/loaders/configure/iframe-message-handler.js +270 -0
- package/dist/esm/common/config/init.js +17 -2
- package/dist/esm/common/config/runtime.js +2 -1
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/constants/iframe-constants.js +10 -0
- package/dist/esm/common/url/add-url.js +16 -0
- package/dist/esm/common/util/console.js +40 -33
- package/dist/esm/common/v2/mfe-vitals.js +9 -2
- package/dist/esm/common/v2/script-correlation.js +2 -0
- package/dist/esm/common/v2/script-tracker-constants.js +7 -0
- package/dist/esm/common/v2/script-tracker.js +54 -29
- package/dist/esm/common/v2/timing-factory.js +12 -0
- package/dist/esm/common/v2/utils.js +97 -6
- package/dist/esm/features/ajax/instrument/index.js +1 -11
- package/dist/esm/features/utils/instrument-base.js +13 -0
- package/dist/esm/interfaces/registered-iframe-entity.js +518 -0
- package/dist/esm/loaders/api/register.js +10 -6
- package/dist/esm/loaders/configure/configure.js +15 -1
- package/dist/esm/loaders/configure/iframe-message-handler.js +263 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/common/config/init.d.ts.map +1 -1
- package/dist/types/common/config/runtime.d.ts.map +1 -1
- package/dist/types/common/constants/iframe-constants.d.ts +11 -0
- package/dist/types/common/constants/iframe-constants.d.ts.map +1 -0
- package/dist/types/common/url/add-url.d.ts +2 -0
- package/dist/types/common/url/add-url.d.ts.map +1 -0
- package/dist/types/common/util/console.d.ts +40 -33
- package/dist/types/common/util/console.d.ts.map +1 -1
- package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -1
- package/dist/types/common/v2/script-correlation.d.ts +2 -0
- package/dist/types/common/v2/script-correlation.d.ts.map +1 -1
- package/dist/types/common/v2/script-tracker-constants.d.ts +7 -0
- package/dist/types/common/v2/script-tracker-constants.d.ts.map +1 -0
- package/dist/types/common/v2/script-tracker.d.ts +4 -1
- package/dist/types/common/v2/script-tracker.d.ts.map +1 -1
- package/dist/types/common/v2/timing-factory.d.ts +9 -0
- package/dist/types/common/v2/timing-factory.d.ts.map +1 -0
- package/dist/types/common/v2/utils.d.ts +37 -0
- package/dist/types/common/v2/utils.d.ts.map +1 -1
- package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
- package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
- package/dist/types/interfaces/registered-iframe-entity.d.ts +116 -0
- package/dist/types/interfaces/registered-iframe-entity.d.ts.map +1 -0
- package/dist/types/loaders/configure/configure.d.ts.map +1 -1
- package/dist/types/loaders/configure/iframe-message-handler.d.ts +17 -0
- package/dist/types/loaders/configure/iframe-message-handler.d.ts.map +1 -0
- package/package.json +9 -1
- package/src/common/config/init.js +13 -2
- package/src/common/config/runtime.js +2 -1
- package/src/common/constants/iframe-constants.js +11 -0
- package/src/common/url/add-url.js +18 -0
- package/src/common/util/console.js +40 -33
- package/src/common/v2/mfe-vitals.js +7 -4
- package/src/common/v2/script-correlation.js +2 -0
- package/src/common/v2/script-tracker-constants.js +7 -0
- package/src/common/v2/script-tracker.js +65 -26
- package/src/common/v2/timing-factory.js +10 -0
- package/src/common/v2/utils.js +102 -6
- package/src/features/ajax/instrument/index.js +1 -13
- package/src/features/utils/instrument-base.js +12 -0
- package/src/interfaces/registered-iframe-entity.js +482 -0
- package/src/loaders/api/register.js +9 -5
- package/src/loaders/configure/configure.js +15 -1
- package/src/loaders/configure/iframe-message-handler.js +244 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.handleMethodCall = handleMethodCall;
|
|
7
|
+
exports.setupIframeMFEMessageListener = setupIframeMFEMessageListener;
|
|
8
|
+
var _console = require("../../common/util/console");
|
|
9
|
+
var _utils = require("../../common/v2/utils");
|
|
10
|
+
var _iframeConstants = require("../../common/constants/iframe-constants");
|
|
11
|
+
var _constants = require("../api/constants");
|
|
12
|
+
var _handle = require("../../common/event-emitter/handle");
|
|
13
|
+
var _features = require("../features/features");
|
|
14
|
+
var _stringify = require("../../common/util/stringify");
|
|
15
|
+
var _registerHandler = require("../../common/event-emitter/register-handler");
|
|
16
|
+
var _drain = require("../../common/drain/drain");
|
|
17
|
+
var _runtime = require("../../common/constants/runtime");
|
|
18
|
+
/**
|
|
19
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
20
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Retrieves the registered entity associated with the iframeInterfaceId from the event data, and validates that the origin of the message event matches the expected origin for that entity.
|
|
25
|
+
* @param {MessageEvent} event - The message event containing the method call or registration request
|
|
26
|
+
* @param {Object} agent - The browser agent instance
|
|
27
|
+
* @returns {Object|undefined} - The registered entity if it exists and the origin is valid, otherwise undefined
|
|
28
|
+
*/
|
|
29
|
+
function getValidEntity(event, agent) {
|
|
30
|
+
try {
|
|
31
|
+
const {
|
|
32
|
+
iframeInterfaceId
|
|
33
|
+
} = event.data;
|
|
34
|
+
if (!iframeInterfaceId) return;
|
|
35
|
+
const entity = (0, _utils.getRegisteredEntityByIframeInterfaceId)(iframeInterfaceId, agent);
|
|
36
|
+
if (!isValidOrigin(event, entity)) return;
|
|
37
|
+
return entity;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// couldnt get entity or validate origin, just let the method return undefined
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function isSafeProperty(obj, property) {
|
|
43
|
+
return property in obj && typeof property === 'string' && !['__proto__', 'constructor', 'prototype'].includes(property);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Handles timing property updates from iframes. Mutates the timings object on the entity's metadata with the new value for the specified property.
|
|
48
|
+
* @param {MessageEvent} event - The message event containing the timing update
|
|
49
|
+
* @param {Object} agent - The browser agent instance
|
|
50
|
+
* @returns
|
|
51
|
+
*/
|
|
52
|
+
function handleTimingUpdate(event, agent) {
|
|
53
|
+
const entity = getValidEntity(event, agent);
|
|
54
|
+
if (!entity) return;
|
|
55
|
+
if (!entity.metadata.timings) return;
|
|
56
|
+
event.data.entries?.forEach(({
|
|
57
|
+
property,
|
|
58
|
+
value
|
|
59
|
+
}) => {
|
|
60
|
+
if (isSafeProperty(entity.metadata.timings, property)) {
|
|
61
|
+
entity.metadata.timings[property] = value;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Handles vitals property updates from iframes. Mutates the vitals object on the entity's metadata with the new value for the specified property.
|
|
68
|
+
* @param {MessageEvent} event - The message event containing the vitals update
|
|
69
|
+
* @param {Object} agent - The browser agent instance
|
|
70
|
+
* @returns
|
|
71
|
+
*/
|
|
72
|
+
function handleVitalsUpdate(event, agent) {
|
|
73
|
+
const entity = getValidEntity(event, agent);
|
|
74
|
+
if (!entity) return;
|
|
75
|
+
if (!entity.metadata?.vitals) return;
|
|
76
|
+
event.data.entries?.forEach(({
|
|
77
|
+
property,
|
|
78
|
+
value
|
|
79
|
+
}) => {
|
|
80
|
+
if (isSafeProperty(entity.metadata.vitals, property) && (!!Number(value) || value === 0)) {
|
|
81
|
+
entity.metadata.vitals[property].value = value;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Handles ajax events from iframes and forwards them to the agent's event emitter
|
|
88
|
+
* @param {MessageEvent} event - The message event containing the method call or registration request
|
|
89
|
+
* @param {Object} agent - The browser agent instance
|
|
90
|
+
* @returns
|
|
91
|
+
*/
|
|
92
|
+
function handleAjax(event, agent) {
|
|
93
|
+
const entity = getValidEntity(event, agent);
|
|
94
|
+
if (!entity) return;
|
|
95
|
+
const {
|
|
96
|
+
entries
|
|
97
|
+
} = event.data;
|
|
98
|
+
entries?.forEach(({
|
|
99
|
+
params,
|
|
100
|
+
metrics,
|
|
101
|
+
start,
|
|
102
|
+
end,
|
|
103
|
+
initiatorType
|
|
104
|
+
}) => {
|
|
105
|
+
(0, _handle.handle)('xhr', [params, metrics, start, end, initiatorType, entity.metadata.target], undefined, _features.FEATURE_NAMES.ajax, agent.ee);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Validates that the origin of the message event matches the expected origin for the registered entity
|
|
111
|
+
* @param {MessageEvent} event - The message event containing the method call or registration request
|
|
112
|
+
* @param {Object} entity - The registered entity associated with the iframeInterfaceId from the event data
|
|
113
|
+
* @returns {boolean}
|
|
114
|
+
*/
|
|
115
|
+
function isValidOrigin(event, entity) {
|
|
116
|
+
if (!entity) return false;
|
|
117
|
+
try {
|
|
118
|
+
// "null" is the literal serialization the spec uses for every opaque origin (sandboxed iframes
|
|
119
|
+
// without allow-same-origin, data:/javascript: URLs, etc.) -- it does not uniquely identify a
|
|
120
|
+
// single origin, so treating two "null" origins as a match would let an unrelated opaque-origin
|
|
121
|
+
// frame impersonate the one that actually registered.
|
|
122
|
+
if (event.origin === 'null' || entity.metadata.target.iframeOrigin === 'null') return false;
|
|
123
|
+
return event.origin === entity.metadata.target.iframeOrigin;
|
|
124
|
+
} catch (e) {
|
|
125
|
+
(0, _console.warn)(77, e);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Handles both entity registration and method calls on existing entities
|
|
132
|
+
* @async
|
|
133
|
+
* @param {MessageEvent} event - The message event containing the method call or registration request
|
|
134
|
+
* @param {Object} agent - The browser agent instance
|
|
135
|
+
* @returns {Promise<{entity: Object|null, result: any}>}
|
|
136
|
+
*/
|
|
137
|
+
async function handleMethodCall(event, agent) {
|
|
138
|
+
const {
|
|
139
|
+
target,
|
|
140
|
+
iframeInterfaceId,
|
|
141
|
+
timestamp
|
|
142
|
+
} = event.data;
|
|
143
|
+
// Method calls are always sent one-at-a-time (each has its own postMessage response), so only
|
|
144
|
+
// the first entry is ever populated -- but the payload shape stays consistent with every other
|
|
145
|
+
// iframe message type (timing/vitals/ajax), which all wrap their data in an `entries` array.
|
|
146
|
+
const [{
|
|
147
|
+
method,
|
|
148
|
+
args
|
|
149
|
+
} = {}] = event.data.entries || [];
|
|
150
|
+
const output = {
|
|
151
|
+
entity: null,
|
|
152
|
+
result: null
|
|
153
|
+
};
|
|
154
|
+
// Registration of a new entity needs to be handled differently than method calls on existing entities
|
|
155
|
+
if (method === _constants.REGISTER) {
|
|
156
|
+
// "null" is the opaque-origin serialization (sandboxed iframes, data:/javascript: URLs) -- it
|
|
157
|
+
// doesn't identify a single origin, so an entity registered from one can never be safely
|
|
158
|
+
// distinguished from another later on (see isValidOrigin). Reject up front rather than
|
|
159
|
+
// registering an entity that would silently fail every subsequent call.
|
|
160
|
+
if (event.origin === 'null') {
|
|
161
|
+
(0, _console.warn)(74, event.origin);
|
|
162
|
+
return output;
|
|
163
|
+
}
|
|
164
|
+
const iframeDomains = agent.init.api.register.iframe_domains;
|
|
165
|
+
if (iframeDomains.length && !iframeDomains.includes(event.origin)) {
|
|
166
|
+
(0, _console.warn)(74, event.origin);
|
|
167
|
+
return output;
|
|
168
|
+
}
|
|
169
|
+
const targetData = args?.[0] || target || {};
|
|
170
|
+
if (!agent[_constants.REGISTER]) {
|
|
171
|
+
(0, _console.warn)(35, _constants.REGISTER);
|
|
172
|
+
return output;
|
|
173
|
+
}
|
|
174
|
+
const freshTarget = {
|
|
175
|
+
id: targetData.id,
|
|
176
|
+
name: targetData.name,
|
|
177
|
+
type: targetData.type,
|
|
178
|
+
version: targetData.version,
|
|
179
|
+
tags: targetData.tags
|
|
180
|
+
};
|
|
181
|
+
output.entity = agent[_constants.REGISTER](freshTarget);
|
|
182
|
+
output.entity.metadata.target.iframeInterfaceId = iframeInterfaceId;
|
|
183
|
+
output.entity.metadata.target.iframeOrigin = event.origin;
|
|
184
|
+
return output;
|
|
185
|
+
}
|
|
186
|
+
const entity = (0, _utils.getRegisteredEntityByIframeInterfaceId)(iframeInterfaceId, agent);
|
|
187
|
+
if (!isValidOrigin(event, entity)) {
|
|
188
|
+
(0, _console.warn)(76);
|
|
189
|
+
return output;
|
|
190
|
+
}
|
|
191
|
+
output.entity = entity;
|
|
192
|
+
const methodFn = entity[method];
|
|
193
|
+
if (!isSafeProperty(entity, method) || typeof methodFn !== 'function') {
|
|
194
|
+
(0, _console.warn)(35, method);
|
|
195
|
+
return output;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Supply the timestamp captured inside the iframe at call time (from event.data.timestamp)
|
|
199
|
+
entity.metadata.events.latestTimestamp = timestamp;
|
|
200
|
+
output.result = await methodFn.apply(entity, args || []);
|
|
201
|
+
return output;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Serializes entity metadata for postMessage response.
|
|
206
|
+
* Strips out any non-serializable properties and returns a plain object.
|
|
207
|
+
* postMessage cant handle complex objects that cause DataCloneErrors to throw like functions, circular references, etc.
|
|
208
|
+
* @param {Object} entity - The registered entity whose metadata is to be serialized. Uses a pattern of stringify and parse to remove any non-serializable properties from the metadata object.
|
|
209
|
+
* @returns {Object} - The serialized metadata object
|
|
210
|
+
*/
|
|
211
|
+
function serializeMetadata(entity) {
|
|
212
|
+
const meta = entity.metadata;
|
|
213
|
+
return JSON.parse((0, _stringify.stringify)(meta));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Sends a postMessage response to the iframe
|
|
218
|
+
* @param {MessageEvent} event - The message event containing the method call or registration request
|
|
219
|
+
* @param {Object} payload - The response payload to send back to the iframe
|
|
220
|
+
*/
|
|
221
|
+
function sendResponse(event, payload) {
|
|
222
|
+
event.source.postMessage({
|
|
223
|
+
type: _iframeConstants.IFRAME_API_RESPONSE,
|
|
224
|
+
messageId: event.data.messageId,
|
|
225
|
+
iframeInterfaceId: event.data.iframeInterfaceId,
|
|
226
|
+
...payload
|
|
227
|
+
}, event.origin);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Sets up a postMessage listener to handle API calls and timing updates from iframes
|
|
232
|
+
* @param {Object} agent The agent instance
|
|
233
|
+
*/
|
|
234
|
+
function setupIframeMFEMessageListener(agent) {
|
|
235
|
+
if (!_runtime.isBrowserScope || !window.addEventListener || agent.runtime.listeningForIframeMessages) return;
|
|
236
|
+
(0, _registerHandler.registerHandler)('iframe-message', async event => {
|
|
237
|
+
if (!event.data) return;
|
|
238
|
+
switch (event.data.type) {
|
|
239
|
+
case _iframeConstants.IFRAME_TIMING_UPDATE:
|
|
240
|
+
handleTimingUpdate(event, agent);
|
|
241
|
+
return;
|
|
242
|
+
case _iframeConstants.IFRAME_VITALS_UPDATE:
|
|
243
|
+
handleVitalsUpdate(event, agent);
|
|
244
|
+
return;
|
|
245
|
+
case _iframeConstants.IFRAME_AJAX:
|
|
246
|
+
handleAjax(event, agent);
|
|
247
|
+
return;
|
|
248
|
+
case _iframeConstants.IFRAME_API:
|
|
249
|
+
try {
|
|
250
|
+
if (!event.source || !event.data.messageId || !event.data.entries?.[0]?.method) return;
|
|
251
|
+
const {
|
|
252
|
+
entity,
|
|
253
|
+
result
|
|
254
|
+
} = await handleMethodCall(event, agent);
|
|
255
|
+
const metadata = entity ? serializeMetadata(entity) : undefined;
|
|
256
|
+
sendResponse(event, {
|
|
257
|
+
result,
|
|
258
|
+
metadata
|
|
259
|
+
});
|
|
260
|
+
} catch (error) {
|
|
261
|
+
const errorMessage = error?.message || String(error);
|
|
262
|
+
sendResponse(event, {
|
|
263
|
+
error: errorMessage
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}, 'IFRAME', agent.ee);
|
|
268
|
+
(0, _drain.drain)(agent, 'IFRAME', true); // drain any buffered iframe messages that were received before the listener was set up
|
|
269
|
+
agent.runtime.listeningForIframeMessages = true;
|
|
270
|
+
}
|
|
@@ -23,7 +23,9 @@ const InitModelFn = () => {
|
|
|
23
23
|
feature_flags: [],
|
|
24
24
|
experimental: {
|
|
25
25
|
register: false,
|
|
26
|
-
resources: false
|
|
26
|
+
resources: false,
|
|
27
|
+
iframe_bridge: false,
|
|
28
|
+
iframe_domains: []
|
|
27
29
|
},
|
|
28
30
|
mask_selector: '*',
|
|
29
31
|
block_selector: '[data-nr-block]',
|
|
@@ -63,7 +65,20 @@ const InitModelFn = () => {
|
|
|
63
65
|
set enabled(val) {
|
|
64
66
|
hiddenState.experimental.register = val;
|
|
65
67
|
},
|
|
66
|
-
duplicate_data_to_container: false
|
|
68
|
+
duplicate_data_to_container: false,
|
|
69
|
+
// experimental iframe bridge feature
|
|
70
|
+
get allow_iframe_bridge() {
|
|
71
|
+
return hiddenState.feature_flags.includes(FEATURE_FLAGS.IFRAME_BRIDGE) || hiddenState.experimental.iframe_bridge;
|
|
72
|
+
},
|
|
73
|
+
set allow_iframe_bridge(val) {
|
|
74
|
+
hiddenState.experimental.iframe_bridge = val;
|
|
75
|
+
},
|
|
76
|
+
get iframe_domains() {
|
|
77
|
+
return hiddenState.experimental.iframe_domains;
|
|
78
|
+
},
|
|
79
|
+
set iframe_domains(val) {
|
|
80
|
+
if (Array.isArray(val)) hiddenState.experimental.iframe_domains = val;else warn(1, val);
|
|
81
|
+
}
|
|
67
82
|
}
|
|
68
83
|
},
|
|
69
84
|
browser_consent_mode: {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
export const prefix = 'newrelic-iframe-';
|
|
6
|
+
export const IFRAME_TIMING_UPDATE = prefix + 'timing-update';
|
|
7
|
+
export const IFRAME_API = prefix + 'api';
|
|
8
|
+
export const IFRAME_API_RESPONSE = prefix + 'api-response';
|
|
9
|
+
export const IFRAME_VITALS_UPDATE = prefix + 'vitals-update';
|
|
10
|
+
export const IFRAME_AJAX = prefix + 'ajax';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { parseUrl } from './parse-url.js';
|
|
6
|
+
export function addUrl(ctx, url) {
|
|
7
|
+
var parsed = parseUrl(url);
|
|
8
|
+
var params = ctx.params || ctx;
|
|
9
|
+
params.hostname = parsed.hostname;
|
|
10
|
+
params.port = parsed.port;
|
|
11
|
+
params.protocol = parsed.protocol;
|
|
12
|
+
params.host = parsed.hostname + ':' + parsed.port;
|
|
13
|
+
params.pathname = parsed.pathname;
|
|
14
|
+
ctx.parsedOrigin = parsed;
|
|
15
|
+
ctx.sameOrigin = parsed.sameOrigin;
|
|
16
|
+
}
|
|
@@ -16,39 +16,39 @@ import { dispatchGlobalEvent } from '../dispatch/global-event';
|
|
|
16
16
|
*
|
|
17
17
|
* | Code | Message |
|
|
18
18
|
* |------|---------|
|
|
19
|
-
* | 1 | An error occurred while setting a property of a Configurable |
|
|
20
|
-
* | 2 | An error occurred while setting a Configurable |
|
|
21
|
-
* | 3 | Setting a Configurable requires an object as input |
|
|
22
|
-
* | 4 | Setting a Configurable requires a model to set its initial properties |
|
|
19
|
+
* | 1 | An error occurred while setting a property of a Configurable. |
|
|
20
|
+
* | 2 | An error occurred while setting a Configurable. |
|
|
21
|
+
* | 3 | Setting a Configurable requires an object as input. |
|
|
22
|
+
* | 4 | Setting a Configurable requires a model to set its initial properties. |
|
|
23
23
|
* | 5 | An invalid session_replay.mask_selector was provided. \* will be used. |
|
|
24
|
-
* | 6 | An invalid session_replay.block_selector was provided and will not be used |
|
|
25
|
-
* | 7 | An invalid session_replay.mask_input_option was provided and will not be used |
|
|
26
|
-
* | 8 | Shared context requires an object as input |
|
|
27
|
-
* | 9 | An error occurred while setting SharedContext |
|
|
28
|
-
* | 10 | Failed to read from storage API |
|
|
29
|
-
* | 11 | Failed to write to the storage API |
|
|
24
|
+
* | 6 | An invalid session_replay.block_selector was provided and will not be used. |
|
|
25
|
+
* | 7 | An invalid session_replay.mask_input_option was provided and will not be used. |
|
|
26
|
+
* | 8 | Shared context requires an object as input. |
|
|
27
|
+
* | 9 | An error occurred while setting SharedContext. |
|
|
28
|
+
* | 10 | Failed to read from storage API. |
|
|
29
|
+
* | 11 | Failed to write to the storage API. |
|
|
30
30
|
* | 12 | An obfuscation replacement rule was detected missing a "regex" value. |
|
|
31
|
-
* | 13 | An obfuscation replacement rule contains a "regex" value with an invalid type (must be a string or RegExp) |
|
|
32
|
-
* | 14 | An obfuscation replacement rule contains a "replacement" value with an invalid type (must be a string) |
|
|
33
|
-
* | 15 | An error occurred while intercepting XHR |
|
|
34
|
-
* | 16 | Could not cast log message to string |
|
|
31
|
+
* | 13 | An obfuscation replacement rule contains a "regex" value with an invalid type (must be a string or RegExp). |
|
|
32
|
+
* | 14 | An obfuscation replacement rule contains a "replacement" value with an invalid type (must be a string). |
|
|
33
|
+
* | 15 | An error occurred while intercepting XHR. |
|
|
34
|
+
* | 16 | Could not cast log message to string. |
|
|
35
35
|
* | 17 | Could not calculate New Relic server time. Agent shutting down. |
|
|
36
36
|
* | 18 | RUM call failed. Agent shutting down. |
|
|
37
37
|
* | 19 | SPA scheduler is not initialized. Saved interaction is not sent! |
|
|
38
38
|
* | 20 | A problem occurred when starting up session manager. This page will not start or extend any session. |
|
|
39
39
|
* | 21 | Failed to initialize the agent. Could not determine the runtime environment. |
|
|
40
40
|
* | 22 | Failed to initialize all enabled instrument classes (agent aborted) - |
|
|
41
|
-
* | 23 | An unexpected issue occurred |
|
|
41
|
+
* | 23 | An unexpected issue occurred. |
|
|
42
42
|
* | 24 | Something prevented the agent from instrumenting. |
|
|
43
43
|
* | 25 | Something prevented the agent from being downloaded. |
|
|
44
44
|
* | 26 | Failed to initialize instrument classes. |
|
|
45
45
|
* | 27 | Downloading runtime APIs failed... |
|
|
46
46
|
* | 28 | The Browser Agent is attempting to send a very large payload. This is usually tied to large amounts of custom attributes. Please check your configurations. |
|
|
47
|
-
* | 29 | Failed to wrap logger: invalid argument(s) |
|
|
48
|
-
* | 30 | Invalid log level |
|
|
49
|
-
* | 31 | Ignored log: Log is larger than maximum payload size |
|
|
50
|
-
* | 32 | Ignored log: Invalid message |
|
|
51
|
-
* | 33 | Session Replay Aborted |
|
|
47
|
+
* | 29 | Failed to wrap logger: invalid argument(s). |
|
|
48
|
+
* | 30 | Invalid log level. |
|
|
49
|
+
* | 31 | Ignored log: Log is larger than maximum payload size. |
|
|
50
|
+
* | 32 | Ignored log: Invalid message. |
|
|
51
|
+
* | 33 | Session Replay Aborted. |
|
|
52
52
|
* | 34 | Downloading and initializing a feature failed... |
|
|
53
53
|
* | 35 | Call to agent api failed. The API is not currently initialized. |
|
|
54
54
|
* | 36 | A feature is enabled but one or more dependent features have not been initialized. This may cause unintended consequences or missing data... |
|
|
@@ -57,7 +57,7 @@ import { dispatchGlobalEvent } from '../dispatch/global-event';
|
|
|
57
57
|
* | 39 | Failed to execute setCustomAttribute. Name must be a string type. |
|
|
58
58
|
* | 40 | Failed to execute setCustomAttribute. Non-null value must be a string, number or boolean type. |
|
|
59
59
|
* | 41 | Failed to execute setUserId. Non-null value must be a string type. |
|
|
60
|
-
* | 42 | Failed to execute setApplicationVersion. Expected <String \| null
|
|
60
|
+
* | 42 | Failed to execute setApplicationVersion. Expected <String \| null>. |
|
|
61
61
|
* | 43 | Agent not configured properly. |
|
|
62
62
|
* | 44 | Invalid object passed to generic event aggregate. Missing "eventType". |
|
|
63
63
|
* | 45 | An internal agent process failed to execute. |
|
|
@@ -66,27 +66,34 @@ import { dispatchGlobalEvent } from '../dispatch/global-event';
|
|
|
66
66
|
* | 48 | Supplied an invalid API target. Must be an <Object> that contains valid (string) id and name properties. |
|
|
67
67
|
* | 49 | Supplied API target is missing an entityGuid. Some APIs may not behave correctly without a valid entityGuid (ex. logs). |
|
|
68
68
|
* | 50 | Failed to connect. Cannot allow registered API. |
|
|
69
|
-
* | 51 | Container agent is not available to register with. Can not connect |
|
|
69
|
+
* | 51 | Container agent is not available to register with. Can not connect. |
|
|
70
70
|
* | 52 | Unexpected problem encountered. There should be at least one app for harvest! |
|
|
71
|
-
* | 53 | Did not receive a valid entityGuid from connection response |
|
|
72
|
-
* | 54 | An experimental feature is being used. Support can not be offered for issues |
|
|
73
|
-
* | 55 | Register API has been disabled on the container agent |
|
|
74
|
-
* | 56 | Could not find a matching entity to store data |
|
|
71
|
+
* | 53 | Did not receive a valid entityGuid from connection response. |
|
|
72
|
+
* | 54 | An experimental feature is being used. Support can not be offered for issues. |
|
|
73
|
+
* | 55 | Register API has been disabled on the container agent. |
|
|
74
|
+
* | 56 | Could not find a matching entity to store data. |
|
|
75
75
|
* | 57 | Failed to execute measure. Arguments must have valid types. |
|
|
76
76
|
* | 58 | Failed to execute measure. Resulting duration must be non-negative. |
|
|
77
|
-
* | 59 | Session replay harvested before a session trace payload could be sent. This could be problematic for replays that rely on a trace |
|
|
78
|
-
* | 60 | Session trace aborted |
|
|
77
|
+
* | 59 | Session replay harvested before a session trace payload could be sent. This could be problematic for replays that rely on a trace. |
|
|
78
|
+
* | 60 | Session trace aborted. |
|
|
79
79
|
* | 61 | Timestamps must be non-negative and end time cannot be before start time. |
|
|
80
|
-
* | 62 | Timestamp must be a unix timestamp greater than the page origin time |
|
|
81
|
-
* | 63 | A single event was larger than the maximum allowed payload size |
|
|
80
|
+
* | 62 | Timestamp must be a unix timestamp greater than the page origin time. |
|
|
81
|
+
* | 63 | A single event was larger than the maximum allowed payload size. |
|
|
82
82
|
* | 64 | Required globals have been mutated before being accessed by the browser agent. This can cause issues and should be avoided. |
|
|
83
|
-
* | 65 | Consent API argument must be boolean or undefined |
|
|
84
|
-
* | 66 | A new agent session has started |
|
|
83
|
+
* | 65 | Consent API argument must be boolean or undefined. |
|
|
84
|
+
* | 66 | A new agent session has started. |
|
|
85
85
|
* | 67 | The "spa" feature has been deprecated and disabled. Please use/import "soft_navigations" instead for tracking of BrowserInteraction data. |
|
|
86
86
|
* | 68 | API has been deregistered and can no longer be used. Call "register" API again with credentials to start over. |
|
|
87
|
-
* | 69 | More than one Browser agent is running on the page |
|
|
87
|
+
* | 69 | More than one Browser agent is running on the page. |
|
|
88
88
|
* | 70 | 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. |
|
|
89
89
|
* | 71 | An invalid feature mode was detected and set to "off". |
|
|
90
|
+
* | 72 | RegisteredIframeEntity failed to transmit API data from an iframe to window context. |
|
|
91
|
+
* | 73 | RegisteredIframeEntity failed to register with window context. |
|
|
92
|
+
* | 74 | RegisteredIframeEntity rejected message from unauthorized origin. |
|
|
93
|
+
* | 75 | RegisteredIframeEntity rejected message with mismatched iframeInterfaceId. |
|
|
94
|
+
* | 76 | Agent rejected post message, could not match with existing entity. |
|
|
95
|
+
* | 77 | Agent rejected post message, could not validate origin. |
|
|
96
|
+
* | 78 | RegisteredIframeEntity could not determine parent origin and will not register, to avoid trusting messages from any origin. |
|
|
90
97
|
*
|
|
91
98
|
* @param {number} code The warning code to emit, which will be used to link to the warning code documentation
|
|
92
99
|
* @param {*} [secondary] Secondary data to include, usually an extra message, error or object
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { globalScope, isBrowserScope } from '../constants/runtime';
|
|
7
7
|
import { now } from '../timing/now';
|
|
8
|
+
import { CORRELATION_STALE_THRESHOLD_MS } from './script-tracker-constants';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* @typedef {import('../../loaders/api/register-api-types').RegisterAPITimings} RegisterAPITimings
|
|
@@ -134,11 +135,17 @@ export function trackMFEVitals(target, timings) {
|
|
|
134
135
|
fcp: {
|
|
135
136
|
get value() {
|
|
136
137
|
return getTimeRelativeToScriptStart(fcpObservedAt);
|
|
138
|
+
},
|
|
139
|
+
set value(v) {
|
|
140
|
+
fcpObservedAt = v;
|
|
137
141
|
}
|
|
138
142
|
},
|
|
139
143
|
lcp: {
|
|
140
144
|
get value() {
|
|
141
145
|
return getTimeRelativeToScriptStart(lcpObservedAt);
|
|
146
|
+
},
|
|
147
|
+
set value(v) {
|
|
148
|
+
lcpObservedAt = v;
|
|
142
149
|
}
|
|
143
150
|
},
|
|
144
151
|
cls: {
|
|
@@ -152,11 +159,11 @@ export function trackMFEVitals(target, timings) {
|
|
|
152
159
|
if (!target || !isBrowserScope || !globalScope.MutationObserver || !globalScope.PerformanceObserver) return vitals;
|
|
153
160
|
const observers = [];
|
|
154
161
|
|
|
155
|
-
// If FCP hasn't been observed within
|
|
162
|
+
// If FCP hasn't been observed within this window, give up and shut down all observers.
|
|
156
163
|
// Once FCP is observed, the other vitals are left to record until their natural lifespan ends.
|
|
157
164
|
setTimeout(() => {
|
|
158
165
|
if (!fcpObservedAt) vitals.disconnect();
|
|
159
|
-
},
|
|
166
|
+
}, CORRELATION_STALE_THRESHOLD_MS);
|
|
160
167
|
const populateVitalMinimums = () => {
|
|
161
168
|
fcpObservedAt ??= now();
|
|
162
169
|
lcpObservedAt ??= now();
|
|
@@ -18,6 +18,8 @@ export class ScriptCorrelation {
|
|
|
18
18
|
constructor(url) {
|
|
19
19
|
/** @type {string} The cleaned URL of the script */
|
|
20
20
|
this.url = url;
|
|
21
|
+
/** @type {Set<string>} MFE target ids that have already claimed this correlation via a completed `findScriptTimings()` call. Keyed per target rather than a single flag so one script that registers multiple distinct MFEs (each with its own id) doesn't have the second MFE's registration mistaken for a stale reuse of the first's. */
|
|
22
|
+
this.claimedBy = new Set();
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @type {number} - Correlations are keyed only by script URL and never cleared per-registration, so a script registered more than once in a session (SPA remount, shared bundle, etc.) can reuse a correlation from a much earlier load. If the correlation's recorded start predates `registeredAt` by more than this, treat it as stale and fall back to `registeredAt`. */
|
|
7
|
+
export const CORRELATION_STALE_THRESHOLD_MS = 10000;
|
|
@@ -8,9 +8,12 @@ import { now } from '../timing/now';
|
|
|
8
8
|
import { cleanURL } from '../url/clean-url';
|
|
9
9
|
import { chrome, chromeEval, gecko } from '../util/browser-stack-matchers';
|
|
10
10
|
import { ScriptCorrelation } from './script-correlation';
|
|
11
|
+
import { CORRELATION_STALE_THRESHOLD_MS } from './script-tracker-constants';
|
|
12
|
+
import { timingFactory } from './timing-factory';
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
|
|
16
|
+
* @typedef {import('../../loaders/api/register-api-types').RegisterAPITarget} RegisterAPITarget
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
19
|
/** export for testing purposes */
|
|
@@ -22,7 +25,7 @@ try {
|
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
/** @type {(entry: PerformanceEntry) => boolean} - A shared function to determine if a performance entry is a valid script or link resource for evaluation */
|
|
25
|
-
const validEntryCriteria = entry => entry.initiatorType === 'script' || ['link', 'fetch'].includes(entry.initiatorType) && entry.name.endsWith('.js');
|
|
28
|
+
const validEntryCriteria = entry => entry.initiatorType === 'script' || ['link', 'fetch'].includes(entry.initiatorType) && cleanURL(entry.name).endsWith('.js');
|
|
26
29
|
|
|
27
30
|
/** @type {Map<string, ScriptCorrelation>} - Central registry for script correlations containing both DOM and Performance data */
|
|
28
31
|
export const scriptCorrelations = new Map();
|
|
@@ -197,11 +200,32 @@ function applyPerformanceEntry(timings, entry) {
|
|
|
197
200
|
timings.type = entry.initiatorType;
|
|
198
201
|
}
|
|
199
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Subscribes to late resource timing emissions for a script URL.
|
|
205
|
+
* @param {RegisterAPITimings} timings - The timings object to update
|
|
206
|
+
* @param {string} mfeScriptUrl - The script URL to match
|
|
207
|
+
*/
|
|
208
|
+
function subscribeToLatePerformanceEntry(timings, mfeScriptUrl) {
|
|
209
|
+
if (!globalScope.PerformanceObserver?.supportedEntryTypes?.includes('resource')) return;
|
|
210
|
+
poSubscribers.push({
|
|
211
|
+
addedAt: now(),
|
|
212
|
+
test: entry => {
|
|
213
|
+
if (entryMatchesUrl(entry, mfeScriptUrl)) {
|
|
214
|
+
applyPerformanceEntry(timings, entry);
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
200
222
|
/**
|
|
201
223
|
* 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.
|
|
224
|
+
* @param {RegisterAPITarget} [target] - The MFE target being registered. Its id is used to scope stale-correlation detection per-MFE rather than per-script-URL, so one script registering multiple distinct MFEs doesn't misclassify a later MFE's registration as a stale reuse of an earlier one's.
|
|
202
225
|
* @returns {RegisterAPITimings} Object containing script fetch start and end times, and the asset URL if found
|
|
203
226
|
*/
|
|
204
|
-
export function findScriptTimings() {
|
|
227
|
+
export function findScriptTimings(target) {
|
|
228
|
+
const mfeId = target?.id;
|
|
205
229
|
const timings = {
|
|
206
230
|
registeredAt: now(),
|
|
207
231
|
reportedAt: undefined,
|
|
@@ -231,38 +255,39 @@ export function findScriptTimings() {
|
|
|
231
255
|
// Get correlation data
|
|
232
256
|
timings.correlation = findCorrelation(mfeScriptUrl);
|
|
233
257
|
|
|
234
|
-
// Use correlation's performance entry if available, otherwise check live performance API
|
|
235
|
-
const performanceEntry = timings.correlation?.performance.value || performance
|
|
258
|
+
// Use correlation's performance entry if available, otherwise check the live performance API before falling back to the buffered observer.
|
|
259
|
+
const performanceEntry = timings.correlation?.performance.value || globalScope.performance?.getEntriesByType('resource')?.find(e => entryMatchesUrl(e, mfeScriptUrl));
|
|
236
260
|
if (performanceEntry) {
|
|
237
261
|
applyPerformanceEntry(timings, performanceEntry);
|
|
238
|
-
} else
|
|
239
|
-
|
|
240
|
-
timings.asset = mfeScriptUrl;
|
|
241
|
-
timings.type = 'preload';
|
|
262
|
+
} else {
|
|
263
|
+
const isPreloaded = wasPreloaded(mfeScriptUrl);
|
|
242
264
|
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
return true;
|
|
250
|
-
}
|
|
251
|
-
return false;
|
|
252
|
-
}
|
|
253
|
-
});
|
|
265
|
+
// Handle preloaded scripts and any late resource emissions through the shared buffered observer.
|
|
266
|
+
if (isPreloaded) {
|
|
267
|
+
timings.asset = mfeScriptUrl;
|
|
268
|
+
timings.type = 'preload';
|
|
269
|
+
}
|
|
270
|
+
subscribeToLatePerformanceEntry(timings, mfeScriptUrl);
|
|
254
271
|
}
|
|
255
272
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
273
|
+
// A correlation can be reused across multiple `register()` calls for the same script URL (e.g. an SPA
|
|
274
|
+
// remounting the same MFE without the script actually reloading). When that happens, its dom/performance
|
|
275
|
+
// timings still describe the *original* load, not this one. Detect that case so scriptStart/scriptEnd
|
|
276
|
+
// below can ignore the stale data instead of reporting it as if it were fresh.
|
|
277
|
+
const correlation = timings.correlation;
|
|
278
|
+
const alreadyClaimedByThisMFE = !!mfeId && !!correlation?.claimedBy.has(mfeId);
|
|
279
|
+
if (correlation && mfeId) correlation.claimedBy.add(mfeId);
|
|
280
|
+
const isCorrelationStale = () => {
|
|
281
|
+
const correlationStart = correlation?.script.start;
|
|
282
|
+
if (!alreadyClaimedByThisMFE || !correlationStart) return false;
|
|
283
|
+
const staleness = timings.registeredAt - correlationStart;
|
|
284
|
+
return staleness > CORRELATION_STALE_THRESHOLD_MS;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// 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.
|
|
288
|
+
// Non-stale: fall back to fetchEnd if correlation data isn't available yet (our best approximation for script execution start). Stale: fall back straight to registeredAt — fetchEnd would be derived from the same stale correlation, so it can't be trusted either.
|
|
289
|
+
Object.defineProperty(timings, 'scriptStart', timingFactory(() => isCorrelationStale() ? timings.registeredAt : correlation?.script.start ?? timings.fetchEnd));
|
|
290
|
+
Object.defineProperty(timings, 'scriptEnd', timingFactory(() => isCorrelationStale() ? timings.registeredAt : correlation?.script.end ?? timings.registeredAt));
|
|
266
291
|
} catch (error) {
|
|
267
292
|
// Don't let stack parsing errors break anything
|
|
268
293
|
}
|