@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/cjs/common/constants/env.cdn.js +1 -1
  3. package/dist/cjs/common/constants/env.npm.js +1 -1
  4. package/dist/cjs/common/payloads/payloads.js +3 -2
  5. package/dist/cjs/common/serialize/bel-serializer.js +17 -4
  6. package/dist/cjs/common/v2/mfe-vitals.js +34 -11
  7. package/dist/cjs/features/ajax/aggregate/index.js +19 -8
  8. package/dist/cjs/features/page_view_timing/aggregate/index.js +28 -13
  9. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +16 -4
  10. package/dist/cjs/features/soft_navigations/aggregate/interaction.js +10 -3
  11. package/dist/esm/common/constants/env.cdn.js +1 -1
  12. package/dist/esm/common/constants/env.npm.js +1 -1
  13. package/dist/esm/common/payloads/payloads.js +3 -2
  14. package/dist/esm/common/serialize/bel-serializer.js +17 -4
  15. package/dist/esm/common/v2/mfe-vitals.js +34 -11
  16. package/dist/esm/features/ajax/aggregate/index.js +19 -8
  17. package/dist/esm/features/page_view_timing/aggregate/index.js +28 -13
  18. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +16 -4
  19. package/dist/esm/features/soft_navigations/aggregate/interaction.js +10 -3
  20. package/dist/types/common/payloads/payloads.d.ts +3 -2
  21. package/dist/types/common/payloads/payloads.d.ts.map +1 -1
  22. package/dist/types/common/serialize/bel-serializer.d.ts +9 -1
  23. package/dist/types/common/serialize/bel-serializer.d.ts.map +1 -1
  24. package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -1
  25. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  26. package/dist/types/features/page_view_timing/aggregate/index.d.ts +0 -1
  27. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  28. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  29. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/common/payloads/payloads.js +3 -2
  32. package/src/common/serialize/bel-serializer.js +13 -4
  33. package/src/common/v2/mfe-vitals.js +39 -12
  34. package/src/features/ajax/aggregate/index.js +12 -9
  35. package/src/features/page_view_timing/aggregate/index.js +19 -14
  36. package/src/features/soft_navigations/aggregate/ajax-node.js +10 -7
  37. 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.0";
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.0";
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 both string adder functions
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
- function addCustomAttributes(attrs, addString) {
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 = addString(key, false);
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 = addString((0, _stringify.stringify)(val));
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 = addString(val);
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
- return getTimeRelativeToScriptStart(fcpObservedAt);
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
- fcpObservedAt ??= (0, _now.now)();
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
- lcpObservedAt = (0, _now.now)();
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
- addString(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 : '', addString(0),
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, addString, true) +
171
+ (0, _belSerializer.nullable)(event.spanId, addStringRaw, true) +
171
172
  // guid
172
- (0, _belSerializer.nullable)(event.traceId, addString, true) +
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
- const regularAttrs = (0, _belSerializer.addCustomAttributes)({
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
- }, addString);
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
- }, addStringWithTruncation);
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
- appendGlobalCustomAttributes(timing) {
137
- var timingAttributes = timing.attrs || {};
138
- var reservedAttributes = ['size', 'eid', 'cls', 'type', 'fid', 'elTag', 'elUrl', 'net-type', 'net-etype', 'net-rtt', 'net-dlink'];
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
- var addString = (0, _belSerializer.getAddStringContext)(this.obfuscator);
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 += addString(timing.name) + ',';
166
+ payload += addStringRaw(timing.name) + ',';
160
167
  payload += (0, _belSerializer.nullable)(timing.value, _belSerializer.numeric, false) + ',';
161
- this.appendGlobalCustomAttributes(timing);
162
- var attrParts = (0, _belSerializer.addCustomAttributes)(timing.attrs, addString);
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
- addString(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, addString(this.nodeId), (0, _belSerializer.nullable)(this.spanId, addString, true) + (0, _belSerializer.nullable)(this.traceId, addString, true) + (0, _belSerializer.nullable)(this.spanTimestamp, _belSerializer.numeric)];
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
- }, addString);
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
- }, addStringWithTruncation);
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 addString = (0, _belSerializer.getAddStringContext)(interactionObfuscator);
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
- addString(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) + addString(this.id), addString(this.nodeId), (0, _belSerializer.nullable)(this.firstPaint, _belSerializer.numeric, true) + (0, _belSerializer.nullable)(this.firstContentfulPaint, _belSerializer.numeric)];
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 || {}, addString); // start with all custom attributes
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.)
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.320.0";
14
+ export const VERSION = "1.320.1";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.320.0";
14
+ export const VERSION = "1.320.1";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -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 both string adder functions
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
- export function addCustomAttributes(attrs, addString) {
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 = addString(key, false);
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 = addString(stringify(val));
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 = addString(val);
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
- return getTimeRelativeToScriptStart(fcpObservedAt);
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
- fcpObservedAt ??= now();
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
- lcpObservedAt = now();
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
  }