@newrelic/browser-agent 1.318.0 → 1.319.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 +3 -1
  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
@@ -19,9 +19,9 @@ export function getAddStringContext(obfuscator, truncator) {
19
19
  let stringTableIdx = 0;
20
20
  const stringTable = Object.prototype.hasOwnProperty.call(Object, 'create') ? Object.create(null) : {};
21
21
  return addString;
22
- function addString(str) {
22
+ function addString(str, obfuscate = true) {
23
23
  if (typeof str === 'undefined' || str === '') return '';
24
- str = obfuscator?.obfuscateString(String(str)) ?? String(str);
24
+ str = obfuscate ? obfuscator?.obfuscateString(String(str)) ?? String(str) : String(str);
25
25
  str = truncator?.(str) ?? str;
26
26
  if (hasOwnProp.call(stringTable, str)) {
27
27
  return numeric(stringTable[str], true);
@@ -38,7 +38,7 @@ export function addCustomAttributes(attrs, addString) {
38
38
  var type = 5;
39
39
  var serializedValue;
40
40
  // add key to string table first
41
- key = addString(key);
41
+ key = addString(key, false);
42
42
  switch (typeof val) {
43
43
  case 'object':
44
44
  if (val) {
@@ -7,12 +7,92 @@ import { dispatchGlobalEvent } from '../dispatch/global-event';
7
7
 
8
8
  /* eslint no-console: ["error", { allow: ["debug"] }] */
9
9
 
10
+ /* GENERATED-WARNING-CODES:START -- run `npm run generate:warning-codes` after editing docs/warning-codes.md, do not hand-edit this block */
10
11
  /**
11
- * A helper method to warn to the console with New Relic: decoration
12
- * @param {string} message The primary message to warn
13
- * @param {*} [secondary] Secondary data to include, usually an error or object
12
+ * A helper method to warn to the console with New Relic: decoration.
13
+ *
14
+ * `code` corresponds to an entry in
15
+ * {@link https://github.com/newrelic/newrelic-browser-agent/blob/main/docs/warning-codes.md warning-codes.md}:
16
+ *
17
+ * | Code | Message |
18
+ * |------|---------|
19
+ * | 1 | An error occurred while setting a property of a Configurable |
20
+ * | 2 | An error occurred while setting a Configurable |
21
+ * | 3 | Setting a Configurable requires an object as input |
22
+ * | 4 | Setting a Configurable requires a model to set its initial properties |
23
+ * | 5 | An invalid session_replay.mask_selector was provided. \* will be used. |
24
+ * | 6 | An invalid session_replay.block_selector was provided and will not be used |
25
+ * | 7 | An invalid session_replay.mask_input_option was provided and will not be used |
26
+ * | 8 | Shared context requires an object as input |
27
+ * | 9 | An error occurred while setting SharedContext |
28
+ * | 10 | Failed to read from storage API |
29
+ * | 11 | Failed to write to the storage API |
30
+ * | 12 | An obfuscation replacement rule was detected missing a "regex" value. |
31
+ * | 13 | An obfuscation replacement rule contains a "regex" value with an invalid type (must be a string or RegExp) |
32
+ * | 14 | An obfuscation replacement rule contains a "replacement" value with an invalid type (must be a string) |
33
+ * | 15 | An error occurred while intercepting XHR |
34
+ * | 16 | Could not cast log message to string |
35
+ * | 17 | Could not calculate New Relic server time. Agent shutting down. |
36
+ * | 18 | RUM call failed. Agent shutting down. |
37
+ * | 19 | SPA scheduler is not initialized. Saved interaction is not sent! |
38
+ * | 20 | A problem occurred when starting up session manager. This page will not start or extend any session. |
39
+ * | 21 | Failed to initialize the agent. Could not determine the runtime environment. |
40
+ * | 22 | Failed to initialize all enabled instrument classes (agent aborted) - |
41
+ * | 23 | An unexpected issue occurred |
42
+ * | 24 | Something prevented the agent from instrumenting. |
43
+ * | 25 | Something prevented the agent from being downloaded. |
44
+ * | 26 | Failed to initialize instrument classes. |
45
+ * | 27 | Downloading runtime APIs failed... |
46
+ * | 28 | The Browser Agent is attempting to send a very large payload. This is usually tied to large amounts of custom attributes. Please check your configurations. |
47
+ * | 29 | Failed to wrap logger: invalid argument(s) |
48
+ * | 30 | Invalid log level |
49
+ * | 31 | Ignored log: Log is larger than maximum payload size |
50
+ * | 32 | Ignored log: Invalid message |
51
+ * | 33 | Session Replay Aborted |
52
+ * | 34 | Downloading and initializing a feature failed... |
53
+ * | 35 | Call to agent api failed. The API is not currently initialized. |
54
+ * | 36 | A feature is enabled but one or more dependent features have not been initialized. This may cause unintended consequences or missing data... |
55
+ * | 37 | Invalid feature name supplied. |
56
+ * | 38 | Call to api was made before agent fully initialized. |
57
+ * | 39 | Failed to execute setCustomAttribute. Name must be a string type. |
58
+ * | 40 | Failed to execute setCustomAttribute. Non-null value must be a string, number or boolean type. |
59
+ * | 41 | Failed to execute setUserId. Non-null value must be a string type. |
60
+ * | 42 | Failed to execute setApplicationVersion. Expected <String \| null> |
61
+ * | 43 | Agent not configured properly. |
62
+ * | 44 | Invalid object passed to generic event aggregate. Missing "eventType". |
63
+ * | 45 | An internal agent process failed to execute. |
64
+ * | 46 | A reserved eventType was provided to recordCustomEvent(...) -- The event was not recorded. |
65
+ * | 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. |
66
+ * | 48 | Supplied an invalid API target. Must be an <Object> that contains valid (string) id and name properties. |
67
+ * | 49 | Supplied API target is missing an entityGuid. Some APIs may not behave correctly without a valid entityGuid (ex. logs). |
68
+ * | 50 | Failed to connect. Cannot allow registered API. |
69
+ * | 51 | Container agent is not available to register with. Can not connect |
70
+ * | 52 | Unexpected problem encountered. There should be at least one app for harvest! |
71
+ * | 53 | Did not receive a valid entityGuid from connection response |
72
+ * | 54 | An experimental feature is being used. Support can not be offered for issues |
73
+ * | 55 | Register API has been disabled on the container agent |
74
+ * | 56 | Could not find a matching entity to store data |
75
+ * | 57 | Failed to execute measure. Arguments must have valid types. |
76
+ * | 58 | Failed to execute measure. Resulting duration must be non-negative. |
77
+ * | 59 | Session replay harvested before a session trace payload could be sent. This could be problematic for replays that rely on a trace |
78
+ * | 60 | Session trace aborted |
79
+ * | 61 | Timestamps must be non-negative and end time cannot be before start time. |
80
+ * | 62 | Timestamp must be a unix timestamp greater than the page origin time |
81
+ * | 63 | A single event was larger than the maximum allowed payload size |
82
+ * | 64 | Required globals have been mutated before being accessed by the browser agent. This can cause issues and should be avoided. |
83
+ * | 65 | Consent API argument must be boolean or undefined |
84
+ * | 66 | A new agent session has started |
85
+ * | 67 | The "spa" feature has been deprecated and disabled. Please use/import "soft_navigations" instead for tracking of BrowserInteraction data. |
86
+ * | 68 | API has been deregistered and can no longer be used. Call "register" API again with credentials to start over. |
87
+ * | 69 | More than one Browser agent is running on the page |
88
+ * | 70 | A session replay payload failed to send and is being retried. Recording is paused during the retry period, and will resume when a successful harvest is made. Some replay activity may be missed during retry phases. |
89
+ * | 71 | An invalid feature mode was detected and set to "off". |
90
+ *
91
+ * @param {number} code The warning code to emit, which will be used to link to the warning code documentation
92
+ * @param {*} [secondary] Secondary data to include, usually an extra message, error or object
14
93
  * @returns
15
94
  */
95
+ /* GENERATED-WARNING-CODES:END */
16
96
  export function warn(code, secondary) {
17
97
  if (typeof console.debug !== 'function') return;
18
98
  console.debug("New Relic Warning: https://github.com/newrelic/newrelic-browser-agent/blob/main/docs/warning-codes.md#".concat(code), secondary);
@@ -7,7 +7,9 @@ import { globalScope, isBrowserScope } from '../constants/runtime';
7
7
  import { now } from '../timing/now';
8
8
 
9
9
  /**
10
- * @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
10
+ * @typedef {import('../../loaders/api/register-api-types').RegisterAPITimings} RegisterAPITimings
11
+ * @typedef {import('../../loaders/api/register-api-types').RegisterAPITarget} RegisterAPITarget
12
+ * @typedef {import('../../loaders/api/register-api-types').RegisterAPIVitals} RegisterAPIVitals
11
13
  */
12
14
 
13
15
  const isObservable = node => {
@@ -17,6 +19,15 @@ const isObservable = node => {
17
19
  return false;
18
20
  }
19
21
  };
22
+ const isMatch = (dataset, target) => dataset?.nrMfeId === target.id && (!dataset?.nrMfeObserved || dataset?.nrMfeObserved === target.instance);
23
+ const escapeSelectorValue = value => {
24
+ try {
25
+ return globalScope.CSS.escape(value);
26
+ } catch (e) {
27
+ // give up
28
+ return value;
29
+ }
30
+ };
20
31
 
21
32
  /**
22
33
  * Check if node is within a specific MFE
@@ -24,12 +35,17 @@ const isObservable = node => {
24
35
  * @param {string} id - MFE ID to match
25
36
  * @returns {boolean}
26
37
  */
27
- const isInMFE = (node, id) => {
28
- if (!node || !id) return false;
38
+ const isInMFE = (node, target = {}) => {
39
+ const id = target.id;
40
+ const instance = target.instance;
41
+ if (!node || !id || !instance) return false;
29
42
  try {
30
43
  let curr = node.nodeType === 1 ? node : node.parentElement;
31
44
  while (curr?.tagName) {
32
- if (curr.dataset?.nrMfeId === id) return true;
45
+ if (isMatch(curr.dataset, target)) {
46
+ curr.dataset.nrMfeObserved = instance; // mark that this MFE has been observed for vitals
47
+ return true;
48
+ }
33
49
  curr = curr.parentNode;
34
50
  }
35
51
  } catch (e) {}
@@ -42,16 +58,19 @@ const isInMFE = (node, id) => {
42
58
  * @param {Function} onMatch - Callback when matching node is *added*
43
59
  * @returns {MutationObserver}
44
60
  */
45
- const observeMutations = (id, onMatch) => {
61
+ const observeMutations = (target, onMatch) => {
46
62
  // Try to find existing MFE root
47
- const mfeRoot = globalScope.document?.querySelector("[data-nr-mfe-id=\"".concat(id, "\"]"));
63
+ const potentialMatches = globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
64
+ const mfeRoot = (Array.from(potentialMatches) || []).find(x => isMatch(x.dataset, target));
48
65
  let observingRoot = !!mfeRoot;
66
+ if (observingRoot) mfeRoot.dataset.nrMfeObserved ??= target.instance; // mark that this MFE has been observed for vitals
67
+
49
68
  const obs = new globalScope.MutationObserver(mutations => {
50
69
  mutations.forEach(m => {
51
70
  m.addedNodes.forEach(node => {
52
71
  // Check if this is the MFE root being added
53
72
  const elem = node.nodeType === 1 ? node : null;
54
- if (elem?.dataset?.nrMfeId === id) {
73
+ if (!observingRoot && isMatch(elem?.dataset, target)) {
55
74
  // Found the root! Lets switch to observing just this subtree for performance reasons
56
75
  obs.disconnect();
57
76
  obs.observe(elem, {
@@ -59,10 +78,11 @@ const observeMutations = (id, onMatch) => {
59
78
  subtree: true
60
79
  });
61
80
  observingRoot = true;
81
+ elem.dataset.nrMfeObserved = target.instance; // mark that this MFE has been observed for vitals
62
82
  }
63
83
 
64
84
  // Only check isInMFE if we're observing the whole document; skip expensive ancestor walk when observing root
65
- if (isObservable(node) && (observingRoot || isInMFE(node, id))) {
85
+ if (isObservable(node) && (observingRoot || isInMFE(node, target))) {
66
86
  onMatch(node, obs);
67
87
  }
68
88
  });
@@ -99,14 +119,15 @@ const observePerformance = (observers, config, onEntry) => {
99
119
 
100
120
  /**
101
121
  * Tracks all Core Web Vitals for a specific MFE.
102
- * @param {string} id - The MFE ID to track
103
- * @returns {{fcp: object|null, lcp: object|null, cls: object|null, inp: object|null, disconnect: Function}}
122
+ * @param {RegisterAPITarget} target - The MFE target to track vitals for
123
+ * @param {RegisterAPITimings} timings - The timings object to use for relative time calculations
124
+ * @returns {RegisterAPIVitals} An object containing the vitals values and a disconnect method to stop tracking
104
125
  */
105
- export function trackMFEVitals(id, timings) {
106
- let fcpObservedAt = null;
107
- let lcpObservedAt = null;
126
+ export function trackMFEVitals(target, timings) {
127
+ let fcpObservedAt;
128
+ let lcpObservedAt;
108
129
  const getTimeRelativeToScriptStart = capturedAt => {
109
- if (capturedAt === null) return null;
130
+ if (capturedAt == null) return capturedAt;
110
131
  return capturedAt - (timings?.scriptStart || timings?.registeredAt || 0);
111
132
  };
112
133
  const vitals = {
@@ -121,15 +142,21 @@ export function trackMFEVitals(id, timings) {
121
142
  }
122
143
  },
123
144
  cls: {
124
- value: null
145
+ value: undefined
125
146
  },
126
147
  inp: {
127
- value: null
148
+ value: undefined
128
149
  },
129
150
  disconnect: () => {}
130
151
  };
131
- if (!id || !isBrowserScope || !globalScope.MutationObserver || !globalScope.PerformanceObserver) return vitals;
152
+ if (!target || !isBrowserScope || !globalScope.MutationObserver || !globalScope.PerformanceObserver) return vitals;
132
153
  const observers = [];
154
+
155
+ // If FCP hasn't been observed within 10 seconds, give up and shut down all observers.
156
+ // Once FCP is observed, the other vitals are left to record until their natural lifespan ends.
157
+ setTimeout(() => {
158
+ if (!fcpObservedAt) vitals.disconnect();
159
+ }, 10000);
133
160
  const populateVitalMinimums = () => {
134
161
  fcpObservedAt ??= now();
135
162
  lcpObservedAt ??= now();
@@ -137,14 +164,16 @@ export function trackMFEVitals(id, timings) {
137
164
  };
138
165
 
139
166
  // if the MFE has already rendered something on the page before we could set up listeners, just populate vital minimums immediately
140
- if (globalScope.document?.querySelector("[data-nr-mfe-id=\"".concat(id, "\"]"))) populateVitalMinimums();
167
+ const existingRoots = globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
168
+ if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums();
141
169
 
142
170
  // Track FCP - first contentful paint
143
- observeMutations(id, (_, obs) => {
171
+ const fcpObs = observeMutations(target, (_, obs) => {
144
172
  // An observed "FCP" means _something_ rendered, so at minimum we can populate all the baseline values for the vitals
145
173
  populateVitalMinimums();
146
174
  obs.disconnect();
147
175
  });
176
+ observers.push(fcpObs);
148
177
 
149
178
  // Track LCP - largest contentful paint
150
179
  let largestSize = 0;
@@ -168,7 +197,7 @@ export function trackMFEVitals(id, timings) {
168
197
  } catch (e) {
169
198
  // ResizeObserver not supported
170
199
  }
171
- const lcpObs = observeMutations(id, node => {
200
+ const lcpObs = observeMutations(target, node => {
172
201
  // an observed "LCP" means _something_ rendered, so at minimum we can make sure all the baseline values are populated for the vitals
173
202
  populateVitalMinimums();
174
203
  if (resizeObs) {
@@ -220,7 +249,7 @@ export function trackMFEVitals(id, timings) {
220
249
  }, entry => {
221
250
  if (entry.hadRecentInput) return;
222
251
  (entry.sources || []).some(source => {
223
- if (isInMFE(source.node, id)) {
252
+ if (isInMFE(source.node, target)) {
224
253
  // 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
225
254
  populateVitalMinimums();
226
255
  vitals.cls.value += entry.value;
@@ -236,8 +265,8 @@ export function trackMFEVitals(id, timings) {
236
265
  buffered: true,
237
266
  durationThreshold: 40
238
267
  }, entry => {
239
- if (!entry.interactionId || !isInMFE(entry.target, id)) return;
240
- if (vitals.inp.value === null || entry.duration > vitals.inp.value) {
268
+ if (!entry.interactionId || !isInMFE(entry.target, target)) return;
269
+ if (vitals.inp.value === undefined || entry.duration > vitals.inp.value) {
241
270
  // 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
242
271
  populateVitalMinimums();
243
272
  vitals.inp.value = entry.duration;
@@ -263,6 +292,12 @@ export function trackMFEVitals(id, timings) {
263
292
  }
264
293
  });
265
294
  disconnectInteractionListeners();
295
+ ['visibilitychange', 'pagehide'].forEach(type => {
296
+ globalScope.removeEventListener(type, vitals.disconnect, {
297
+ once: true,
298
+ passive: true
299
+ });
300
+ });
266
301
  };
267
302
 
268
303
  // Auto-disconnect LCP observer on user interaction (per Web Vitals spec)
@@ -276,7 +311,7 @@ export function trackMFEVitals(id, timings) {
276
311
  }
277
312
  };
278
313
  const handleInteraction = event => {
279
- if (!isInMFE(event?.target, id)) return;
314
+ if (!isInMFE(event?.target, target)) return;
280
315
  disconnectLCP();
281
316
  disconnectInteractionListeners();
282
317
  };
@@ -96,7 +96,7 @@ export function shouldDuplicate(target, aggregateInstance) {
96
96
  * @returns {Array} An array of targets found from the stack trace. If no targets are found or allowed, returns an array with undefined.
97
97
  */
98
98
  export function findTargetsFromStackTrace(agentRef) {
99
- if (!agentRef?.init.api.register.enabled) return [undefined];
99
+ if (!agentRef?.init.api.register.enabled || !agentRef?.runtime?.registeredEntities?.length) return [undefined];
100
100
  const targets = [];
101
101
  try {
102
102
  var urls = extractUrlsFromStack(getDeepStackTrace());
@@ -6,13 +6,14 @@ import { globalScope } from '../constants/runtime';
6
6
  import { generateRandomHexString } from '../ids/unique-id';
7
7
  import { now } from '../timing/now';
8
8
  import { cleanURL } from '../url/clean-url';
9
+ import { findTargetsFromStackTrace } from '../v2/utils';
9
10
  import { gosNREUMOriginals } from '../window/nreum';
10
11
  import { subscribeToPageUnload } from '../window/page-visibility';
11
12
  import { EVENT_TYPES } from '../constants/events';
12
13
  const wrapped = {};
13
14
  const openWebSockets = new Set(); // track all instances to close out metrics on page unload
14
15
 
15
- export function wrapWebSocket(sharedEE) {
16
+ export function wrapWebSocket(sharedEE, agentRef) {
16
17
  const originals = gosNREUMOriginals().o;
17
18
  if (!originals.WS) return sharedEE;
18
19
  const wsEE = sharedEE.get('websockets');
@@ -30,7 +31,7 @@ export function wrapWebSocket(sharedEE) {
30
31
  if (ws.nrData.openedAt) {
31
32
  ws.nrData.connectedDuration = unloadTime - ws.nrData.openedAt;
32
33
  }
33
- wsEE.emit('ws', [ws.nrData], ws);
34
+ wsEE.emit('ws', [ws.nrData, ws.targets], ws);
34
35
  });
35
36
  });
36
37
  class WrappedWebSocket extends WebSocket {
@@ -58,6 +59,7 @@ export function wrapWebSocket(sharedEE) {
58
59
  super(...args);
59
60
  /** @type {WebSocketData} */
60
61
  this.nrData = new WebSocketData(args[0], args[1]);
62
+ this.targets = findTargetsFromStackTrace(agentRef);
61
63
  this.addEventListener('open', () => {
62
64
  this.nrData.openedAt = now();
63
65
  ['protocol', 'extensions', 'binaryType'].forEach(prop => {
@@ -86,7 +88,7 @@ export function wrapWebSocket(sharedEE) {
86
88
  this.nrData.closeWasClean = event.wasClean;
87
89
  this.nrData.connectedDuration = this.nrData.closedAt - this.nrData.openedAt;
88
90
  openWebSockets.delete(this); // remove from tracking set since it's now closed
89
- wsEE.emit('ws', [this.nrData], this);
91
+ wsEE.emit('ws', [this.nrData, this.targets], this);
90
92
  });
91
93
  }
92
94
  addEventListener(type, listener, ...rest) {
@@ -90,20 +90,26 @@ export class Aggregate extends AggregateBase {
90
90
  callbackDuration: metrics.cbTime,
91
91
  [AJAX_ID]: generateUuid() // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
92
92
  };
93
- event.gql = params.gql = parseGQL({
93
+ event.gql = parseGQL({
94
94
  body: ctx.requestBody,
95
95
  query: ctx.parsedOrigin?.search
96
96
  });
97
- if (event.gql) event.gql.operationHasErrors = params.gql.operationHasErrors = hasGQLErrors(ctx.responseBody);
97
+ if (event.gql) event.gql.operationHasErrors = hasGQLErrors(ctx.responseBody);
98
98
  const capturePayloadSetting = this.agentRef.init.ajax.capture_payloads;
99
- const shouldCapturePayload = canCapturePayload(capturePayloadSetting, params.status, event.gql?.operationHasErrors);
99
+ const shouldCapturePayload = canCapturePayload(capturePayloadSetting, {
100
+ statusCode: event.status,
101
+ hasGQLErrors: event.gql?.operationHasErrors,
102
+ payloadHostname: params.hostname,
103
+ payloadHost: params.host,
104
+ payloadPathname: params.pathname
105
+ }, this.agentRef.beacons);
100
106
  if (shouldCapturePayload) {
101
107
  // Store raw data; obfuscation and truncation will happen in the serializer
102
- params.requestQuery = event.requestQuery = parseQueryString(ctx.parsedOrigin?.search);
103
- params.requestHeaders = event.requestHeaders = ctx.requestHeaders;
104
- params.responseHeaders = event.responseHeaders = ctx.responseHeaders;
105
- if (isLikelyHumanReadable(ctx.requestHeaders, ctx.requestBody)) params.requestBody = event.requestBody = ctx.requestBody;
106
- if (isLikelyHumanReadable(ctx.responseHeaders, ctx.responseBody)) params.responseBody = event.responseBody = ctx.responseBody;
108
+ event.requestQuery = parseQueryString(ctx.parsedOrigin?.search);
109
+ event.requestHeaders = ctx.requestHeaders;
110
+ event.responseHeaders = ctx.responseHeaders;
111
+ if (isLikelyHumanReadable(ctx.requestHeaders, ctx.requestBody)) event.requestBody = ctx.requestBody;
112
+ if (isLikelyHumanReadable(ctx.responseHeaders, ctx.responseBody)) event.responseBody = ctx.responseBody;
107
113
  }
108
114
  if (ctx.dt) {
109
115
  event.spanId = ctx.dt.spanId;
@@ -255,19 +255,21 @@ export class Aggregate extends AggregateBase {
255
255
  this.addEvent(event, target);
256
256
  }, this.featureName, this.ee);
257
257
  if (agentRef.init.feature_flags.includes('websockets') || agentRef.init.web_sockets?.enabled) {
258
- registerHandler('ws-complete', nrData => {
259
- const event = {
260
- ...nrData,
261
- eventType: EVENT_TYPES.WS,
262
- timestamp: this.#toEpoch(nrData.timestamp),
263
- openedAt: this.#toEpoch(nrData.openedAt),
264
- closedAt: this.#toEpoch(nrData.closedAt)
265
- };
258
+ registerHandler('ws-complete', (nrData, targets = [undefined]) => {
259
+ targets.forEach(target => {
260
+ const event = {
261
+ ...nrData,
262
+ eventType: EVENT_TYPES.WS,
263
+ timestamp: this.#toEpoch(nrData.timestamp),
264
+ openedAt: this.#toEpoch(nrData.openedAt),
265
+ closedAt: this.#toEpoch(nrData.closedAt)
266
+ };
266
267
 
267
- // Report supportability metrics for WebSocket completion
268
- this.reportSupportabilityMetric('WebSocket/Completed/Seen');
269
- this.reportSupportabilityMetric('WebSocket/Completed/Bytes', stringify(event).length);
270
- this.addEvent(event);
268
+ // Report supportability metrics for WebSocket completion
269
+ this.reportSupportabilityMetric('WebSocket/Completed/Seen');
270
+ this.reportSupportabilityMetric('WebSocket/Completed/Bytes', stringify(event).length);
271
+ this.addEvent(event, target);
272
+ });
271
273
  }, this.featureName, this.ee);
272
274
  }
273
275
  if (!agentRef.init.feature_flags.includes('no_spv')) {
@@ -45,9 +45,9 @@ export class Instrument extends InstrumentBase {
45
45
  let historyEE;
46
46
  if (websocketsEnabled) {
47
47
  // this can apply outside browser scope such as in worker
48
- const websocketsEE = wrapWebSocket(this.ee);
49
- websocketsEE.on('ws', nrData => {
50
- handle('ws-complete', [nrData], undefined, this.featureName, this.ee);
48
+ const websocketsEE = wrapWebSocket(this.ee, agentRef);
49
+ websocketsEE.on('ws', (nrData, targets) => {
50
+ handle('ws-complete', [nrData, targets], undefined, this.featureName, this.ee);
51
51
  });
52
52
  }
53
53
  if (securityPolicyViolationEnabled) {
@@ -53,4 +53,12 @@
53
53
  * @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.
54
54
  */
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
+ */
63
+
56
64
  export default {};
@@ -80,7 +80,7 @@ function register(agentRef, target) {
80
80
  const timings = findScriptTimings();
81
81
 
82
82
  // Track MFE vitals for this entity
83
- const vitals = trackMFEVitals(target.id, timings);
83
+ const vitals = trackMFEVitals(target, timings);
84
84
  const attrs = {};
85
85
 
86
86
  // Only define attributes getter if it doesn't already exist
@@ -226,13 +226,21 @@ function register(agentRef, target) {
226
226
  timeToRegister: timings.registeredAt,
227
227
  // timestamp when register() was called
228
228
  // leave room to extend these with more data keys as needed
229
- 'nr.vitals.fcp.value': vitals.fcp?.value ?? null,
229
+ ...(vitals.fcp.value >= 0 && {
230
+ 'nr.vitals.fcp.value': vitals.fcp.value
231
+ }),
230
232
  // FCP vital object with value and metadata
231
- 'nr.vitals.lcp.value': vitals.lcp?.value ?? null,
233
+ ...(vitals.lcp.value >= 0 && {
234
+ 'nr.vitals.lcp.value': vitals.lcp.value
235
+ }),
232
236
  // LCP vital object with value and metadata
233
- 'nr.vitals.cls.value': vitals.cls?.value ?? null,
237
+ ...(vitals.cls.value >= 0 && {
238
+ 'nr.vitals.cls.value': vitals.cls.value
239
+ }),
234
240
  // CLS vital object with value and metadata
235
- 'nr.vitals.inp.value': vitals.inp?.value ?? null // INP vital object with value and metadata
241
+ ...(vitals.inp.value >= 0 && {
242
+ 'nr.vitals.inp.value': vitals.inp.value
243
+ }) // INP vital object with value and metadata
236
244
  };
237
245
  api.recordCustomEvent('MicroFrontEndTiming', eventData);
238
246
  }
@@ -1,12 +1,23 @@
1
1
  /**
2
2
  * Determines whether payload data should be captured based on the capture mode setting,
3
- * HTTP status code, and GraphQL error status.
3
+ * HTTP status code, and GraphQL error status. Will never capture agent's own payloads.
4
4
  * @param {string} captureMode - The capture mode setting ('none', 'all', or 'failures')
5
- * @param {number} statusCode - The HTTP status code
6
- * @param {boolean} hasGQLErrors - Whether the response contains GraphQL errors
5
+ * @param {Object} event - An object representing the AJAX event
6
+ * @param {number} event.statusCode - The HTTP status code
7
+ * @param {boolean} event.hasGQLErrors - Whether the response contains GraphQL errors
8
+ * @param {string} event.payloadHost - The host of the AJAX payload (includes port)
9
+ * @param {string} event.payloadHostname - The hostname of the AJAX payload
10
+ * @param {string} [event.payloadPathname=''] - The pathname of the AJAX payload
11
+ * @param {string[]} [beacons=[]] - Array of beacon hostnames to avoid capturing
7
12
  * @returns {boolean} True if payload should be captured
8
13
  */
9
- export function canCapturePayload(captureMode: string, statusCode: number, hasGQLErrors: boolean): boolean;
14
+ export function canCapturePayload(captureMode: string, { statusCode, hasGQLErrors, payloadHost, payloadHostname, payloadPathname }: {
15
+ statusCode: number;
16
+ hasGQLErrors: boolean;
17
+ payloadHost: string;
18
+ payloadHostname: string;
19
+ payloadPathname?: string | undefined;
20
+ }, beacons?: string[]): boolean;
10
21
  /**
11
22
  * Parses a query string into an object of key-value pairs.
12
23
  * @param {string} search a query string starting with "?" or without (ex. new URL(...).search)
@@ -1 +1 @@
1
- {"version":3,"file":"payloads.d.ts","sourceRoot":"","sources":["../../../../src/common/payloads/payloads.js"],"names":[],"mappings":"AAQA;;;;;;;GAOG;AACH,+CALW,MAAM,cACN,MAAM,gBACN,OAAO,GACL,OAAO,CAQnB;AAED;;;;GAIG;AACH,yCAHW,MAAM,GACJ,MAAM,GAAC,SAAS,CAgB5B;AAED;;;;;GAKG;AACH,+CAJW,MAAM,QACN,GAAC,GACC,OAAO,CA2BnB;AAED;;;;;;GAMG;AACH,uCAHW,GAAC,GACC,MAAM,CA2BlB;AAED;;;;;;GAMG;AACH,8EAHW,MAAM,GACJ;IAAC,SAAS,WAAW;IAAC,uBAAuB,WAAU;CAAC,CAiBpE"}
1
+ {"version":3,"file":"payloads.d.ts","sourceRoot":"","sources":["../../../../src/common/payloads/payloads.js"],"names":[],"mappings":"AAQA;;;;;;;;;;;;GAYG;AACH,+CAVW,MAAM,+EAEd;IAAsB,UAAU,EAAxB,MAAM;IACS,YAAY,EAA3B,OAAO;IACO,WAAW,EAAzB,MAAM;IACQ,eAAe,EAA7B,MAAM;IACS,eAAe;CACtC,YAAQ,MAAM,EAAE,GACN,OAAO,CAqBnB;AAED;;;;GAIG;AACH,yCAHW,MAAM,GACJ,MAAM,GAAC,SAAS,CAgB5B;AAED;;;;;GAKG;AACH,+CAJW,MAAM,QACN,GAAC,GACC,OAAO,CA2BnB;AAED;;;;;;GAMG;AACH,uCAHW,GAAC,GACC,MAAM,CA2BlB;AAED;;;;;;GAMG;AACH,8EAHW,MAAM,GACJ;IAAC,SAAS,WAAW;IAAC,uBAAuB,WAAU;CAAC,CAiBpE"}
@@ -1,5 +1,5 @@
1
1
  export function nullable(val: any, fn: any, comma: any): string;
2
2
  export function numeric(n: any, noDefault: any): string;
3
- export function getAddStringContext(obfuscator: any, truncator: any): (str: any) => string;
3
+ export function getAddStringContext(obfuscator: any, truncator: any): (str: any, obfuscate?: boolean) => string;
4
4
  export function addCustomAttributes(attrs: any, addString: any): any[];
5
5
  //# sourceMappingURL=bel-serializer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bel-serializer.d.ts","sourceRoot":"","sources":["../../../../src/common/serialize/bel-serializer.js"],"names":[],"mappings":"AAUA,gEAIC;AAED,wDAKC;AAED,2FAiBC;AAED,uEAwCC"}
1
+ {"version":3,"file":"bel-serializer.d.ts","sourceRoot":"","sources":["../../../../src/common/serialize/bel-serializer.js"],"names":[],"mappings":"AAUA,gEAIC;AAED,wDAKC;AAED,gHAiBC;AAED,uEAwCC"}
@@ -1,8 +1,86 @@
1
1
  /**
2
- * A helper method to warn to the console with New Relic: decoration
3
- * @param {string} message The primary message to warn
4
- * @param {*} [secondary] Secondary data to include, usually an error or object
2
+ * A helper method to warn to the console with New Relic: decoration.
3
+ *
4
+ * `code` corresponds to an entry in
5
+ * {@link https://github.com/newrelic/newrelic-browser-agent/blob/main/docs/warning-codes.md warning-codes.md}:
6
+ *
7
+ * | Code | Message |
8
+ * |------|---------|
9
+ * | 1 | An error occurred while setting a property of a Configurable |
10
+ * | 2 | An error occurred while setting a Configurable |
11
+ * | 3 | Setting a Configurable requires an object as input |
12
+ * | 4 | Setting a Configurable requires a model to set its initial properties |
13
+ * | 5 | An invalid session_replay.mask_selector was provided. \* will be used. |
14
+ * | 6 | An invalid session_replay.block_selector was provided and will not be used |
15
+ * | 7 | An invalid session_replay.mask_input_option was provided and will not be used |
16
+ * | 8 | Shared context requires an object as input |
17
+ * | 9 | An error occurred while setting SharedContext |
18
+ * | 10 | Failed to read from storage API |
19
+ * | 11 | Failed to write to the storage API |
20
+ * | 12 | An obfuscation replacement rule was detected missing a "regex" value. |
21
+ * | 13 | An obfuscation replacement rule contains a "regex" value with an invalid type (must be a string or RegExp) |
22
+ * | 14 | An obfuscation replacement rule contains a "replacement" value with an invalid type (must be a string) |
23
+ * | 15 | An error occurred while intercepting XHR |
24
+ * | 16 | Could not cast log message to string |
25
+ * | 17 | Could not calculate New Relic server time. Agent shutting down. |
26
+ * | 18 | RUM call failed. Agent shutting down. |
27
+ * | 19 | SPA scheduler is not initialized. Saved interaction is not sent! |
28
+ * | 20 | A problem occurred when starting up session manager. This page will not start or extend any session. |
29
+ * | 21 | Failed to initialize the agent. Could not determine the runtime environment. |
30
+ * | 22 | Failed to initialize all enabled instrument classes (agent aborted) - |
31
+ * | 23 | An unexpected issue occurred |
32
+ * | 24 | Something prevented the agent from instrumenting. |
33
+ * | 25 | Something prevented the agent from being downloaded. |
34
+ * | 26 | Failed to initialize instrument classes. |
35
+ * | 27 | Downloading runtime APIs failed... |
36
+ * | 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. |
37
+ * | 29 | Failed to wrap logger: invalid argument(s) |
38
+ * | 30 | Invalid log level |
39
+ * | 31 | Ignored log: Log is larger than maximum payload size |
40
+ * | 32 | Ignored log: Invalid message |
41
+ * | 33 | Session Replay Aborted |
42
+ * | 34 | Downloading and initializing a feature failed... |
43
+ * | 35 | Call to agent api failed. The API is not currently initialized. |
44
+ * | 36 | A feature is enabled but one or more dependent features have not been initialized. This may cause unintended consequences or missing data... |
45
+ * | 37 | Invalid feature name supplied. |
46
+ * | 38 | Call to api was made before agent fully initialized. |
47
+ * | 39 | Failed to execute setCustomAttribute. Name must be a string type. |
48
+ * | 40 | Failed to execute setCustomAttribute. Non-null value must be a string, number or boolean type. |
49
+ * | 41 | Failed to execute setUserId. Non-null value must be a string type. |
50
+ * | 42 | Failed to execute setApplicationVersion. Expected <String \| null> |
51
+ * | 43 | Agent not configured properly. |
52
+ * | 44 | Invalid object passed to generic event aggregate. Missing "eventType". |
53
+ * | 45 | An internal agent process failed to execute. |
54
+ * | 46 | A reserved eventType was provided to recordCustomEvent(...) -- The event was not recorded. |
55
+ * | 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. |
56
+ * | 48 | Supplied an invalid API target. Must be an <Object> that contains valid (string) id and name properties. |
57
+ * | 49 | Supplied API target is missing an entityGuid. Some APIs may not behave correctly without a valid entityGuid (ex. logs). |
58
+ * | 50 | Failed to connect. Cannot allow registered API. |
59
+ * | 51 | Container agent is not available to register with. Can not connect |
60
+ * | 52 | Unexpected problem encountered. There should be at least one app for harvest! |
61
+ * | 53 | Did not receive a valid entityGuid from connection response |
62
+ * | 54 | An experimental feature is being used. Support can not be offered for issues |
63
+ * | 55 | Register API has been disabled on the container agent |
64
+ * | 56 | Could not find a matching entity to store data |
65
+ * | 57 | Failed to execute measure. Arguments must have valid types. |
66
+ * | 58 | Failed to execute measure. Resulting duration must be non-negative. |
67
+ * | 59 | Session replay harvested before a session trace payload could be sent. This could be problematic for replays that rely on a trace |
68
+ * | 60 | Session trace aborted |
69
+ * | 61 | Timestamps must be non-negative and end time cannot be before start time. |
70
+ * | 62 | Timestamp must be a unix timestamp greater than the page origin time |
71
+ * | 63 | A single event was larger than the maximum allowed payload size |
72
+ * | 64 | Required globals have been mutated before being accessed by the browser agent. This can cause issues and should be avoided. |
73
+ * | 65 | Consent API argument must be boolean or undefined |
74
+ * | 66 | A new agent session has started |
75
+ * | 67 | The "spa" feature has been deprecated and disabled. Please use/import "soft_navigations" instead for tracking of BrowserInteraction data. |
76
+ * | 68 | API has been deregistered and can no longer be used. Call "register" API again with credentials to start over. |
77
+ * | 69 | More than one Browser agent is running on the page |
78
+ * | 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. |
79
+ * | 71 | An invalid feature mode was detected and set to "off". |
80
+ *
81
+ * @param {number} code The warning code to emit, which will be used to link to the warning code documentation
82
+ * @param {*} [secondary] Secondary data to include, usually an extra message, error or object
5
83
  * @returns
6
84
  */
7
- export function warn(code: any, secondary?: any): void;
85
+ export function warn(code: number, secondary?: any): void;
8
86
  //# sourceMappingURL=console.d.ts.map