@newrelic/browser-agent 1.320.0 → 1.320.1
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 +8 -0
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/payloads/payloads.js +3 -2
- package/dist/cjs/common/serialize/bel-serializer.js +17 -4
- package/dist/cjs/common/v2/mfe-vitals.js +34 -11
- package/dist/cjs/features/ajax/aggregate/index.js +19 -8
- package/dist/cjs/features/page_view_timing/aggregate/index.js +28 -13
- package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +16 -4
- package/dist/cjs/features/soft_navigations/aggregate/interaction.js +10 -3
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/payloads/payloads.js +3 -2
- package/dist/esm/common/serialize/bel-serializer.js +17 -4
- package/dist/esm/common/v2/mfe-vitals.js +34 -11
- package/dist/esm/features/ajax/aggregate/index.js +19 -8
- package/dist/esm/features/page_view_timing/aggregate/index.js +28 -13
- package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +16 -4
- package/dist/esm/features/soft_navigations/aggregate/interaction.js +10 -3
- package/dist/types/common/payloads/payloads.d.ts +3 -2
- package/dist/types/common/payloads/payloads.d.ts.map +1 -1
- package/dist/types/common/serialize/bel-serializer.d.ts +9 -1
- package/dist/types/common/serialize/bel-serializer.d.ts.map +1 -1
- package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -1
- package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_timing/aggregate/index.d.ts +0 -1
- package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/payloads/payloads.js +3 -2
- package/src/common/serialize/bel-serializer.js +13 -4
- package/src/common/v2/mfe-vitals.js +39 -12
- package/src/features/ajax/aggregate/index.js +12 -9
- package/src/features/page_view_timing/aggregate/index.js +19 -14
- package/src/features/soft_navigations/aggregate/ajax-node.js +10 -7
- package/src/features/soft_navigations/aggregate/interaction.js +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [1.320.1](https://github.com/newrelic/newrelic-browser-agent/compare/v1.320.0...v1.320.1) (2026-08-12)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* Apply hard cap to MFE FCP ([#1828](https://github.com/newrelic/newrelic-browser-agent/issues/1828)) ([095a1d6](https://github.com/newrelic/newrelic-browser-agent/commit/095a1d66a917b5044f64757f61ee57607b2879ae))
|
|
12
|
+
* Avoid obfuscating keys and reserved system vals ([#1817](https://github.com/newrelic/newrelic-browser-agent/issues/1817)) ([1f19198](https://github.com/newrelic/newrelic-browser-agent/commit/1f1919809498e6aa8f1fc2ada0c00c3872cb30b8))
|
|
13
|
+
|
|
6
14
|
## [1.320.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.319.0...v1.320.0) (2026-08-07)
|
|
7
15
|
|
|
8
16
|
|
|
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.RRWEB_PACKAGE_NAME = exports.D
|
|
|
17
17
|
/**
|
|
18
18
|
* Exposes the version of the agent
|
|
19
19
|
*/
|
|
20
|
-
const VERSION = exports.VERSION = "1.320.
|
|
20
|
+
const VERSION = exports.VERSION = "1.320.1";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Exposes the build type of the agent
|
|
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.RRWEB_PACKAGE_NAME = exports.D
|
|
|
17
17
|
/**
|
|
18
18
|
* Exposes the version of the agent
|
|
19
19
|
*/
|
|
20
|
-
const VERSION = exports.VERSION = "1.320.
|
|
20
|
+
const VERSION = exports.VERSION = "1.320.1";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Exposes the build type of the agent
|
|
@@ -123,11 +123,11 @@ function truncateAsString(data) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
* Creates string adder functions for BEL serialization with obfuscation and optional truncation.
|
|
126
|
+
* Creates string adder functions for BEL serialization with obfuscation and optional truncation, as well as raw string.
|
|
127
127
|
* This ensures a single string table is used while providing separate handling for regular vs payload attributes.
|
|
128
128
|
* @param {Function} getAddStringContext - Function that creates a new string table context
|
|
129
129
|
* @param {Object} obfuscator - Optional obfuscator instance for string obfuscation
|
|
130
|
-
* @returns {{addString: Function, addStringWithTruncation: Function}} Object containing
|
|
130
|
+
* @returns {{addString: Function, addStringRaw: Function, addStringWithTruncation: Function}} Object containing various string adder functions
|
|
131
131
|
*/
|
|
132
132
|
function createStringAdders(getAddStringContext, obfuscator) {
|
|
133
133
|
const addStringRaw = getAddStringContext();
|
|
@@ -140,6 +140,7 @@ function createStringAdders(getAddStringContext, obfuscator) {
|
|
|
140
140
|
};
|
|
141
141
|
return {
|
|
142
142
|
addString: str => processString(str, false),
|
|
143
|
+
addStringRaw: str => addStringRaw(str),
|
|
143
144
|
addStringWithTruncation: str => processString(str, true)
|
|
144
145
|
};
|
|
145
146
|
}
|
|
@@ -40,19 +40,32 @@ function getAddStringContext(obfuscator, truncator) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Attributes are serialized according to their types, using the provided serializers. Only the first 64 attributes will be added to the payload.
|
|
46
|
+
* @param attrs
|
|
47
|
+
* @param serializers
|
|
48
|
+
* @param {function} serializers.addKey -function for serializing attribute keys
|
|
49
|
+
* @param {function} serializers.addVal - function for serializing attribute values
|
|
50
|
+
* @returns {*[]}
|
|
51
|
+
*/
|
|
52
|
+
function addCustomAttributes(attrs, serializers) {
|
|
53
|
+
const {
|
|
54
|
+
addKey,
|
|
55
|
+
addVal
|
|
56
|
+
} = serializers;
|
|
44
57
|
var attrParts = [];
|
|
45
58
|
Object.entries(attrs || {}).forEach(([key, val]) => {
|
|
46
59
|
if (attrParts.length >= MAX_ATTRIBUTES) return;
|
|
47
60
|
var type = 5;
|
|
48
61
|
var serializedValue;
|
|
49
62
|
// add key to string table first
|
|
50
|
-
key =
|
|
63
|
+
key = addKey(key);
|
|
51
64
|
switch (typeof val) {
|
|
52
65
|
case 'object':
|
|
53
66
|
if (val) {
|
|
54
67
|
// serialize objects to strings
|
|
55
|
-
serializedValue =
|
|
68
|
+
serializedValue = addVal((0, _stringify.stringify)(val));
|
|
56
69
|
} else {
|
|
57
70
|
// null attribute type
|
|
58
71
|
type = 9;
|
|
@@ -71,7 +84,7 @@ function addCustomAttributes(attrs, addString) {
|
|
|
71
84
|
type = 9;
|
|
72
85
|
break;
|
|
73
86
|
default:
|
|
74
|
-
serializedValue =
|
|
87
|
+
serializedValue = addVal(val);
|
|
75
88
|
}
|
|
76
89
|
attrParts.push([type, key + (serializedValue ? ',' + serializedValue : '')]);
|
|
77
90
|
});
|
|
@@ -132,25 +132,33 @@ const observePerformance = (observers, config, onEntry) => {
|
|
|
132
132
|
function trackMFEVitals(target, timings) {
|
|
133
133
|
let fcpObservedAt;
|
|
134
134
|
let lcpObservedAt;
|
|
135
|
+
// Once disconnect() runs, no further vitals data should be written - even from an observer callback that was
|
|
136
|
+
// already in flight when disconnect() was called.
|
|
137
|
+
let disconnected = false;
|
|
135
138
|
const getTimeRelativeToScriptStart = capturedAt => {
|
|
136
139
|
if (capturedAt == null) return capturedAt;
|
|
137
140
|
return capturedAt - (timings?.scriptStart || timings?.registeredAt || 0);
|
|
138
141
|
};
|
|
139
142
|
const vitals = {
|
|
140
143
|
fcp: {
|
|
144
|
+
// A clock freeze/tab suspend between script registration and first paint can make this value huge once the
|
|
145
|
+
// tab resumes. Rather than report a multi-hour "FCP", drop it so bad data doesn't reach the backend.
|
|
141
146
|
get value() {
|
|
142
|
-
|
|
147
|
+
const relative = getTimeRelativeToScriptStart(fcpObservedAt);
|
|
148
|
+
return relative > _scriptTrackerConstants.CORRELATION_STALE_THRESHOLD_MS ? undefined : relative;
|
|
143
149
|
},
|
|
144
150
|
set value(v) {
|
|
145
|
-
fcpObservedAt = v;
|
|
151
|
+
if (!disconnected) fcpObservedAt = v;
|
|
146
152
|
}
|
|
147
153
|
},
|
|
148
154
|
lcp: {
|
|
155
|
+
// LCP can never occur before FCP, so if FCP was dropped as invalid, LCP is derived from the same untrustworthy clock and must be dropped too.
|
|
149
156
|
get value() {
|
|
157
|
+
if (vitals.fcp.value === undefined) return undefined;
|
|
150
158
|
return getTimeRelativeToScriptStart(lcpObservedAt);
|
|
151
159
|
},
|
|
152
160
|
set value(v) {
|
|
153
|
-
lcpObservedAt = v;
|
|
161
|
+
if (!disconnected) lcpObservedAt = v;
|
|
154
162
|
}
|
|
155
163
|
},
|
|
156
164
|
cls: {
|
|
@@ -170,15 +178,22 @@ function trackMFEVitals(target, timings) {
|
|
|
170
178
|
if (!fcpObservedAt) vitals.disconnect();
|
|
171
179
|
}, _scriptTrackerConstants.CORRELATION_STALE_THRESHOLD_MS);
|
|
172
180
|
const populateVitalMinimums = () => {
|
|
173
|
-
|
|
181
|
+
if (disconnected) return;
|
|
182
|
+
if (fcpObservedAt == null) {
|
|
183
|
+
const capturedAt = (0, _now.now)();
|
|
184
|
+
// If the very first observed render already exceeds the window, the clock is untrustworthy for this
|
|
185
|
+
// MFE (e.g. tab was frozen/suspended between registration and paint) - shut everything down instead
|
|
186
|
+
// of seeding vitals with data derived from it.
|
|
187
|
+
if (getTimeRelativeToScriptStart(capturedAt) > _scriptTrackerConstants.CORRELATION_STALE_THRESHOLD_MS) {
|
|
188
|
+
vitals.disconnect();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
fcpObservedAt = capturedAt;
|
|
192
|
+
}
|
|
174
193
|
lcpObservedAt ??= (0, _now.now)();
|
|
175
194
|
vitals.cls.value ??= 0;
|
|
176
195
|
};
|
|
177
196
|
|
|
178
|
-
// if the MFE has already rendered something on the page before we could set up listeners, just populate vital minimums immediately
|
|
179
|
-
const existingRoots = _runtime.globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
|
|
180
|
-
if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums();
|
|
181
|
-
|
|
182
197
|
// Track FCP - first contentful paint
|
|
183
198
|
const fcpObs = observeMutations(target, (_, obs) => {
|
|
184
199
|
// An observed "FCP" means _something_ rendered, so at minimum we can populate all the baseline values for the vitals
|
|
@@ -193,12 +208,13 @@ function trackMFEVitals(target, timings) {
|
|
|
193
208
|
const observedElements = new Set();
|
|
194
209
|
try {
|
|
195
210
|
resizeObs = new _runtime.globalScope.ResizeObserver(entries => {
|
|
211
|
+
if (disconnected) return;
|
|
196
212
|
entries.forEach(entry => {
|
|
197
213
|
try {
|
|
198
214
|
const size = entry.contentRect.width * entry.contentRect.height;
|
|
199
215
|
if (size > largestSize) {
|
|
200
216
|
largestSize = size;
|
|
201
|
-
|
|
217
|
+
vitals.lcp.value = (0, _now.now)();
|
|
202
218
|
}
|
|
203
219
|
resizeObs.unobserve(entry.target);
|
|
204
220
|
} catch (e) {
|
|
@@ -259,7 +275,7 @@ function trackMFEVitals(target, timings) {
|
|
|
259
275
|
type: 'layout-shift',
|
|
260
276
|
buffered: true
|
|
261
277
|
}, entry => {
|
|
262
|
-
if (entry.hadRecentInput) return;
|
|
278
|
+
if (disconnected || entry.hadRecentInput) return;
|
|
263
279
|
(entry.sources || []).some(source => {
|
|
264
280
|
if (isInMFE(source.node, target)) {
|
|
265
281
|
// an observed "CLS" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
|
|
@@ -277,7 +293,7 @@ function trackMFEVitals(target, timings) {
|
|
|
277
293
|
buffered: true,
|
|
278
294
|
durationThreshold: 40
|
|
279
295
|
}, entry => {
|
|
280
|
-
if (!entry.interactionId || !isInMFE(entry.target, target)) return;
|
|
296
|
+
if (disconnected || !entry.interactionId || !isInMFE(entry.target, target)) return;
|
|
281
297
|
if (vitals.inp.value === undefined || entry.duration > vitals.inp.value) {
|
|
282
298
|
// an observed "INP" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
|
|
283
299
|
populateVitalMinimums();
|
|
@@ -295,6 +311,7 @@ function trackMFEVitals(target, timings) {
|
|
|
295
311
|
|
|
296
312
|
// Disconnect all observers
|
|
297
313
|
vitals.disconnect = () => {
|
|
314
|
+
disconnected = true;
|
|
298
315
|
// Disconnect all observers
|
|
299
316
|
observers.forEach(obs => {
|
|
300
317
|
try {
|
|
@@ -341,5 +358,11 @@ function trackMFEVitals(target, timings) {
|
|
|
341
358
|
passive: true
|
|
342
359
|
});
|
|
343
360
|
});
|
|
361
|
+
|
|
362
|
+
// If the MFE has already rendered something on the page before we could set up listeners, just populate vital
|
|
363
|
+
// minimums immediately. Done last, once `observers` is fully populated and `vitals.disconnect` has its real
|
|
364
|
+
// implementation, so a stale-clock detection here can actually tear everything down instead of no-op'ing.
|
|
365
|
+
const existingRoots = _runtime.globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
|
|
366
|
+
if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums();
|
|
344
367
|
return vitals;
|
|
345
368
|
}
|
|
@@ -152,6 +152,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
152
152
|
if (!eventBuffer.length) return;
|
|
153
153
|
const {
|
|
154
154
|
addString,
|
|
155
|
+
addStringRaw,
|
|
155
156
|
addStringWithTruncation
|
|
156
157
|
} = (0, _payloads.createStringAdders)(_belSerializer.getAddStringContext, this.obfuscator);
|
|
157
158
|
let payload = 'bel.7;';
|
|
@@ -165,11 +166,11 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
165
166
|
// callbackEnd
|
|
166
167
|
(0, _belSerializer.numeric)(0),
|
|
167
168
|
// no callbackDuration for non-SPA events
|
|
168
|
-
|
|
169
|
+
addStringRaw(event.method), (0, _belSerializer.numeric)(event.status), addString(event.domain), addString(event.path), (0, _belSerializer.numeric)(event.requestSize), (0, _belSerializer.numeric)(event.responseSize), event.type === 'fetch' ? 1 : '', addStringRaw(0),
|
|
169
170
|
// nodeId
|
|
170
|
-
(0, _belSerializer.nullable)(event.spanId,
|
|
171
|
+
(0, _belSerializer.nullable)(event.spanId, addStringRaw, true) +
|
|
171
172
|
// guid
|
|
172
|
-
(0, _belSerializer.nullable)(event.traceId,
|
|
173
|
+
(0, _belSerializer.nullable)(event.traceId, addStringRaw, true) +
|
|
173
174
|
// traceId
|
|
174
175
|
(0, _belSerializer.nullable)(event.spanTimestamp, _belSerializer.numeric, false) // timestamp
|
|
175
176
|
];
|
|
@@ -179,14 +180,21 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
179
180
|
const jsAttributes = this.agentRef.info.jsAttributes;
|
|
180
181
|
|
|
181
182
|
// Regular attributes: obfuscate only
|
|
182
|
-
|
|
183
|
+
let regularAttrs = (0, _belSerializer.addCustomAttributes)({
|
|
183
184
|
...(jsAttributes || {}),
|
|
184
|
-
[_constants.AJAX_ID]: event[_constants.AJAX_ID],
|
|
185
|
-
// all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
|
|
186
185
|
...(event.targetAttributes || {}),
|
|
187
186
|
// used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
|
|
188
187
|
...(event.gql || {})
|
|
189
|
-
},
|
|
188
|
+
}, {
|
|
189
|
+
addKey: addStringRaw,
|
|
190
|
+
addVal: addString
|
|
191
|
+
});
|
|
192
|
+
regularAttrs.push(...(0, _belSerializer.addCustomAttributes)({
|
|
193
|
+
[_constants.AJAX_ID]: event[_constants.AJAX_ID] // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
|
|
194
|
+
}, {
|
|
195
|
+
addKey: addStringRaw,
|
|
196
|
+
addVal: addStringRaw
|
|
197
|
+
}));
|
|
190
198
|
|
|
191
199
|
// Payload attributes: obfuscate then truncate
|
|
192
200
|
const payloadAttrs = (0, _belSerializer.addCustomAttributes)({
|
|
@@ -205,7 +213,10 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
205
213
|
...(event.responseHeaders ? {
|
|
206
214
|
responseHeaders: event.responseHeaders
|
|
207
215
|
} : {})
|
|
208
|
-
},
|
|
216
|
+
}, {
|
|
217
|
+
addKey: addStringRaw,
|
|
218
|
+
addVal: addStringWithTruncation
|
|
219
|
+
});
|
|
209
220
|
const attrParts = [...regularAttrs, ...payloadAttrs];
|
|
210
221
|
fields.unshift((0, _belSerializer.numeric)(attrParts.length));
|
|
211
222
|
insert += fields.join(',');
|
|
@@ -24,6 +24,7 @@ var _loadTime = require("../../../common/vitals/load-time");
|
|
|
24
24
|
var _webdriverDetection = require("../../../common/util/webdriver-detection");
|
|
25
25
|
var _cleanUrl = require("../../../common/url/clean-url");
|
|
26
26
|
var _events = require("../../../common/constants/events");
|
|
27
|
+
var _payloads = require("../../../common/payloads/payloads");
|
|
27
28
|
/**
|
|
28
29
|
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
29
30
|
* SPDX-License-Identifier: Apache-2.0
|
|
@@ -104,6 +105,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
104
105
|
if (name !== _constants2.VITAL_NAMES.CUMULATIVE_LAYOUT_SHIFT && _cumulativeLayoutShift.cumulativeLayoutShift.current.value >= 0) {
|
|
105
106
|
attrs.cls = _cumulativeLayoutShift.cumulativeLayoutShift.current.value;
|
|
106
107
|
}
|
|
108
|
+
attrs.webdriverDetected = _webdriverDetection.webdriverDetected;
|
|
107
109
|
const timing = {
|
|
108
110
|
name,
|
|
109
111
|
value,
|
|
@@ -133,15 +135,9 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
133
135
|
loadState: document.readyState
|
|
134
136
|
});
|
|
135
137
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
Object.entries(this.agentRef.info.jsAttributes || {}).forEach(([key, val]) => {
|
|
140
|
-
if (reservedAttributes.indexOf(key) < 0) {
|
|
141
|
-
timingAttributes[key] = val;
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
timingAttributes.webdriverDetected = _webdriverDetection.webdriverDetected;
|
|
138
|
+
#getGlobalCustomAttributes() {
|
|
139
|
+
const reservedAttributes = ['size', 'eid', 'cls', 'type', 'fid', 'elTag', 'elUrl', 'net-type', 'net-etype', 'net-rtt', 'net-dlink', 'webdriverDetected'];
|
|
140
|
+
return Object.fromEntries(Object.entries(this.agentRef.info.jsAttributes || {}).filter(([key]) => !reservedAttributes.includes(key)));
|
|
145
141
|
}
|
|
146
142
|
preHarvestChecks() {
|
|
147
143
|
this.checkForFirstInteraction();
|
|
@@ -151,15 +147,34 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
151
147
|
// serialize array of timing data
|
|
152
148
|
serializer(eventBuffer) {
|
|
153
149
|
if (!eventBuffer?.length) return '';
|
|
154
|
-
|
|
150
|
+
const {
|
|
151
|
+
addString,
|
|
152
|
+
addStringRaw
|
|
153
|
+
} = (0, _payloads.createStringAdders)(_belSerializer.getAddStringContext, this.obfuscator);
|
|
154
|
+
const keepOrigValue = {
|
|
155
|
+
addKey: addStringRaw,
|
|
156
|
+
addVal: addStringRaw
|
|
157
|
+
};
|
|
158
|
+
const obfuscateValue = {
|
|
159
|
+
addKey: addStringRaw,
|
|
160
|
+
addVal: addString
|
|
161
|
+
};
|
|
155
162
|
var payload = 'bel.6;';
|
|
156
163
|
for (var i = 0; i < eventBuffer.length; i++) {
|
|
157
164
|
var timing = eventBuffer[i];
|
|
158
165
|
payload += 'e,';
|
|
159
|
-
payload +=
|
|
166
|
+
payload += addStringRaw(timing.name) + ',';
|
|
160
167
|
payload += (0, _belSerializer.nullable)(timing.value, _belSerializer.numeric, false) + ',';
|
|
161
|
-
this
|
|
162
|
-
|
|
168
|
+
const userAttrs = this.#getGlobalCustomAttributes();
|
|
169
|
+
const {
|
|
170
|
+
pageUrl,
|
|
171
|
+
...systemReservedAttrs
|
|
172
|
+
} = timing.attrs || {};
|
|
173
|
+
var attrParts = (0, _belSerializer.addCustomAttributes)(systemReservedAttrs, keepOrigValue);
|
|
174
|
+
attrParts.push(...(0, _belSerializer.addCustomAttributes)(userAttrs, obfuscateValue));
|
|
175
|
+
if (pageUrl) attrParts.push(...(0, _belSerializer.addCustomAttributes)({
|
|
176
|
+
pageUrl
|
|
177
|
+
}, obfuscateValue));
|
|
163
178
|
if (attrParts && attrParts.length > 0) {
|
|
164
179
|
payload += (0, _belSerializer.numeric)(attrParts.length) + ';' + attrParts.join(';');
|
|
165
180
|
}
|
|
@@ -48,6 +48,7 @@ class AjaxNode extends _belNode.BelNode {
|
|
|
48
48
|
serialize(parentStartTimestamp, agentRef, ajaxObfuscator) {
|
|
49
49
|
const {
|
|
50
50
|
addString,
|
|
51
|
+
addStringRaw,
|
|
51
52
|
addStringWithTruncation
|
|
52
53
|
} = (0, _payloads.createStringAdders)(_belSerializer.getAddStringContext, ajaxObfuscator);
|
|
53
54
|
const nodeList = [];
|
|
@@ -63,13 +64,21 @@ class AjaxNode extends _belNode.BelNode {
|
|
|
63
64
|
// callbackEnd is relative to end
|
|
64
65
|
(0, _belSerializer.numeric)(this.callbackDuration),
|
|
65
66
|
// not relative
|
|
66
|
-
|
|
67
|
+
addStringRaw(this.method), (0, _belSerializer.numeric)(this.status), addString(this.domain), addString(this.path), (0, _belSerializer.numeric)(this.txSize), (0, _belSerializer.numeric)(this.rxSize), this.requestedWith, addStringRaw(this.nodeId), (0, _belSerializer.nullable)(this.spanId, addStringRaw, true) + (0, _belSerializer.nullable)(this.traceId, addStringRaw, true) + (0, _belSerializer.nullable)(this.spanTimestamp, _belSerializer.numeric)];
|
|
67
68
|
// Regular attributes: obfuscate only
|
|
68
69
|
const regularAttrs = (0, _belSerializer.addCustomAttributes)({
|
|
69
|
-
[_constants.AJAX_ID]: this[_constants.AJAX_ID],
|
|
70
70
|
...(this.targetAttributes || {}),
|
|
71
71
|
...(this.gql || {})
|
|
72
|
-
},
|
|
72
|
+
}, {
|
|
73
|
+
addKey: addStringRaw,
|
|
74
|
+
addVal: addString
|
|
75
|
+
});
|
|
76
|
+
regularAttrs.push(...(0, _belSerializer.addCustomAttributes)({
|
|
77
|
+
[_constants.AJAX_ID]: this[_constants.AJAX_ID]
|
|
78
|
+
}, {
|
|
79
|
+
addKey: addStringRaw,
|
|
80
|
+
addVal: addStringRaw
|
|
81
|
+
}));
|
|
73
82
|
|
|
74
83
|
// Payload attributes: obfuscate then truncate
|
|
75
84
|
const payloadAttrs = (0, _belSerializer.addCustomAttributes)({
|
|
@@ -88,7 +97,10 @@ class AjaxNode extends _belNode.BelNode {
|
|
|
88
97
|
...(this.responseHeaders ? {
|
|
89
98
|
responseHeaders: this.responseHeaders
|
|
90
99
|
} : {})
|
|
91
|
-
},
|
|
100
|
+
}, {
|
|
101
|
+
addKey: addStringRaw,
|
|
102
|
+
addVal: addStringWithTruncation
|
|
103
|
+
});
|
|
92
104
|
let allAttachedNodes = [...regularAttrs, ...payloadAttrs];
|
|
93
105
|
this.children.forEach(node => allAttachedNodes.push(node.serialize())); // no children is expected under ajax nodes at this time
|
|
94
106
|
|
|
@@ -11,6 +11,7 @@ var _now = require("../../../common/timing/now");
|
|
|
11
11
|
var _cleanUrl = require("../../../common/url/clean-url");
|
|
12
12
|
var _constants = require("../constants");
|
|
13
13
|
var _belNode = require("./bel-node");
|
|
14
|
+
var _payloads = require("../../../common/payloads/payloads");
|
|
14
15
|
/**
|
|
15
16
|
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
16
17
|
* SPDX-License-Identifier: Apache-2.0
|
|
@@ -133,7 +134,10 @@ class Interaction extends _belNode.BelNode {
|
|
|
133
134
|
*/
|
|
134
135
|
serialize(firstStartTimeOfPayload, agentRef, interactionObfuscator, ajaxObfuscator) {
|
|
135
136
|
const isFirstIxnOfPayload = firstStartTimeOfPayload === undefined;
|
|
136
|
-
const
|
|
137
|
+
const {
|
|
138
|
+
addString,
|
|
139
|
+
addStringRaw
|
|
140
|
+
} = (0, _payloads.createStringAdders)(_belSerializer.getAddStringContext, interactionObfuscator);
|
|
137
141
|
const nodeList = [];
|
|
138
142
|
let ixnType;
|
|
139
143
|
if (this.trigger === _constants.IPL_TRIGGER_NAME) ixnType = _constants.INTERACTION_TYPE.INITIAL_PAGE_LOAD;else if (this.newURL !== this.oldURL) ixnType = _constants.INTERACTION_TYPE.ROUTE_CHANGE;else ixnType = _constants.INTERACTION_TYPE.UNSPECIFIED;
|
|
@@ -149,12 +153,15 @@ class Interaction extends _belNode.BelNode {
|
|
|
149
153
|
// callbackEnd -- relative to start; not used by BrowserInteraction events so these are always 0
|
|
150
154
|
(0, _belSerializer.numeric)(0),
|
|
151
155
|
// not relative; always 0 for BrowserInteraction
|
|
152
|
-
|
|
156
|
+
addStringRaw(this.trigger), addString((0, _cleanUrl.cleanURL)(this.initialPageURL, true)), addString((0, _cleanUrl.cleanURL)(this.oldURL, true)), addString((0, _cleanUrl.cleanURL)(this.newURL, true)), addString(this.customName), ixnType, (0, _belSerializer.nullable)(this.queueTime, _belSerializer.numeric, true) + (0, _belSerializer.nullable)(this.appTime, _belSerializer.numeric, true) + (0, _belSerializer.nullable)(this.oldRoute, addString, true) + (0, _belSerializer.nullable)(this.newRoute, addString, true) + addStringRaw(this.id), addStringRaw(this.nodeId), (0, _belSerializer.nullable)(this.firstPaint, _belSerializer.numeric, true) + (0, _belSerializer.nullable)(this.firstContentfulPaint, _belSerializer.numeric)];
|
|
153
157
|
const customAttributes = {
|
|
154
158
|
...agentRef.info.jsAttributes,
|
|
155
159
|
...this.customAttributes
|
|
156
160
|
}; // attrs specific to this interaction should have precedence over the general custom attrs
|
|
157
|
-
const allAttachedNodes = (0, _belSerializer.addCustomAttributes)(customAttributes || {},
|
|
161
|
+
const allAttachedNodes = (0, _belSerializer.addCustomAttributes)(customAttributes || {}, {
|
|
162
|
+
addKey: addStringRaw,
|
|
163
|
+
addVal: addString
|
|
164
|
+
}); // start with all custom attributes
|
|
158
165
|
if (agentRef.info.atts) allAttachedNodes.push('a,' + addString(agentRef.info.atts)); // add apm provided attributes
|
|
159
166
|
/* Querypack encoder+decoder quirkiness:
|
|
160
167
|
- If first ixn node of payload is being processed, its children's start time must be offset by this node's start. (firstStartTime should be undefined.)
|
|
@@ -114,11 +114,11 @@ export function truncateAsString(data) {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
* Creates string adder functions for BEL serialization with obfuscation and optional truncation.
|
|
117
|
+
* Creates string adder functions for BEL serialization with obfuscation and optional truncation, as well as raw string.
|
|
118
118
|
* This ensures a single string table is used while providing separate handling for regular vs payload attributes.
|
|
119
119
|
* @param {Function} getAddStringContext - Function that creates a new string table context
|
|
120
120
|
* @param {Object} obfuscator - Optional obfuscator instance for string obfuscation
|
|
121
|
-
* @returns {{addString: Function, addStringWithTruncation: Function}} Object containing
|
|
121
|
+
* @returns {{addString: Function, addStringRaw: Function, addStringWithTruncation: Function}} Object containing various string adder functions
|
|
122
122
|
*/
|
|
123
123
|
export function createStringAdders(getAddStringContext, obfuscator) {
|
|
124
124
|
const addStringRaw = getAddStringContext();
|
|
@@ -131,6 +131,7 @@ export function createStringAdders(getAddStringContext, obfuscator) {
|
|
|
131
131
|
};
|
|
132
132
|
return {
|
|
133
133
|
addString: str => processString(str, false),
|
|
134
|
+
addStringRaw: str => addStringRaw(str),
|
|
134
135
|
addStringWithTruncation: str => processString(str, true)
|
|
135
136
|
};
|
|
136
137
|
}
|
|
@@ -31,19 +31,32 @@ export function getAddStringContext(obfuscator, truncator) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Attributes are serialized according to their types, using the provided serializers. Only the first 64 attributes will be added to the payload.
|
|
37
|
+
* @param attrs
|
|
38
|
+
* @param serializers
|
|
39
|
+
* @param {function} serializers.addKey -function for serializing attribute keys
|
|
40
|
+
* @param {function} serializers.addVal - function for serializing attribute values
|
|
41
|
+
* @returns {*[]}
|
|
42
|
+
*/
|
|
43
|
+
export function addCustomAttributes(attrs, serializers) {
|
|
44
|
+
const {
|
|
45
|
+
addKey,
|
|
46
|
+
addVal
|
|
47
|
+
} = serializers;
|
|
35
48
|
var attrParts = [];
|
|
36
49
|
Object.entries(attrs || {}).forEach(([key, val]) => {
|
|
37
50
|
if (attrParts.length >= MAX_ATTRIBUTES) return;
|
|
38
51
|
var type = 5;
|
|
39
52
|
var serializedValue;
|
|
40
53
|
// add key to string table first
|
|
41
|
-
key =
|
|
54
|
+
key = addKey(key);
|
|
42
55
|
switch (typeof val) {
|
|
43
56
|
case 'object':
|
|
44
57
|
if (val) {
|
|
45
58
|
// serialize objects to strings
|
|
46
|
-
serializedValue =
|
|
59
|
+
serializedValue = addVal(stringify(val));
|
|
47
60
|
} else {
|
|
48
61
|
// null attribute type
|
|
49
62
|
type = 9;
|
|
@@ -62,7 +75,7 @@ export function addCustomAttributes(attrs, addString) {
|
|
|
62
75
|
type = 9;
|
|
63
76
|
break;
|
|
64
77
|
default:
|
|
65
|
-
serializedValue =
|
|
78
|
+
serializedValue = addVal(val);
|
|
66
79
|
}
|
|
67
80
|
attrParts.push([type, key + (serializedValue ? ',' + serializedValue : '')]);
|
|
68
81
|
});
|
|
@@ -127,25 +127,33 @@ const observePerformance = (observers, config, onEntry) => {
|
|
|
127
127
|
export function trackMFEVitals(target, timings) {
|
|
128
128
|
let fcpObservedAt;
|
|
129
129
|
let lcpObservedAt;
|
|
130
|
+
// Once disconnect() runs, no further vitals data should be written - even from an observer callback that was
|
|
131
|
+
// already in flight when disconnect() was called.
|
|
132
|
+
let disconnected = false;
|
|
130
133
|
const getTimeRelativeToScriptStart = capturedAt => {
|
|
131
134
|
if (capturedAt == null) return capturedAt;
|
|
132
135
|
return capturedAt - (timings?.scriptStart || timings?.registeredAt || 0);
|
|
133
136
|
};
|
|
134
137
|
const vitals = {
|
|
135
138
|
fcp: {
|
|
139
|
+
// A clock freeze/tab suspend between script registration and first paint can make this value huge once the
|
|
140
|
+
// tab resumes. Rather than report a multi-hour "FCP", drop it so bad data doesn't reach the backend.
|
|
136
141
|
get value() {
|
|
137
|
-
|
|
142
|
+
const relative = getTimeRelativeToScriptStart(fcpObservedAt);
|
|
143
|
+
return relative > CORRELATION_STALE_THRESHOLD_MS ? undefined : relative;
|
|
138
144
|
},
|
|
139
145
|
set value(v) {
|
|
140
|
-
fcpObservedAt = v;
|
|
146
|
+
if (!disconnected) fcpObservedAt = v;
|
|
141
147
|
}
|
|
142
148
|
},
|
|
143
149
|
lcp: {
|
|
150
|
+
// LCP can never occur before FCP, so if FCP was dropped as invalid, LCP is derived from the same untrustworthy clock and must be dropped too.
|
|
144
151
|
get value() {
|
|
152
|
+
if (vitals.fcp.value === undefined) return undefined;
|
|
145
153
|
return getTimeRelativeToScriptStart(lcpObservedAt);
|
|
146
154
|
},
|
|
147
155
|
set value(v) {
|
|
148
|
-
lcpObservedAt = v;
|
|
156
|
+
if (!disconnected) lcpObservedAt = v;
|
|
149
157
|
}
|
|
150
158
|
},
|
|
151
159
|
cls: {
|
|
@@ -165,15 +173,22 @@ export function trackMFEVitals(target, timings) {
|
|
|
165
173
|
if (!fcpObservedAt) vitals.disconnect();
|
|
166
174
|
}, CORRELATION_STALE_THRESHOLD_MS);
|
|
167
175
|
const populateVitalMinimums = () => {
|
|
168
|
-
|
|
176
|
+
if (disconnected) return;
|
|
177
|
+
if (fcpObservedAt == null) {
|
|
178
|
+
const capturedAt = now();
|
|
179
|
+
// If the very first observed render already exceeds the window, the clock is untrustworthy for this
|
|
180
|
+
// MFE (e.g. tab was frozen/suspended between registration and paint) - shut everything down instead
|
|
181
|
+
// of seeding vitals with data derived from it.
|
|
182
|
+
if (getTimeRelativeToScriptStart(capturedAt) > CORRELATION_STALE_THRESHOLD_MS) {
|
|
183
|
+
vitals.disconnect();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
fcpObservedAt = capturedAt;
|
|
187
|
+
}
|
|
169
188
|
lcpObservedAt ??= now();
|
|
170
189
|
vitals.cls.value ??= 0;
|
|
171
190
|
};
|
|
172
191
|
|
|
173
|
-
// if the MFE has already rendered something on the page before we could set up listeners, just populate vital minimums immediately
|
|
174
|
-
const existingRoots = globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
|
|
175
|
-
if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums();
|
|
176
|
-
|
|
177
192
|
// Track FCP - first contentful paint
|
|
178
193
|
const fcpObs = observeMutations(target, (_, obs) => {
|
|
179
194
|
// An observed "FCP" means _something_ rendered, so at minimum we can populate all the baseline values for the vitals
|
|
@@ -188,12 +203,13 @@ export function trackMFEVitals(target, timings) {
|
|
|
188
203
|
const observedElements = new Set();
|
|
189
204
|
try {
|
|
190
205
|
resizeObs = new globalScope.ResizeObserver(entries => {
|
|
206
|
+
if (disconnected) return;
|
|
191
207
|
entries.forEach(entry => {
|
|
192
208
|
try {
|
|
193
209
|
const size = entry.contentRect.width * entry.contentRect.height;
|
|
194
210
|
if (size > largestSize) {
|
|
195
211
|
largestSize = size;
|
|
196
|
-
|
|
212
|
+
vitals.lcp.value = now();
|
|
197
213
|
}
|
|
198
214
|
resizeObs.unobserve(entry.target);
|
|
199
215
|
} catch (e) {
|
|
@@ -254,7 +270,7 @@ export function trackMFEVitals(target, timings) {
|
|
|
254
270
|
type: 'layout-shift',
|
|
255
271
|
buffered: true
|
|
256
272
|
}, entry => {
|
|
257
|
-
if (entry.hadRecentInput) return;
|
|
273
|
+
if (disconnected || entry.hadRecentInput) return;
|
|
258
274
|
(entry.sources || []).some(source => {
|
|
259
275
|
if (isInMFE(source.node, target)) {
|
|
260
276
|
// an observed "CLS" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
|
|
@@ -272,7 +288,7 @@ export function trackMFEVitals(target, timings) {
|
|
|
272
288
|
buffered: true,
|
|
273
289
|
durationThreshold: 40
|
|
274
290
|
}, entry => {
|
|
275
|
-
if (!entry.interactionId || !isInMFE(entry.target, target)) return;
|
|
291
|
+
if (disconnected || !entry.interactionId || !isInMFE(entry.target, target)) return;
|
|
276
292
|
if (vitals.inp.value === undefined || entry.duration > vitals.inp.value) {
|
|
277
293
|
// an observed "INP" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
|
|
278
294
|
populateVitalMinimums();
|
|
@@ -290,6 +306,7 @@ export function trackMFEVitals(target, timings) {
|
|
|
290
306
|
|
|
291
307
|
// Disconnect all observers
|
|
292
308
|
vitals.disconnect = () => {
|
|
309
|
+
disconnected = true;
|
|
293
310
|
// Disconnect all observers
|
|
294
311
|
observers.forEach(obs => {
|
|
295
312
|
try {
|
|
@@ -336,5 +353,11 @@ export function trackMFEVitals(target, timings) {
|
|
|
336
353
|
passive: true
|
|
337
354
|
});
|
|
338
355
|
});
|
|
356
|
+
|
|
357
|
+
// If the MFE has already rendered something on the page before we could set up listeners, just populate vital
|
|
358
|
+
// minimums immediately. Done last, once `observers` is fully populated and `vitals.disconnect` has its real
|
|
359
|
+
// implementation, so a stale-clock detection here can actually tear everything down instead of no-op'ing.
|
|
360
|
+
const existingRoots = globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
|
|
361
|
+
if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums();
|
|
339
362
|
return vitals;
|
|
340
363
|
}
|