@newrelic/browser-agent 1.318.0 → 1.319.0-rc.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +15 -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 +22 -4
  5. package/dist/cjs/common/serialize/bel-serializer.js +3 -3
  6. package/dist/cjs/common/util/console.js +83 -3
  7. package/dist/cjs/common/v2/mfe-vitals.js +59 -24
  8. package/dist/cjs/common/v2/utils.js +1 -1
  9. package/dist/cjs/common/wrap/wrap-websocket.js +5 -3
  10. package/dist/cjs/features/ajax/aggregate/index.js +14 -8
  11. package/dist/cjs/features/generic_events/aggregate/index.js +14 -12
  12. package/dist/cjs/features/generic_events/instrument/index.js +3 -3
  13. package/dist/cjs/loaders/api/register-api-types.js +7 -0
  14. package/dist/cjs/loaders/api/register.js +13 -5
  15. package/dist/esm/common/constants/env.cdn.js +1 -1
  16. package/dist/esm/common/constants/env.npm.js +1 -1
  17. package/dist/esm/common/payloads/payloads.js +22 -4
  18. package/dist/esm/common/serialize/bel-serializer.js +3 -3
  19. package/dist/esm/common/util/console.js +83 -3
  20. package/dist/esm/common/v2/mfe-vitals.js +59 -24
  21. package/dist/esm/common/v2/utils.js +1 -1
  22. package/dist/esm/common/wrap/wrap-websocket.js +5 -3
  23. package/dist/esm/features/ajax/aggregate/index.js +14 -8
  24. package/dist/esm/features/generic_events/aggregate/index.js +14 -12
  25. package/dist/esm/features/generic_events/instrument/index.js +3 -3
  26. package/dist/esm/loaders/api/register-api-types.js +8 -0
  27. package/dist/esm/loaders/api/register.js +13 -5
  28. package/dist/types/common/payloads/payloads.d.ts +15 -4
  29. package/dist/types/common/payloads/payloads.d.ts.map +1 -1
  30. package/dist/types/common/serialize/bel-serializer.d.ts +1 -1
  31. package/dist/types/common/serialize/bel-serializer.d.ts.map +1 -1
  32. package/dist/types/common/util/console.d.ts +82 -4
  33. package/dist/types/common/util/console.d.ts.map +1 -1
  34. package/dist/types/common/v2/mfe-vitals.d.ts +7 -10
  35. package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -1
  36. package/dist/types/common/wrap/wrap-websocket.d.ts +1 -1
  37. package/dist/types/common/wrap/wrap-websocket.d.ts.map +1 -1
  38. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  39. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  40. package/dist/types/loaders/api/register-api-types.d.ts +18 -0
  41. package/package.json +4 -2
  42. package/src/common/payloads/payloads.js +22 -4
  43. package/src/common/serialize/bel-serializer.js +3 -3
  44. package/src/common/util/console.js +83 -3
  45. package/src/common/v2/mfe-vitals.js +57 -24
  46. package/src/common/v2/utils.js +1 -1
  47. package/src/common/wrap/wrap-websocket.js +5 -3
  48. package/src/features/ajax/aggregate/index.js +14 -8
  49. package/src/features/generic_events/aggregate/index.js +14 -12
  50. package/src/features/generic_events/instrument/index.js +3 -3
  51. package/src/loaders/api/register-api-types.js +8 -0
  52. package/src/loaders/api/register.js +5 -5
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
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.319.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.318.0...v1.319.0) (2026-07-28)
7
+
8
+
9
+ ### Features
10
+
11
+ * Automatically Detect MFE Web Sockets ([#1726](https://github.com/newrelic/newrelic-browser-agent/issues/1726)) ([746d055](https://github.com/newrelic/newrelic-browser-agent/commit/746d05504c451b954cd0d2b86b92415c22293f2a))
12
+ * Do not obfuscate attribute keys ([#1810](https://github.com/newrelic/newrelic-browser-agent/issues/1810)) ([c5d9a77](https://github.com/newrelic/newrelic-browser-agent/commit/c5d9a773470d670cb27b63777dd035934f1dbfd9))
13
+ * MFE vitals improvements ([#1813](https://github.com/newrelic/newrelic-browser-agent/issues/1813)) ([21f38a9](https://github.com/newrelic/newrelic-browser-agent/commit/21f38a9af35f5204a47be02bfa2a73d41d8ba1f6))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * Avoid capturing agent AJAX payloads ([#1812](https://github.com/newrelic/newrelic-browser-agent/issues/1812)) ([ba069fc](https://github.com/newrelic/newrelic-browser-agent/commit/ba069fcea5f381db698044b84064bbba97676cec))
19
+ * Keep Ajax payload out of unrelated events ([#1809](https://github.com/newrelic/newrelic-browser-agent/issues/1809)) ([b0cb098](https://github.com/newrelic/newrelic-browser-agent/commit/b0cb098d0ea36d1d35212355bc51051f3c02c1ae))
20
+
6
21
  ## [1.318.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.317.0...v1.318.0) (2026-07-08)
7
22
 
8
23
 
@@ -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.318.0";
20
+ const VERSION = exports.VERSION = "1.319.0-rc.0";
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.318.0";
20
+ const VERSION = exports.VERSION = "1.319.0-rc.0";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -17,13 +17,31 @@ var _constants = require("../../features/ajax/constants");
17
17
 
18
18
  /**
19
19
  * Determines whether payload data should be captured based on the capture mode setting,
20
- * HTTP status code, and GraphQL error status.
20
+ * HTTP status code, and GraphQL error status. Will never capture agent's own payloads.
21
21
  * @param {string} captureMode - The capture mode setting ('none', 'all', or 'failures')
22
- * @param {number} statusCode - The HTTP status code
23
- * @param {boolean} hasGQLErrors - Whether the response contains GraphQL errors
22
+ * @param {Object} event - An object representing the AJAX event
23
+ * @param {number} event.statusCode - The HTTP status code
24
+ * @param {boolean} event.hasGQLErrors - Whether the response contains GraphQL errors
25
+ * @param {string} event.payloadHost - The host of the AJAX payload (includes port)
26
+ * @param {string} event.payloadHostname - The hostname of the AJAX payload
27
+ * @param {string} [event.payloadPathname=''] - The pathname of the AJAX payload
28
+ * @param {string[]} [beacons=[]] - Array of beacon hostnames to avoid capturing
24
29
  * @returns {boolean} True if payload should be captured
25
30
  */
26
- function canCapturePayload(captureMode, statusCode, hasGQLErrors) {
31
+ function canCapturePayload(captureMode, {
32
+ statusCode,
33
+ hasGQLErrors,
34
+ payloadHost,
35
+ payloadHostname,
36
+ payloadPathname = ''
37
+ }, beacons = []) {
38
+ if (payloadHostname) {
39
+ const payloadUrl = payloadHostname + payloadPathname;
40
+ const payloadUrlWithPort = payloadHost + payloadPathname;
41
+ if (beacons.some(b => {
42
+ return b === payloadUrl || payloadUrl.startsWith(b + '/') || b === payloadUrlWithPort || payloadUrlWithPort.startsWith(b + '/');
43
+ })) return false;
44
+ }
27
45
  if (captureMode === _constants.CAPTURE_PAYLOAD_SETTINGS.ALL) return true;
28
46
  if (!captureMode || captureMode === _constants.CAPTURE_PAYLOAD_SETTINGS.NONE) return false;
29
47
 
@@ -28,9 +28,9 @@ function getAddStringContext(obfuscator, truncator) {
28
28
  let stringTableIdx = 0;
29
29
  const stringTable = Object.prototype.hasOwnProperty.call(Object, 'create') ? Object.create(null) : {};
30
30
  return addString;
31
- function addString(str) {
31
+ function addString(str, obfuscate = true) {
32
32
  if (typeof str === 'undefined' || str === '') return '';
33
- str = obfuscator?.obfuscateString(String(str)) ?? String(str);
33
+ str = obfuscate ? obfuscator?.obfuscateString(String(str)) ?? String(str) : String(str);
34
34
  str = truncator?.(str) ?? str;
35
35
  if (hasOwnProp.call(stringTable, str)) {
36
36
  return numeric(stringTable[str], true);
@@ -47,7 +47,7 @@ function addCustomAttributes(attrs, addString) {
47
47
  var type = 5;
48
48
  var serializedValue;
49
49
  // add key to string table first
50
- key = addString(key);
50
+ key = addString(key, false);
51
51
  switch (typeof val) {
52
52
  case 'object':
53
53
  if (val) {
@@ -12,12 +12,92 @@ var _globalEvent = require("../dispatch/global-event");
12
12
 
13
13
  /* eslint no-console: ["error", { allow: ["debug"] }] */
14
14
 
15
+ /* GENERATED-WARNING-CODES:START -- run `npm run generate:warning-codes` after editing docs/warning-codes.md, do not hand-edit this block */
15
16
  /**
16
- * A helper method to warn to the console with New Relic: decoration
17
- * @param {string} message The primary message to warn
18
- * @param {*} [secondary] Secondary data to include, usually an error or object
17
+ * A helper method to warn to the console with New Relic: decoration.
18
+ *
19
+ * `code` corresponds to an entry in
20
+ * {@link https://github.com/newrelic/newrelic-browser-agent/blob/main/docs/warning-codes.md warning-codes.md}:
21
+ *
22
+ * | Code | Message |
23
+ * |------|---------|
24
+ * | 1 | An error occurred while setting a property of a Configurable |
25
+ * | 2 | An error occurred while setting a Configurable |
26
+ * | 3 | Setting a Configurable requires an object as input |
27
+ * | 4 | Setting a Configurable requires a model to set its initial properties |
28
+ * | 5 | An invalid session_replay.mask_selector was provided. \* will be used. |
29
+ * | 6 | An invalid session_replay.block_selector was provided and will not be used |
30
+ * | 7 | An invalid session_replay.mask_input_option was provided and will not be used |
31
+ * | 8 | Shared context requires an object as input |
32
+ * | 9 | An error occurred while setting SharedContext |
33
+ * | 10 | Failed to read from storage API |
34
+ * | 11 | Failed to write to the storage API |
35
+ * | 12 | An obfuscation replacement rule was detected missing a "regex" value. |
36
+ * | 13 | An obfuscation replacement rule contains a "regex" value with an invalid type (must be a string or RegExp) |
37
+ * | 14 | An obfuscation replacement rule contains a "replacement" value with an invalid type (must be a string) |
38
+ * | 15 | An error occurred while intercepting XHR |
39
+ * | 16 | Could not cast log message to string |
40
+ * | 17 | Could not calculate New Relic server time. Agent shutting down. |
41
+ * | 18 | RUM call failed. Agent shutting down. |
42
+ * | 19 | SPA scheduler is not initialized. Saved interaction is not sent! |
43
+ * | 20 | A problem occurred when starting up session manager. This page will not start or extend any session. |
44
+ * | 21 | Failed to initialize the agent. Could not determine the runtime environment. |
45
+ * | 22 | Failed to initialize all enabled instrument classes (agent aborted) - |
46
+ * | 23 | An unexpected issue occurred |
47
+ * | 24 | Something prevented the agent from instrumenting. |
48
+ * | 25 | Something prevented the agent from being downloaded. |
49
+ * | 26 | Failed to initialize instrument classes. |
50
+ * | 27 | Downloading runtime APIs failed... |
51
+ * | 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. |
52
+ * | 29 | Failed to wrap logger: invalid argument(s) |
53
+ * | 30 | Invalid log level |
54
+ * | 31 | Ignored log: Log is larger than maximum payload size |
55
+ * | 32 | Ignored log: Invalid message |
56
+ * | 33 | Session Replay Aborted |
57
+ * | 34 | Downloading and initializing a feature failed... |
58
+ * | 35 | Call to agent api failed. The API is not currently initialized. |
59
+ * | 36 | A feature is enabled but one or more dependent features have not been initialized. This may cause unintended consequences or missing data... |
60
+ * | 37 | Invalid feature name supplied. |
61
+ * | 38 | Call to api was made before agent fully initialized. |
62
+ * | 39 | Failed to execute setCustomAttribute. Name must be a string type. |
63
+ * | 40 | Failed to execute setCustomAttribute. Non-null value must be a string, number or boolean type. |
64
+ * | 41 | Failed to execute setUserId. Non-null value must be a string type. |
65
+ * | 42 | Failed to execute setApplicationVersion. Expected <String \| null> |
66
+ * | 43 | Agent not configured properly. |
67
+ * | 44 | Invalid object passed to generic event aggregate. Missing "eventType". |
68
+ * | 45 | An internal agent process failed to execute. |
69
+ * | 46 | A reserved eventType was provided to recordCustomEvent(...) -- The event was not recorded. |
70
+ * | 47 | We tried to access a stylesheet's contents but failed due to browser security. For best results, ensure that cross-domain CSS assets are decorated with "crossorigin='anonymous'" attribution or are otherwise publicly accessible. |
71
+ * | 48 | Supplied an invalid API target. Must be an <Object> that contains valid (string) id and name properties. |
72
+ * | 49 | Supplied API target is missing an entityGuid. Some APIs may not behave correctly without a valid entityGuid (ex. logs). |
73
+ * | 50 | Failed to connect. Cannot allow registered API. |
74
+ * | 51 | Container agent is not available to register with. Can not connect |
75
+ * | 52 | Unexpected problem encountered. There should be at least one app for harvest! |
76
+ * | 53 | Did not receive a valid entityGuid from connection response |
77
+ * | 54 | An experimental feature is being used. Support can not be offered for issues |
78
+ * | 55 | Register API has been disabled on the container agent |
79
+ * | 56 | Could not find a matching entity to store data |
80
+ * | 57 | Failed to execute measure. Arguments must have valid types. |
81
+ * | 58 | Failed to execute measure. Resulting duration must be non-negative. |
82
+ * | 59 | Session replay harvested before a session trace payload could be sent. This could be problematic for replays that rely on a trace |
83
+ * | 60 | Session trace aborted |
84
+ * | 61 | Timestamps must be non-negative and end time cannot be before start time. |
85
+ * | 62 | Timestamp must be a unix timestamp greater than the page origin time |
86
+ * | 63 | A single event was larger than the maximum allowed payload size |
87
+ * | 64 | Required globals have been mutated before being accessed by the browser agent. This can cause issues and should be avoided. |
88
+ * | 65 | Consent API argument must be boolean or undefined |
89
+ * | 66 | A new agent session has started |
90
+ * | 67 | The "spa" feature has been deprecated and disabled. Please use/import "soft_navigations" instead for tracking of BrowserInteraction data. |
91
+ * | 68 | API has been deregistered and can no longer be used. Call "register" API again with credentials to start over. |
92
+ * | 69 | More than one Browser agent is running on the page |
93
+ * | 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. |
94
+ * | 71 | An invalid feature mode was detected and set to "off". |
95
+ *
96
+ * @param {number} code The warning code to emit, which will be used to link to the warning code documentation
97
+ * @param {*} [secondary] Secondary data to include, usually an extra message, error or object
19
98
  * @returns
20
99
  */
100
+ /* GENERATED-WARNING-CODES:END */
21
101
  function warn(code, secondary) {
22
102
  if (typeof console.debug !== 'function') return;
23
103
  console.debug("New Relic Warning: https://github.com/newrelic/newrelic-browser-agent/blob/main/docs/warning-codes.md#".concat(code), secondary);
@@ -12,7 +12,9 @@ var _now = require("../timing/now");
12
12
  */
13
13
 
14
14
  /**
15
- * @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
15
+ * @typedef {import('../../loaders/api/register-api-types').RegisterAPITimings} RegisterAPITimings
16
+ * @typedef {import('../../loaders/api/register-api-types').RegisterAPITarget} RegisterAPITarget
17
+ * @typedef {import('../../loaders/api/register-api-types').RegisterAPIVitals} RegisterAPIVitals
16
18
  */
17
19
 
18
20
  const isObservable = node => {
@@ -22,6 +24,15 @@ const isObservable = node => {
22
24
  return false;
23
25
  }
24
26
  };
27
+ const isMatch = (dataset, target) => dataset?.nrMfeId === target.id && (!dataset?.nrMfeObserved || dataset?.nrMfeObserved === target.instance);
28
+ const escapeSelectorValue = value => {
29
+ try {
30
+ return _runtime.globalScope.CSS.escape(value);
31
+ } catch (e) {
32
+ // give up
33
+ return value;
34
+ }
35
+ };
25
36
 
26
37
  /**
27
38
  * Check if node is within a specific MFE
@@ -29,12 +40,17 @@ const isObservable = node => {
29
40
  * @param {string} id - MFE ID to match
30
41
  * @returns {boolean}
31
42
  */
32
- const isInMFE = (node, id) => {
33
- if (!node || !id) return false;
43
+ const isInMFE = (node, target = {}) => {
44
+ const id = target.id;
45
+ const instance = target.instance;
46
+ if (!node || !id || !instance) return false;
34
47
  try {
35
48
  let curr = node.nodeType === 1 ? node : node.parentElement;
36
49
  while (curr?.tagName) {
37
- if (curr.dataset?.nrMfeId === id) return true;
50
+ if (isMatch(curr.dataset, target)) {
51
+ curr.dataset.nrMfeObserved = instance; // mark that this MFE has been observed for vitals
52
+ return true;
53
+ }
38
54
  curr = curr.parentNode;
39
55
  }
40
56
  } catch (e) {}
@@ -47,16 +63,19 @@ const isInMFE = (node, id) => {
47
63
  * @param {Function} onMatch - Callback when matching node is *added*
48
64
  * @returns {MutationObserver}
49
65
  */
50
- const observeMutations = (id, onMatch) => {
66
+ const observeMutations = (target, onMatch) => {
51
67
  // Try to find existing MFE root
52
- const mfeRoot = _runtime.globalScope.document?.querySelector("[data-nr-mfe-id=\"".concat(id, "\"]"));
68
+ const potentialMatches = _runtime.globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
69
+ const mfeRoot = (Array.from(potentialMatches) || []).find(x => isMatch(x.dataset, target));
53
70
  let observingRoot = !!mfeRoot;
71
+ if (observingRoot) mfeRoot.dataset.nrMfeObserved ??= target.instance; // mark that this MFE has been observed for vitals
72
+
54
73
  const obs = new _runtime.globalScope.MutationObserver(mutations => {
55
74
  mutations.forEach(m => {
56
75
  m.addedNodes.forEach(node => {
57
76
  // Check if this is the MFE root being added
58
77
  const elem = node.nodeType === 1 ? node : null;
59
- if (elem?.dataset?.nrMfeId === id) {
78
+ if (!observingRoot && isMatch(elem?.dataset, target)) {
60
79
  // Found the root! Lets switch to observing just this subtree for performance reasons
61
80
  obs.disconnect();
62
81
  obs.observe(elem, {
@@ -64,10 +83,11 @@ const observeMutations = (id, onMatch) => {
64
83
  subtree: true
65
84
  });
66
85
  observingRoot = true;
86
+ elem.dataset.nrMfeObserved = target.instance; // mark that this MFE has been observed for vitals
67
87
  }
68
88
 
69
89
  // Only check isInMFE if we're observing the whole document; skip expensive ancestor walk when observing root
70
- if (isObservable(node) && (observingRoot || isInMFE(node, id))) {
90
+ if (isObservable(node) && (observingRoot || isInMFE(node, target))) {
71
91
  onMatch(node, obs);
72
92
  }
73
93
  });
@@ -104,14 +124,15 @@ const observePerformance = (observers, config, onEntry) => {
104
124
 
105
125
  /**
106
126
  * Tracks all Core Web Vitals for a specific MFE.
107
- * @param {string} id - The MFE ID to track
108
- * @returns {{fcp: object|null, lcp: object|null, cls: object|null, inp: object|null, disconnect: Function}}
127
+ * @param {RegisterAPITarget} target - The MFE target to track vitals for
128
+ * @param {RegisterAPITimings} timings - The timings object to use for relative time calculations
129
+ * @returns {RegisterAPIVitals} An object containing the vitals values and a disconnect method to stop tracking
109
130
  */
110
- function trackMFEVitals(id, timings) {
111
- let fcpObservedAt = null;
112
- let lcpObservedAt = null;
131
+ function trackMFEVitals(target, timings) {
132
+ let fcpObservedAt;
133
+ let lcpObservedAt;
113
134
  const getTimeRelativeToScriptStart = capturedAt => {
114
- if (capturedAt === null) return null;
135
+ if (capturedAt == null) return capturedAt;
115
136
  return capturedAt - (timings?.scriptStart || timings?.registeredAt || 0);
116
137
  };
117
138
  const vitals = {
@@ -126,15 +147,21 @@ function trackMFEVitals(id, timings) {
126
147
  }
127
148
  },
128
149
  cls: {
129
- value: null
150
+ value: undefined
130
151
  },
131
152
  inp: {
132
- value: null
153
+ value: undefined
133
154
  },
134
155
  disconnect: () => {}
135
156
  };
136
- if (!id || !_runtime.isBrowserScope || !_runtime.globalScope.MutationObserver || !_runtime.globalScope.PerformanceObserver) return vitals;
157
+ if (!target || !_runtime.isBrowserScope || !_runtime.globalScope.MutationObserver || !_runtime.globalScope.PerformanceObserver) return vitals;
137
158
  const observers = [];
159
+
160
+ // If FCP hasn't been observed within 10 seconds, give up and shut down all observers.
161
+ // Once FCP is observed, the other vitals are left to record until their natural lifespan ends.
162
+ setTimeout(() => {
163
+ if (!fcpObservedAt) vitals.disconnect();
164
+ }, 10000);
138
165
  const populateVitalMinimums = () => {
139
166
  fcpObservedAt ??= (0, _now.now)();
140
167
  lcpObservedAt ??= (0, _now.now)();
@@ -142,14 +169,16 @@ function trackMFEVitals(id, timings) {
142
169
  };
143
170
 
144
171
  // if the MFE has already rendered something on the page before we could set up listeners, just populate vital minimums immediately
145
- if (_runtime.globalScope.document?.querySelector("[data-nr-mfe-id=\"".concat(id, "\"]"))) populateVitalMinimums();
172
+ const existingRoots = _runtime.globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
173
+ if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums();
146
174
 
147
175
  // Track FCP - first contentful paint
148
- observeMutations(id, (_, obs) => {
176
+ const fcpObs = observeMutations(target, (_, obs) => {
149
177
  // An observed "FCP" means _something_ rendered, so at minimum we can populate all the baseline values for the vitals
150
178
  populateVitalMinimums();
151
179
  obs.disconnect();
152
180
  });
181
+ observers.push(fcpObs);
153
182
 
154
183
  // Track LCP - largest contentful paint
155
184
  let largestSize = 0;
@@ -173,7 +202,7 @@ function trackMFEVitals(id, timings) {
173
202
  } catch (e) {
174
203
  // ResizeObserver not supported
175
204
  }
176
- const lcpObs = observeMutations(id, node => {
205
+ const lcpObs = observeMutations(target, node => {
177
206
  // an observed "LCP" means _something_ rendered, so at minimum we can make sure all the baseline values are populated for the vitals
178
207
  populateVitalMinimums();
179
208
  if (resizeObs) {
@@ -225,7 +254,7 @@ function trackMFEVitals(id, timings) {
225
254
  }, entry => {
226
255
  if (entry.hadRecentInput) return;
227
256
  (entry.sources || []).some(source => {
228
- if (isInMFE(source.node, id)) {
257
+ if (isInMFE(source.node, target)) {
229
258
  // 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
230
259
  populateVitalMinimums();
231
260
  vitals.cls.value += entry.value;
@@ -241,8 +270,8 @@ function trackMFEVitals(id, timings) {
241
270
  buffered: true,
242
271
  durationThreshold: 40
243
272
  }, entry => {
244
- if (!entry.interactionId || !isInMFE(entry.target, id)) return;
245
- if (vitals.inp.value === null || entry.duration > vitals.inp.value) {
273
+ if (!entry.interactionId || !isInMFE(entry.target, target)) return;
274
+ if (vitals.inp.value === undefined || entry.duration > vitals.inp.value) {
246
275
  // 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
247
276
  populateVitalMinimums();
248
277
  vitals.inp.value = entry.duration;
@@ -268,6 +297,12 @@ function trackMFEVitals(id, timings) {
268
297
  }
269
298
  });
270
299
  disconnectInteractionListeners();
300
+ ['visibilitychange', 'pagehide'].forEach(type => {
301
+ _runtime.globalScope.removeEventListener(type, vitals.disconnect, {
302
+ once: true,
303
+ passive: true
304
+ });
305
+ });
271
306
  };
272
307
 
273
308
  // Auto-disconnect LCP observer on user interaction (per Web Vitals spec)
@@ -281,7 +316,7 @@ function trackMFEVitals(id, timings) {
281
316
  }
282
317
  };
283
318
  const handleInteraction = event => {
284
- if (!isInMFE(event?.target, id)) return;
319
+ if (!isInMFE(event?.target, target)) return;
285
320
  disconnectLCP();
286
321
  disconnectInteractionListeners();
287
322
  };
@@ -107,7 +107,7 @@ function shouldDuplicate(target, aggregateInstance) {
107
107
  * @returns {Array} An array of targets found from the stack trace. If no targets are found or allowed, returns an array with undefined.
108
108
  */
109
109
  function findTargetsFromStackTrace(agentRef) {
110
- if (!agentRef?.init.api.register.enabled) return [undefined];
110
+ if (!agentRef?.init.api.register.enabled || !agentRef?.runtime?.registeredEntities?.length) return [undefined];
111
111
  const targets = [];
112
112
  try {
113
113
  var urls = (0, _scriptTracker.extractUrlsFromStack)((0, _scriptTracker.getDeepStackTrace)());
@@ -8,6 +8,7 @@ var _runtime = require("../constants/runtime");
8
8
  var _uniqueId = require("../ids/unique-id");
9
9
  var _now = require("../timing/now");
10
10
  var _cleanUrl = require("../url/clean-url");
11
+ var _utils = require("../v2/utils");
11
12
  var _nreum = require("../window/nreum");
12
13
  var _pageVisibility = require("../window/page-visibility");
13
14
  var _events = require("../constants/events");
@@ -19,7 +20,7 @@ var _events = require("../constants/events");
19
20
  const wrapped = {};
20
21
  const openWebSockets = new Set(); // track all instances to close out metrics on page unload
21
22
 
22
- function wrapWebSocket(sharedEE) {
23
+ function wrapWebSocket(sharedEE, agentRef) {
23
24
  const originals = (0, _nreum.gosNREUMOriginals)().o;
24
25
  if (!originals.WS) return sharedEE;
25
26
  const wsEE = sharedEE.get('websockets');
@@ -37,7 +38,7 @@ function wrapWebSocket(sharedEE) {
37
38
  if (ws.nrData.openedAt) {
38
39
  ws.nrData.connectedDuration = unloadTime - ws.nrData.openedAt;
39
40
  }
40
- wsEE.emit('ws', [ws.nrData], ws);
41
+ wsEE.emit('ws', [ws.nrData, ws.targets], ws);
41
42
  });
42
43
  });
43
44
  class WrappedWebSocket extends WebSocket {
@@ -65,6 +66,7 @@ function wrapWebSocket(sharedEE) {
65
66
  super(...args);
66
67
  /** @type {WebSocketData} */
67
68
  this.nrData = new WebSocketData(args[0], args[1]);
69
+ this.targets = (0, _utils.findTargetsFromStackTrace)(agentRef);
68
70
  this.addEventListener('open', () => {
69
71
  this.nrData.openedAt = (0, _now.now)();
70
72
  ['protocol', 'extensions', 'binaryType'].forEach(prop => {
@@ -93,7 +95,7 @@ function wrapWebSocket(sharedEE) {
93
95
  this.nrData.closeWasClean = event.wasClean;
94
96
  this.nrData.connectedDuration = this.nrData.closedAt - this.nrData.openedAt;
95
97
  openWebSockets.delete(this); // remove from tracking set since it's now closed
96
- wsEE.emit('ws', [this.nrData], this);
98
+ wsEE.emit('ws', [this.nrData, this.targets], this);
97
99
  });
98
100
  }
99
101
  addEventListener(type, listener, ...rest) {
@@ -97,20 +97,26 @@ class Aggregate extends _aggregateBase.AggregateBase {
97
97
  callbackDuration: metrics.cbTime,
98
98
  [_constants.AJAX_ID]: (0, _uniqueId.generateUuid)() // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
99
99
  };
100
- event.gql = params.gql = (0, _gql.parseGQL)({
100
+ event.gql = (0, _gql.parseGQL)({
101
101
  body: ctx.requestBody,
102
102
  query: ctx.parsedOrigin?.search
103
103
  });
104
- if (event.gql) event.gql.operationHasErrors = params.gql.operationHasErrors = (0, _gql.hasGQLErrors)(ctx.responseBody);
104
+ if (event.gql) event.gql.operationHasErrors = (0, _gql.hasGQLErrors)(ctx.responseBody);
105
105
  const capturePayloadSetting = this.agentRef.init.ajax.capture_payloads;
106
- const shouldCapturePayload = (0, _payloads.canCapturePayload)(capturePayloadSetting, params.status, event.gql?.operationHasErrors);
106
+ const shouldCapturePayload = (0, _payloads.canCapturePayload)(capturePayloadSetting, {
107
+ statusCode: event.status,
108
+ hasGQLErrors: event.gql?.operationHasErrors,
109
+ payloadHostname: params.hostname,
110
+ payloadHost: params.host,
111
+ payloadPathname: params.pathname
112
+ }, this.agentRef.beacons);
107
113
  if (shouldCapturePayload) {
108
114
  // Store raw data; obfuscation and truncation will happen in the serializer
109
- params.requestQuery = event.requestQuery = (0, _payloads.parseQueryString)(ctx.parsedOrigin?.search);
110
- params.requestHeaders = event.requestHeaders = ctx.requestHeaders;
111
- params.responseHeaders = event.responseHeaders = ctx.responseHeaders;
112
- if ((0, _payloads.isLikelyHumanReadable)(ctx.requestHeaders, ctx.requestBody)) params.requestBody = event.requestBody = ctx.requestBody;
113
- if ((0, _payloads.isLikelyHumanReadable)(ctx.responseHeaders, ctx.responseBody)) params.responseBody = event.responseBody = ctx.responseBody;
115
+ event.requestQuery = (0, _payloads.parseQueryString)(ctx.parsedOrigin?.search);
116
+ event.requestHeaders = ctx.requestHeaders;
117
+ event.responseHeaders = ctx.responseHeaders;
118
+ if ((0, _payloads.isLikelyHumanReadable)(ctx.requestHeaders, ctx.requestBody)) event.requestBody = ctx.requestBody;
119
+ if ((0, _payloads.isLikelyHumanReadable)(ctx.responseHeaders, ctx.responseBody)) event.responseBody = ctx.responseBody;
114
120
  }
115
121
  if (ctx.dt) {
116
122
  event.spanId = ctx.dt.spanId;
@@ -262,19 +262,21 @@ class Aggregate extends _aggregateBase.AggregateBase {
262
262
  this.addEvent(event, target);
263
263
  }, this.featureName, this.ee);
264
264
  if (agentRef.init.feature_flags.includes('websockets') || agentRef.init.web_sockets?.enabled) {
265
- (0, _registerHandler.registerHandler)('ws-complete', nrData => {
266
- const event = {
267
- ...nrData,
268
- eventType: _events.EVENT_TYPES.WS,
269
- timestamp: this.#toEpoch(nrData.timestamp),
270
- openedAt: this.#toEpoch(nrData.openedAt),
271
- closedAt: this.#toEpoch(nrData.closedAt)
272
- };
265
+ (0, _registerHandler.registerHandler)('ws-complete', (nrData, targets = [undefined]) => {
266
+ targets.forEach(target => {
267
+ const event = {
268
+ ...nrData,
269
+ eventType: _events.EVENT_TYPES.WS,
270
+ timestamp: this.#toEpoch(nrData.timestamp),
271
+ openedAt: this.#toEpoch(nrData.openedAt),
272
+ closedAt: this.#toEpoch(nrData.closedAt)
273
+ };
273
274
 
274
- // Report supportability metrics for WebSocket completion
275
- this.reportSupportabilityMetric('WebSocket/Completed/Seen');
276
- this.reportSupportabilityMetric('WebSocket/Completed/Bytes', (0, _stringify.stringify)(event).length);
277
- this.addEvent(event);
275
+ // Report supportability metrics for WebSocket completion
276
+ this.reportSupportabilityMetric('WebSocket/Completed/Seen');
277
+ this.reportSupportabilityMetric('WebSocket/Completed/Bytes', (0, _stringify.stringify)(event).length);
278
+ this.addEvent(event, target);
279
+ });
278
280
  }, this.featureName, this.ee);
279
281
  }
280
282
  if (!agentRef.init.feature_flags.includes('no_spv')) {
@@ -50,9 +50,9 @@ class Instrument extends _instrumentBase.InstrumentBase {
50
50
  let historyEE;
51
51
  if (websocketsEnabled) {
52
52
  // this can apply outside browser scope such as in worker
53
- const websocketsEE = (0, _wrapWebsocket.wrapWebSocket)(this.ee);
54
- websocketsEE.on('ws', nrData => {
55
- (0, _handle.handle)('ws-complete', [nrData], undefined, this.featureName, this.ee);
53
+ const websocketsEE = (0, _wrapWebsocket.wrapWebSocket)(this.ee, agentRef);
54
+ websocketsEE.on('ws', (nrData, targets) => {
55
+ (0, _handle.handle)('ws-complete', [nrData, targets], undefined, this.featureName, this.ee);
56
56
  });
57
57
  }
58
58
  if (securityPolicyViolationEnabled) {
@@ -53,4 +53,11 @@ exports.default = void 0;
53
53
  * @property {Object} [asset] - The asset path (if found) for the registered entity.
54
54
  * @property {string} type - The type of timing associated with the registered entity, 'script' or 'link' if found with the performance resource API, 'fetch' for dynamic imports, 'inline' if found to be associated with the root document URL, or 'unknown' if no associated resource could be found.
55
55
  */
56
+ /**
57
+ * @typedef {Object} RegisterAPIVitals
58
+ * @property {number} [fcp] - The first contentful paint timing for the registered entity.
59
+ * @property {number} [lcp] - The largest contentful paint timing for the registered entity.
60
+ * @property {number} [cls] - The cumulative layout shift score for the registered entity.
61
+ * @property {number} [inp] - The interaction to next paint timing for the registered entity.
62
+ */
56
63
  var _default = exports.default = {};
@@ -87,7 +87,7 @@ function register(agentRef, target) {
87
87
  const timings = (0, _scriptTracker.findScriptTimings)();
88
88
 
89
89
  // Track MFE vitals for this entity
90
- const vitals = (0, _mfeVitals.trackMFEVitals)(target.id, timings);
90
+ const vitals = (0, _mfeVitals.trackMFEVitals)(target, timings);
91
91
  const attrs = {};
92
92
 
93
93
  // Only define attributes getter if it doesn't already exist
@@ -233,13 +233,21 @@ function register(agentRef, target) {
233
233
  timeToRegister: timings.registeredAt,
234
234
  // timestamp when register() was called
235
235
  // leave room to extend these with more data keys as needed
236
- 'nr.vitals.fcp.value': vitals.fcp?.value ?? null,
236
+ ...(vitals.fcp.value >= 0 && {
237
+ 'nr.vitals.fcp.value': vitals.fcp.value
238
+ }),
237
239
  // FCP vital object with value and metadata
238
- 'nr.vitals.lcp.value': vitals.lcp?.value ?? null,
240
+ ...(vitals.lcp.value >= 0 && {
241
+ 'nr.vitals.lcp.value': vitals.lcp.value
242
+ }),
239
243
  // LCP vital object with value and metadata
240
- 'nr.vitals.cls.value': vitals.cls?.value ?? null,
244
+ ...(vitals.cls.value >= 0 && {
245
+ 'nr.vitals.cls.value': vitals.cls.value
246
+ }),
241
247
  // CLS vital object with value and metadata
242
- 'nr.vitals.inp.value': vitals.inp?.value ?? null // INP vital object with value and metadata
248
+ ...(vitals.inp.value >= 0 && {
249
+ 'nr.vitals.inp.value': vitals.inp.value
250
+ }) // INP vital object with value and metadata
243
251
  };
244
252
  api.recordCustomEvent('MicroFrontEndTiming', eventData);
245
253
  }
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.318.0";
14
+ export const VERSION = "1.319.0-rc.0";
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.318.0";
14
+ export const VERSION = "1.319.0-rc.0";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -8,13 +8,31 @@ import { CAPTURE_PAYLOAD_SETTINGS } from '../../features/ajax/constants';
8
8
 
9
9
  /**
10
10
  * Determines whether payload data should be captured based on the capture mode setting,
11
- * HTTP status code, and GraphQL error status.
11
+ * HTTP status code, and GraphQL error status. Will never capture agent's own payloads.
12
12
  * @param {string} captureMode - The capture mode setting ('none', 'all', or 'failures')
13
- * @param {number} statusCode - The HTTP status code
14
- * @param {boolean} hasGQLErrors - Whether the response contains GraphQL errors
13
+ * @param {Object} event - An object representing the AJAX event
14
+ * @param {number} event.statusCode - The HTTP status code
15
+ * @param {boolean} event.hasGQLErrors - Whether the response contains GraphQL errors
16
+ * @param {string} event.payloadHost - The host of the AJAX payload (includes port)
17
+ * @param {string} event.payloadHostname - The hostname of the AJAX payload
18
+ * @param {string} [event.payloadPathname=''] - The pathname of the AJAX payload
19
+ * @param {string[]} [beacons=[]] - Array of beacon hostnames to avoid capturing
15
20
  * @returns {boolean} True if payload should be captured
16
21
  */
17
- export function canCapturePayload(captureMode, statusCode, hasGQLErrors) {
22
+ export function canCapturePayload(captureMode, {
23
+ statusCode,
24
+ hasGQLErrors,
25
+ payloadHost,
26
+ payloadHostname,
27
+ payloadPathname = ''
28
+ }, beacons = []) {
29
+ if (payloadHostname) {
30
+ const payloadUrl = payloadHostname + payloadPathname;
31
+ const payloadUrlWithPort = payloadHost + payloadPathname;
32
+ if (beacons.some(b => {
33
+ return b === payloadUrl || payloadUrl.startsWith(b + '/') || b === payloadUrlWithPort || payloadUrlWithPort.startsWith(b + '/');
34
+ })) return false;
35
+ }
18
36
  if (captureMode === CAPTURE_PAYLOAD_SETTINGS.ALL) return true;
19
37
  if (!captureMode || captureMode === CAPTURE_PAYLOAD_SETTINGS.NONE) return false;
20
38